Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/execution/incremental/WorkQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions src/execution/incremental/__tests__/WorkQueue-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
46 changes: 46 additions & 0 deletions src/execution/incremental/__tests__/defer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ const query = new GraphQLObjectType({
},
a: { type: a },
g: { type: g },
slow: { type: GraphQLString },
bad: { type: new GraphQLNonNull(GraphQLString) },
},
name: 'Query',
});
Expand Down Expand Up @@ -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 {
Expand Down