Repository navigation
fix(incremental): prevent lost or delayed successful shared results - #4868
Merged
Merged
Conversation
|
@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel. A member of the Team first needs to authorize it. |
Follow-up to merged PR [graphql#4859](graphql#4859). Thanks to Duckki Oe (@duckki) for pointing out these issues and providing an initial fix. That PR documented these bugs with skipped regression tests. This commit implements the successful-result fix and enables its regression tests. Additional integration tests cover delayed delivery and later owners. Previously, we could skip a nested deferred fragment whose shared fields had succeeded before its parent finished. That was incorrect when the shared data had not yet been delivered. If the other fragment failed on a separate field, the data was lost; if its separate fields succeeded later, the data was delayed. This commit announces the nested fragment and delivers its successful shared data when its parent completes. The data, including any shared stream, can therefore reach the client without waiting for the other fragment's separate fields. ## @defer: another fragment fails on a separate field ```graphql { hero { ... @defer(label: "R") { nonNullName id } ... @defer(label: "P") { name ... @defer(label: "C") { id } } } } ``` R and C share `id`, which returns `1`. R fails because its own `nonNullName` returns `null`. After that error is sent, P's `name` resolves to `"Luke"`. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` Then R fails: ```json { "hasNext": true, "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7} ], "path": ["hero", "nonNullName"] } ] } ] } ``` **Before this fix:** Previously, finishing P ended the response without announcing C or sending `id`, even though `id` had succeeded. Response 3: ```json { "hasNext": false, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ {"id": "1"} ] } ``` **After this fix:** This commit announces C and delivers `id` when P finishes, so R's separate failure no longer loses C's data. Response 3: ```json { "hasNext": false, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}}, {"id": "2", "data": {"id": "1"}} ], "completed": [ {"id": "1"}, {"id": "2"} ] } ``` ## @defer: another fragment's separate field is slower ```graphql { hero { ... @defer(label: "R") { nonNullName id } ... @defer(label: "P") { name ... @defer(label: "C") { id } } } } ``` Here all three fields succeed. `id` returns `1` immediately. After the initial response, P's `name` resolves to `"Luke"`. R's `nonNullName` stays pending until after P's response, then also resolves to `"Luke"`. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` **Before this fix:** Previously, P completed without announcing C. The shared `id` wasn't sent until R's separate `nonNullName` finished, unnecessarily delaying data that C was ready to deliver. Response 2: ```json { "hasNext": true, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ {"id": "1"} ] } ``` Response 3: ```json { "hasNext": false, "incremental": [ {"id": "0", "data": {"nonNullName": "Luke"}}, {"id": "0", "data": {"id": "1"}} ], "completed": [ {"id": "0"} ] } ``` **After this fix:** This commit delivers `id` through C as soon as P finishes. R sends its separate field later. Response 2: ```json { "hasNext": true, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}}, {"id": "2", "data": {"id": "1"}} ], "completed": [ {"id": "1"}, {"id": "2"} ] } ``` Response 3: ```json { "hasNext": false, "incremental": [ {"id": "0", "data": {"nonNullName": "Luke"}} ], "completed": [ {"id": "0"} ] } ``` ## @stream: a separate field fails before the list is delivered ```graphql { nestedObject { ... @defer(label: "R") { nonNullScalarField ...Friends } ... @defer(label: "P") { scalarField ... @defer(label: "C") { ...Friends } } } } fragment Friends on NestedObject { nestedFriendList @stream(initialCount: 0) { name } } ``` The shared `nestedFriendList` returns Luke, Han, and Leia immediately. R fails because its own `nonNullScalarField` returns `null`. After that error is sent, P's `scalarField` resolves to `"parent"`. Both versions start with: ```json { "data": {"nestedObject": {}}, "pending": [ {"id": "0", "path": ["nestedObject"], "label": "R"}, {"id": "1", "path": ["nestedObject"], "label": "P"} ], "hasNext": true } ``` Then R fails: ```json { "hasNext": true, "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field NestedObject.nonNullScalarField.", "locations": [ {"line": 4, "column": 7} ], "path": ["nestedObject", "nonNullScalarField"] } ] } ] } ``` **Before this fix:** Previously, P completed and the response ended. C and the stream were never announced, so neither the empty list nor its items were sent. Response 3: ```json { "hasNext": false, "incremental": [ {"id": "1", "data": {"scalarField": "parent"}} ], "completed": [ {"id": "1"} ] } ``` **After this fix:** This commit announces C and the stream with P's result. C sends the empty list, then the stream sends all three friends, so R's separate failure no longer loses the list. Response 3: ```json { "hasNext": true, "pending": [ {"id": "2", "path": ["nestedObject"], "label": "C"}, {"id": "3", "path": ["nestedObject", "nestedFriendList"]} ], "incremental": [ {"id": "1", "data": {"scalarField": "parent"}}, {"id": "2", "data": {"nestedFriendList": []}} ], "completed": [ {"id": "1"}, {"id": "2"} ] } ``` Response 4: ```json { "hasNext": false, "incremental": [ {"id": "3", "items": [{"name": "Luke"}, {"name": "Han"}, {"name": "Leia"}]} ], "completed": [ {"id": "3"} ] } ``` Co-authored-by: Duckki Oe <duckki.dev@gmail.com>
yaacovCR
force-pushed
the
incremental-fix1
branch
from
October 16, 2026 06:59
1df0f01 to
8064e1d
Compare
yaacovCR
added a commit
to yaacovCR/graphql-js
that referenced
this pull request
Oct 16, 2026
Follow-up to merged PR [graphql#4868](graphql#4868). Thanks to Duckki Oe (@duckki) for identifying these issues and providing an initial fix in [graphql#4859](graphql#4859). PR graphql#4868 fixed successful shared results. A nested fragment can now deliver its ready data, including a shared stream, as soon as its parent finishes. Another fragment's separate failure or slower fields no longer cause that data to be lost or delayed. Previously, even after that fix, a shared non-null field failure could complete a nested deferred fragment before it was announced, although its parent's separate fields succeeded. This was incorrect: the response referred to an ID that the client had never received in `pending`. This commit waits until the nested fragment is announced before sending its error completion. The client therefore receives its `pending` entry before its completion, including when both appear in the same response. If several shared fields have already failed, the fragment completes once with one shared error. Its parent's successful data is still delivered. Late outcomes and descendants belonging only to failed fragments are discarded. Shared work needed by a surviving fragment remains available. This enables the remaining regression tests introduced in graphql#4859 and adds integration coverage for error ordering, late outcomes, and shared streams whose remaining fragment fails. ## @defer: the shared field fails while the parent succeeds ```graphql { hero { ... @defer(label: "R") { nonNullName } ... @defer(label: "P") { name ... @defer(label: "C") { nonNullName } } } } ``` `nonNullName` returns `null`, failing R and C. P's separate `name` returns `"Luke"` successfully. Both results are available immediately. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` **Before this fix:** Previously, C completed as ID `"2"` even though only IDs `"0"` and `"1"` had been announced. Response 2: ```json { "hasNext": false, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, { "id": "2", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, {"id": "1"} ] } ``` **After this fix:** This commit announces C as ID `"2"` before sending its error completion, so the client can identify the failed fragment. P still returns its successful `name`. Response 2: ```json { "hasNext": false, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, {"id": "1"}, { "id": "2", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] } ] } ``` ## @defer: two shared fields have already failed ```graphql { hero { ... @defer(label: "R1") { first: nonNullName } ... @defer(label: "R2") { second: nonNullName } ... @defer(label: "P") { name ... @defer(label: "C") { first: nonNullName second: nonNullName } } } } ``` `name` returns `"Luke"`. Both `first` and `second` return `null`. C shares `first` with R1 and `second` with R2; both have failed by the time P completes. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R1"}, {"id": "1", "path": ["hero"], "label": "R2"}, {"id": "2", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` **Before this fix:** Previously, C completed as ID `"3"` without a `pending` entry. Response 2: ```json { "hasNext": false, "incremental": [ {"id": "2", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "1", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 7, "column": 7}, {"line": 13, "column": 9} ], "path": ["hero", "second"] } ] }, {"id": "2"} ] } ``` **After this fix:** This commit announces C before completing it once with a shared error. In this example, its completion reports the `first` error. R1 and R2 each report their own error, and P delivers `name`. Response 2: ```json { "hasNext": false, "pending": [ {"id": "3", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "2", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "1", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 7, "column": 7}, {"line": 13, "column": 9} ], "path": ["hero", "second"] } ] }, {"id": "2"}, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] } ] } ``` Co-authored-by: Duckki Oe <duckki.dev@gmail.com>
yaacovCR
added a commit
that referenced
this pull request
Oct 17, 2026
…#4869) Follow-up to merged PR [#4868](#4868). Thanks to Duckki Oe (@duckki) for identifying these issues and providing an initial fix in [#4859](#4859). PR #4868 fixed successful shared results. A nested fragment can now deliver its ready data, including a shared stream, as soon as its parent finishes. Another fragment's separate failure or slower fields no longer cause that data to be lost or delayed. Previously, even after that fix, a shared non-null field failure could complete a nested deferred fragment before it was announced, although its parent's separate fields succeeded. This was incorrect: the response referred to an ID that the client had never received in `pending`. This PR waits until the nested fragment is announced before sending its error completion. The client therefore receives its `pending` entry before its completion, including when both appear in the same response. If several shared fields have already failed, the fragment completes once with one shared error. Its parent's successful data is still delivered. Late outcomes and descendants belonging only to failed fragments are discarded. Shared work needed by a surviving fragment remains available. This enables the remaining regression tests introduced in #4859 and adds integration coverage for error ordering, late outcomes, and shared streams whose remaining fragment fails. ## @defer: the shared field fails while the parent succeeds ```graphql { hero { ... @defer(label: "R") { nonNullName } ... @defer(label: "P") { name ... @defer(label: "C") { nonNullName } } } } ``` `nonNullName` returns `null`, failing R and C. P's separate `name` returns `"Luke"` successfully. Both results are available immediately. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` **Before this fix:** Previously, C completed as ID `"2"` even though only IDs `"0"` and `"1"` had been announced. Response 2: ```json { "hasNext": false, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, { "id": "2", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, {"id": "1"} ] } ``` **After this fix:** This commit announces C as ID `"2"` before sending its error completion, so the client can identify the failed fragment. P still returns its successful `name`. Response 2: ```json { "hasNext": false, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] }, {"id": "1"}, { "id": "2", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 9, "column": 9} ], "path": ["hero", "nonNullName"] } ] } ] } ``` ## @defer: two shared fields have already failed ```graphql { hero { ... @defer(label: "R1") { first: nonNullName } ... @defer(label: "R2") { second: nonNullName } ... @defer(label: "P") { name ... @defer(label: "C") { first: nonNullName second: nonNullName } } } } ``` `name` returns `"Luke"`. Both `first` and `second` return `null`. C shares `first` with R1 and `second` with R2; both have failed by the time P completes. Both versions start with: ```json { "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R1"}, {"id": "1", "path": ["hero"], "label": "R2"}, {"id": "2", "path": ["hero"], "label": "P"} ], "hasNext": true } ``` **Before this fix:** Previously, C completed as ID `"3"` without a `pending` entry. Response 2: ```json { "hasNext": false, "incremental": [ {"id": "2", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "1", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 7, "column": 7}, {"line": 13, "column": 9} ], "path": ["hero", "second"] } ] }, {"id": "2"} ] } ``` **After this fix:** This PR announces C before completing it once with a shared error. In this example, its completion reports the `first` error. R1 and R2 each report their own error, and P delivers `name`. Response 2: ```json { "hasNext": false, "pending": [ {"id": "3", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "2", "data": {"name": "Luke"}} ], "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] }, { "id": "1", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 7, "column": 7}, {"line": 13, "column": 9} ], "path": ["hero", "second"] } ] }, {"id": "2"}, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7}, {"line": 12, "column": 9} ], "path": ["hero", "first"] } ] } ] } ``` Co-authored-by: Duckki Oe <duckki.dev@gmail.com>
yaacovCR
added a commit
to yaacovCR/graphql-js
that referenced
this pull request
Oct 18, 2026
This proposal extracts the multiple-error reporting idea from Duckki Oe's original implementation in [graphql#4859](graphql#4859). This is an optional follow-up to [graphql#4868](graphql#4868) and [graphql#4869](graphql#4869). Those changes already prevent lost or delayed successful results and completions for unannounced fragments, in line with graphql#4859. This PR extracts an additional element from graphql#4859, i.e. how many errored tasks to report for a delivery group with multiple shared tasks that have errored prior to the start of a deferred group. Currently, when several shared tasks have failed before a deferred group starts, the group completes with the first replayed error. This proposal includes every failure already cached by its retained, started tasks in that one completion. It exposes more information we already have, without waiting for other work. For example: ```graphql { hero { ... @defer(label: "R1") { first: nonNullName } ... @defer(label: "R2") { second: nonNullName } ... @defer(label: "P") { name ... @defer(label: "C") { first: nonNullName second: nonNullName } } } } ``` `first` and `second` both return `null`, failing R1 and R2 while P's `name` is still pending. R1 and R2 each report their own error. When `name` resolves to `"Luke"`, both versions announce C and complete P successfully. The final response on `17.x.x` is: ```json { "hasNext": false, "pending": [ { "id": "3", "path": [ "hero" ], "label": "C" } ], "incremental": [ { "id": "2", "data": { "name": "Luke" } } ], "completed": [ { "id": "2" }, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ { "line": 4, "column": 7 }, { "line": 12, "column": 9 } ], "path": [ "hero", "first" ] } ] } ] } ``` With this proposal, the final response is: ```json { "hasNext": false, "pending": [ { "id": "3", "path": [ "hero" ], "label": "C" } ], "incremental": [ { "id": "2", "data": { "name": "Luke" } } ], "completed": [ { "id": "2" }, { "id": "3", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ { "line": 4, "column": 7 }, { "line": 12, "column": 9 } ], "path": [ "hero", "first" ] }, { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ { "line": 7, "column": 7 }, { "line": 13, "column": 9 } ], "path": [ "hero", "second" ] } ] } ] } ``` The implementation moves cached-failure inspection from `startTask` to `startGroup`. It adds an additional pass over a group's tasks before starting new work to check for cached failures. This also ensures that a group already known to have failed does not start unnecessary tasks. Unlike the original implementation's retained group errors, this version reads the existing computation results. It adds no persistent group-error storage or WeakSet. Co-authored-by: Duckki Oe <duckki.dev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to merged PR #4859.
Thanks to Duckki Oe (@duckki) for pointing out these issues and providing an initial fix.
That PR documented these bugs with skipped regression tests. This PR implements the successful-result fix and enables its regression tests. Additional integration tests cover delayed delivery and later owners.
Previously, we could skip a nested deferred fragment whose shared fields had succeeded before its parent finished. That was incorrect when the shared data had not yet been delivered. If the other fragment failed on a separate field, the data was lost; if its separate fields succeeded later, the data was delayed.
This PR announces the nested fragment and delivers its successful shared data when its parent completes. The data, including any shared stream, can therefore reach the client without waiting for the other fragment's separate fields.
@defer: another fragment fails on a separate field
{ hero { ... @defer(label: "R") { nonNullName id } ... @defer(label: "P") { name ... @defer(label: "C") { id } } } }R and C share
id, which returns1. R fails because its ownnonNullNamereturnsnull. After that error is sent, P'snameresolves to"Luke".Both versions start with:
{ "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true }Then R fails:
{ "hasNext": true, "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field Hero.nonNullName.", "locations": [ {"line": 4, "column": 7} ], "path": ["hero", "nonNullName"] } ] } ] }Before this fix:
Previously, finishing P ended the response without announcing C or sending
id, even thoughidhad succeeded.Response 3:
{ "hasNext": false, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ {"id": "1"} ] }After this fix:
This commit announces C and delivers
idwhen P finishes, so R's separate failure no longer loses C's data.Response 3:
{ "hasNext": false, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}}, {"id": "2", "data": {"id": "1"}} ], "completed": [ {"id": "1"}, {"id": "2"} ] }@defer: another fragment's separate field is slower
{ hero { ... @defer(label: "R") { nonNullName id } ... @defer(label: "P") { name ... @defer(label: "C") { id } } } }Here all three fields succeed.
idreturns1immediately. After the initial response, P'snameresolves to"Luke". R'snonNullNamestays pending until after P's response, then also resolves to"Luke".Both versions start with:
{ "data": {"hero": {}}, "pending": [ {"id": "0", "path": ["hero"], "label": "R"}, {"id": "1", "path": ["hero"], "label": "P"} ], "hasNext": true }Before this fix:
Previously, P completed without announcing C. The shared
idwasn't sent until R's separatenonNullNamefinished, unnecessarily delaying data that C was ready to deliver.Response 2:
{ "hasNext": true, "incremental": [ {"id": "1", "data": {"name": "Luke"}} ], "completed": [ {"id": "1"} ] }Response 3:
{ "hasNext": false, "incremental": [ {"id": "0", "data": {"nonNullName": "Luke"}}, {"id": "0", "data": {"id": "1"}} ], "completed": [ {"id": "0"} ] }After this fix:
This commit delivers
idthrough C as soon as P finishes. R sends its separate field later.Response 2:
{ "hasNext": true, "pending": [ {"id": "2", "path": ["hero"], "label": "C"} ], "incremental": [ {"id": "1", "data": {"name": "Luke"}}, {"id": "2", "data": {"id": "1"}} ], "completed": [ {"id": "1"}, {"id": "2"} ] }Response 3:
{ "hasNext": false, "incremental": [ {"id": "0", "data": {"nonNullName": "Luke"}} ], "completed": [ {"id": "0"} ] }@stream: a separate field fails before the list is delivered
{ nestedObject { ... @defer(label: "R") { nonNullScalarField ...Friends } ... @defer(label: "P") { scalarField ... @defer(label: "C") { ...Friends } } } } fragment Friends on NestedObject { nestedFriendList @stream(initialCount: 0) { name } }The shared
nestedFriendListreturns Luke, Han, and Leia immediately. R fails because its ownnonNullScalarFieldreturnsnull. After that error is sent, P'sscalarFieldresolves to"parent".Both versions start with:
{ "data": {"nestedObject": {}}, "pending": [ {"id": "0", "path": ["nestedObject"], "label": "R"}, {"id": "1", "path": ["nestedObject"], "label": "P"} ], "hasNext": true }Then R fails:
{ "hasNext": true, "completed": [ { "id": "0", "errors": [ { "message": "Cannot return null for non-nullable field NestedObject.nonNullScalarField.", "locations": [ {"line": 4, "column": 7} ], "path": ["nestedObject", "nonNullScalarField"] } ] } ] }Before this fix:
Previously, P completed and the response ended. C and the stream were never announced, so neither the empty list nor its items were sent.
Response 3:
{ "hasNext": false, "incremental": [ {"id": "1", "data": {"scalarField": "parent"}} ], "completed": [ {"id": "1"} ] }After this fix:
This commit announces C and the stream with P's result. C sends the empty list, then the stream sends all three friends, so R's separate failure no longer loses the list.
Response 3:
{ "hasNext": true, "pending": [ {"id": "2", "path": ["nestedObject"], "label": "C"}, {"id": "3", "path": ["nestedObject", "nestedFriendList"]} ], "incremental": [ {"id": "1", "data": {"scalarField": "parent"}}, {"id": "2", "data": {"nestedFriendList": []}} ], "completed": [ {"id": "1"}, {"id": "2"} ] }Response 4:
{ "hasNext": false, "incremental": [ {"id": "3", "items": [{"name": "Luke"}, {"name": "Han"}, {"name": "Leia"}]} ], "completed": [ {"id": "3"} ] }Co-authored-by: Duckki Oe duckki.dev@gmail.com