diff --git a/src/execution/incremental/WorkQueue.ts b/src/execution/incremental/WorkQueue.ts index ade8b3dc6f..b23a642520 100644 --- a/src/execution/incremental/WorkQueue.ts +++ b/src/execution/incremental/WorkQueue.ts @@ -584,7 +584,12 @@ export function createWorkQueue< for (const group of task.groups) { const groupNode = groupNodes.get(group); if (groupNode) { - groupFailureEvents.push(finishGroupFailure(group, groupNode, error)); + // A shared task can fail in a child group before it is released. + const isReleased = rootGroups.has(group); + const failureEvent = finishGroupFailure(group, groupNode, error); + if (isReleased) { + groupFailureEvents.push(failureEvent); + } } } return groupFailureEvents; diff --git a/src/execution/incremental/__tests__/WorkQueue-test.ts b/src/execution/incremental/__tests__/WorkQueue-test.ts index d70e40b2ff..781c1b2500 100644 --- a/src/execution/incremental/__tests__/WorkQueue-test.ts +++ b/src/execution/incremental/__tests__/WorkQueue-test.ts @@ -262,6 +262,41 @@ describe('WorkQueue', () => { expect(childRanSpy.callCount).to.equal(1); }); + it('does not emit failure for an unannounced group sharing a failed task', async () => { + const failingRoot: TestGroup = { parent: undefined }; + const otherRoot: TestGroup = { parent: undefined }; + const child: TestGroup = { parent: otherRoot }; + const error = new Error('shared failure'); + const sharedTask = makeTask([failingRoot, child], () => { + throw error; + }); + const otherTask = makeTask([otherRoot], async () => { + await resolveOnNextTick(); + return { value: 'other' }; + }); + + const workQueue = await collectWorkRun({ + groups: [failingRoot, otherRoot, child], + tasks: [sharedTask, otherTask], + }); + + expect(workQueue).to.deep.equal({ + initialGroups: [failingRoot, otherRoot], + initialStreams: [], + events: [ + { kind: 'GROUP_FAILURE', group: failingRoot, error }, + { kind: 'GROUP_VALUES', group: otherRoot, values: ['other'] }, + { + kind: 'GROUP_SUCCESS', + group: otherRoot, + newGroups: [], + newStreams: [], + }, + { kind: 'WORK_QUEUE_TERMINATION' }, + ], + }); + }); + it('integrates work object returned by task', async () => { const root: TestGroup = { parent: undefined }; const child: TestGroup = { parent: root }; diff --git a/src/execution/incremental/__tests__/defer-test.ts b/src/execution/incremental/__tests__/defer-test.ts index 4b9676398c..a9a3b6c6fa 100644 --- a/src/execution/incremental/__tests__/defer-test.ts +++ b/src/execution/incremental/__tests__/defer-test.ts @@ -137,6 +137,8 @@ const query = new GraphQLObjectType({ }, a: { type: a }, g: { type: g }, + slow: { type: GraphQLString }, + bad: { type: new GraphQLNonNull(GraphQLString) }, }, name: 'Query', }); @@ -1889,6 +1891,50 @@ describe('Execute: defer directive', () => { ]); }); + it('Does not complete an unannounced nested group when a shared task fails', async () => { + const document = parse(` + { + ... @defer(label: "R") { bad } + ... @defer(label: "P") { + slow + ... @defer(label: "C") { bad } + } + } + `); + const result = await complete(document, { slow: 'ok', bad: null }); + + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: '0', path: [], label: 'R' }, + { id: '1', path: [], label: 'P' }, + ], + hasNext: true, + }, + { + incremental: [{ id: '1', data: { slow: 'ok' } }], + completed: [ + { + id: '0', + errors: [ + { + message: 'Cannot return null for non-nullable field Query.bad.', + locations: [ + { line: 3, column: 34 }, + { line: 6, column: 36 }, + ], + path: ['bad'], + }, + ], + }, + { id: '1' }, + ], + hasNext: false, + }, + ]); + }); + it('Handles cancelling child deferred fragments if parent fragment fails', async () => { const document = parse(` query {