Skip to content

Commit 99edfea

Browse files
committed
cancel execution despite pending resolvers
1 parent c16d429 commit 99edfea

File tree

3 files changed

+210
-53
lines changed

3 files changed

+210
-53
lines changed

src/execution/__tests__/abort-signal-test.ts

Lines changed: 149 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { parse } from '../../language/parser.js';
99

1010
import { buildSchema } from '../../utilities/buildASTSchema.js';
1111

12-
import { execute, experimentalExecuteIncrementally } from '../execute.js';
12+
import {
13+
execute,
14+
experimentalExecuteIncrementally,
15+
subscribe,
16+
} from '../execute.js';
1317
import type {
1418
InitialIncrementalExecutionResult,
1519
SubsequentIncrementalExecutionResult,
@@ -52,12 +56,17 @@ const schema = buildSchema(`
5256
5357
type Query {
5458
todo: Todo
59+
nonNullableTodo: Todo!
5560
}
5661
5762
type Mutation {
5863
foo: String
5964
bar: String
6065
}
66+
67+
type Subscription {
68+
foo: String
69+
}
6170
`);
6271

6372
describe('Execute: Cancellation', () => {
@@ -300,6 +309,97 @@ describe('Execute: Cancellation', () => {
300309
});
301310
});
302311

312+
it('should stop the execution when aborted despite a hanging resolver', async () => {
313+
const abortController = new AbortController();
314+
const document = parse(`
315+
query {
316+
todo {
317+
id
318+
author {
319+
id
320+
}
321+
}
322+
}
323+
`);
324+
325+
const resultPromise = execute({
326+
document,
327+
schema,
328+
abortSignal: abortController.signal,
329+
rootValue: {
330+
todo: () =>
331+
new Promise(() => {
332+
/* will never resolve */
333+
}),
334+
},
335+
});
336+
337+
abortController.abort();
338+
339+
const result = await resultPromise;
340+
341+
expect(result.errors?.[0].originalError?.name).to.equal('AbortError');
342+
343+
expectJSON(result).toDeepEqual({
344+
data: {
345+
todo: null,
346+
},
347+
errors: [
348+
{
349+
message: 'This operation was aborted',
350+
path: ['todo'],
351+
locations: [{ line: 3, column: 9 }],
352+
},
353+
],
354+
});
355+
});
356+
357+
it('should stop the execution when aborted with proper null bubbling', async () => {
358+
const abortController = new AbortController();
359+
const document = parse(`
360+
query {
361+
nonNullableTodo {
362+
id
363+
author {
364+
id
365+
}
366+
}
367+
}
368+
`);
369+
370+
const resultPromise = execute({
371+
document,
372+
schema,
373+
abortSignal: abortController.signal,
374+
rootValue: {
375+
nonNullableTodo: async () =>
376+
Promise.resolve({
377+
id: '1',
378+
text: 'Hello, World!',
379+
/* c8 ignore next */
380+
author: () => expect.fail('Should not be called'),
381+
}),
382+
},
383+
});
384+
385+
abortController.abort();
386+
387+
const result = await resultPromise;
388+
389+
expect(result.errors?.[0].originalError?.name).to.equal('AbortError');
390+
391+
expectJSON(result).toDeepEqual({
392+
data: null,
393+
errors: [
394+
{
395+
message: 'This operation was aborted',
396+
path: ['nonNullableTodo'],
397+
locations: [{ line: 3, column: 9 }],
398+
},
399+
],
400+
});
401+
});
402+
303403
it('should stop deferred execution when aborted', async () => {
304404
const abortController = new AbortController();
305405
const document = parse(`
@@ -353,14 +453,12 @@ describe('Execute: Cancellation', () => {
353453
const abortController = new AbortController();
354454
const document = parse(`
355455
query {
356-
todo {
357-
id
358-
... on Todo @defer {
456+
... on Query @defer {
457+
todo {
458+
id
359459
text
360460
author {
361-
... on Author @defer {
362-
id
363-
}
461+
id
364462
}
365463
}
366464
}
@@ -382,41 +480,27 @@ describe('Execute: Cancellation', () => {
382480
abortController.signal,
383481
);
384482

385-
await resolveOnNextTick();
386-
await resolveOnNextTick();
387-
await resolveOnNextTick();
388-
389483
abortController.abort();
390484

391485
const result = await resultPromise;
392486

393487
expectJSON(result).toDeepEqual([
394488
{
395-
data: {
396-
todo: {
397-
id: '1',
398-
},
399-
},
400-
pending: [{ id: '0', path: ['todo'] }],
489+
data: {},
490+
pending: [{ id: '0', path: [] }],
401491
hasNext: true,
402492
},
403493
{
404494
incremental: [
405495
{
406496
data: {
407-
text: 'hello world',
408-
author: null,
497+
todo: null,
409498
},
410499
errors: [
411500
{
412-
locations: [
413-
{
414-
column: 13,
415-
line: 7,
416-
},
417-
],
418501
message: 'This operation was aborted',
419-
path: ['todo', 'author'],
502+
path: ['todo'],
503+
locations: [{ line: 4, column: 11 }],
420504
},
421505
],
422506
id: '0',
@@ -448,6 +532,10 @@ describe('Execute: Cancellation', () => {
448532
},
449533
});
450534

535+
await resolveOnNextTick();
536+
await resolveOnNextTick();
537+
await resolveOnNextTick();
538+
451539
abortController.abort();
452540

453541
const result = await resultPromise;
@@ -498,4 +586,39 @@ describe('Execute: Cancellation', () => {
498586
],
499587
});
500588
});
589+
590+
it('should stop the execution when aborted during subscription', async () => {
591+
const abortController = new AbortController();
592+
const document = parse(`
593+
subscription {
594+
foo
595+
}
596+
`);
597+
598+
const resultPromise = subscribe({
599+
document,
600+
schema,
601+
abortSignal: abortController.signal,
602+
rootValue: {
603+
foo: async () =>
604+
new Promise(() => {
605+
/* will never resolve */
606+
}),
607+
},
608+
});
609+
610+
abortController.abort();
611+
612+
const result = await resultPromise;
613+
614+
expectJSON(result).toDeepEqual({
615+
errors: [
616+
{
617+
message: 'This operation was aborted',
618+
path: ['foo'],
619+
locations: [{ line: 3, column: 9 }],
620+
},
621+
],
622+
});
623+
});
501624
});

src/execution/__tests__/stream-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,11 @@ describe('Execute: stream directive', () => {
16611661
items: [{ name: 'Luke' }],
16621662
id: '1',
16631663
},
1664+
],
1665+
hasNext: true,
1666+
},
1667+
{
1668+
incremental: [
16641669
{
16651670
data: { scalarField: null },
16661671
id: '0',

0 commit comments

Comments
 (0)