Yielding batches of payloads from defer/stream GraphQL requests #41
Replies: 8 comments 14 replies
-
ImplementationSee yaacovCR/graphql-js#154 for a worked example Suggested type signature for execute:export function execute(
args: ExecutionArgs,
): PromiseOrValue<
| ExecutionResult
| AsyncGenerator<ReadonlyArray<AsyncExecutionResult>, void, void>
> {
...
} Suggested type signature for subscribeSame as above. Note that Note also that a choice was made to simplify the signature for subscriptions, as the signature could have theoretically been: export function subscribe(
args: ExecutionArgs,
): PromiseOrValue<
| ExecutionResult
| AsyncGenerator<ExecutionResult | ReadonlyArray<AsyncExecutionResult>, void, void>
> {
...
} If a subscription operation does not use defer or stream, it should yield only single ExecutionResults rather than arrays. Even if it does include defer/stream, depending on |
Beta Was this translation helpful? Give feedback.
-
My very strong preference is for below Option E (as an extension/variation of option D), where defer/stream payloads are wrapped, and stream payloads are also combined: This is an easy pickup of the third type of batching discussed within #38: For the following query from the test suite, modified only as to add labels: query {
nestedObject {
... DeferFragment @defer(label: "Defer")
}
}
fragment DeferFragment on NestedObject {
slowField
asyncIterableList @stream(initialCount: 0, label: "Stream") {
name
}
} {
data: {
nestedObject: {},
},
hasNext: true,
} {
incremental: [
{
data: {
slowField: 'slow',
asyncIterableList: [],
},
label: 'Defer',
path: ['nestedObject'],
},
{
data: [{ name: 'Luke' }, { name: 'Han' }, { name: 'Leia' }],
path: ['nestedObject', 'asyncIterableList', 0],
label: 'Stream',
hasNext: false,
},
],
hasNext: false,
} The idea is that the emitted payloads within the Pros of option E (with 2 and 3 also true for option D):
|
Beta Was this translation helpful? Give feedback.
-
Also consider Option F: {
incremental: [
{
data: {
slowField: 'slow',
asyncIterableList: [{ name: 'Luke' }, { name: 'Han' }, { name: 'Leia' }],
},
label: 'Defer',
path: ['nestedObject'],
},
{
label: 'Stream',
hasNext: false,
},
],
hasNext: false,
} This relates to the third point above: the server/executor also "knows" that the Note:
Consider also nested defer behavior: query HeroNameQuery {
hero {
id
...NameFragment @defer
}
}
fragment NameFragment on Hero {
name
friends {
...NestedFragment @defer
}
}
fragment NestedFragment on Friend {
name
} For options D and E, the first payload is the same. {
data: {
hero: { id: '1' },
},
hasNext: true,
} For option E, the second payload could be: {
incremental: [
{
data: {
name: 'Luke',
friends: [{}, {}, {}],
},
path: ['hero'],
},
{
data: { name: 'Han' },
path: ['hero', 'friends', 0],
},
{
data: { name: 'Leia' },
path: ['hero', 'friends', 1],
},
{
data: { name: 'C-3PO' },
path: ['hero', 'friends', 2],
},
],
hasNext: false,
} While for option F, it could be: {
incremental: [
{
data: {
name: 'Luke',
friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
},
path: ['hero'],
},
],
hasNext: false,
} I think E is better from a typings perspective. To avoid the verbosity, you could slightly rewrite the query, assuming you want the results to all come in at once: query HeroNameQuery {
hero {
id
...NameFragment @defer
}
}
fragment NameFragment on Hero {
name
...NestedFragment @defer
}
fragment NestedFragment on Friend {
friends {
name
}
} We could also purposefully not specify between D, E, and F.... |
Beta Was this translation helpful? Give feedback.
-
I favour option D; I can even see this being extended to subscriptions. It's common to have a subscription that's can sometimes kick out events at a high rate such that it can, when implemented naively, cause significant re-rendering issues on the client. Allowing the subscription to batch these payloads together into one payload to deliver to the client could be nice; but it raises these concerns:
(Point 2 is why I raise this comment; though I know it's bikeshedding!) |
Beta Was this translation helpful? Give feedback.
-
If subsequent payloads are ready at the same time the initial payload is ready, what does option D look like for the initial payload? Something like this? query {
hero {
name
heroFriends {
id
name
... @defer {
homeWorld
}
}
}
} First Payload {
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [{ "line": 6, "column": 7 }],
"path": ["hero", "heroFriends", 1, "name"]
}
],
"data": {
"hero": {
"name": "R2-D2",
"heroFriends": [
{
"id": "1000",
"name": "Luke Skywalker"
},
{
"id": "1002",
"name": null
},
{
"id": "1003",
"name": "Leia Organa"
}
]
}
},
"incremental": [
{
"path": ["hero", "heroFriends", 0],
"data": {
"homeWorld": "Tatooine",
}
},
{
"path": ["hero", "heroFriends", 1],
"data": {
"homeWorld": "Corellia",
}
}
],
"hasNext": true
} Second Payload {
"incremental": [
{
"path": ["hero", "heroFriends", 2],
"data": {
"homeWorld": "Alderaan",
}
}
],
"hasNext": false
} |
Beta Was this translation helpful? Give feedback.
-
I updated option D in the description to
Discussion around a |
Beta Was this translation helpful? Give feedback.
-
It might be helpful for clients to have hard-baked into the response which are the non-incremental fields so that this data could be updated quicker than the stuff in the incremental section. |
Beta Was this translation helpful? Give feedback.
-
Has there been a resolution on this discussion? |
Beta Was this translation helpful? Give feedback.
-
(Continued from #38)
Context
This is not to discuss the payload format of GraphQL responses (e.g. the object containing the
data
,errors
,path
,label
,extensions
fields), but rather how we specify that these payloads are delivered to clients. What's decided here will affect both server implementations (e.g. return type ofexecute
andgraphql
functions in graphql-js) and the GraphQL specification (we describe how payloads should be yielded to clients).At the May 2022 WG meeting we discussed the benefits and tradeoffs of yielding individual payloads vs all multiple payloads.
It was determined that there are benefits to yielding lists of payloads. By yielding lists of payloads, clients can do one render pass for each list of payloads that are received. If we yield single payloads, clients would either render on receipt of each payload or have to insert a debouncing delay to see if additional payloads are being sent in quick succession. If each payload is sent individually, it could overwhelm clients that try to re-render UI on each payload when many are sent in rapid succession.
It's important to consider the common cases, it may be rare that multiple payloads are batched together. If 99% is going to be array of size 1, 99% of users are going to be wondering why that array is there.
The following options have been discussed:
Option A: A Single AsyncExecutionResult
Option B: All Available AsyncExecutionResults
Option C: Single payload or list of payloads
Option D:
@defer
/@stream
payloads are wrapped in an object with anincremental
array field.hasNext
is hoisted to the wrapping object. The initial payload may optional also contain the "incremental".Example:
References
Descision
Discussed in the July 2022 WG meeting and decided to move forward with Option D
Beta Was this translation helpful? Give feedback.
All reactions