Skip to content

Commit ae108b2

Browse files
Introduce possibility of passing in an AbortSignal
Co-Authored-By: yaacovCR <[email protected]>
1 parent f531737 commit ae108b2

File tree

6 files changed

+389
-7
lines changed

6 files changed

+389
-7
lines changed

integrationTests/ts/tsconfig.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{
22
"compilerOptions": {
33
"module": "commonjs",
4-
"lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"],
4+
"lib": [
5+
"es2019",
6+
"es2020.promise",
7+
"es2020.bigint",
8+
"es2020.string",
9+
"DOM"
10+
],
511
"noEmit": true,
612
"types": [],
713
"strict": true,

package-lock.json

+4-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@svgr/webpack": "8.1.0",
6363
"@types/chai": "4.3.19",
6464
"@types/mocha": "10.0.7",
65-
"@types/node": "22.5.4",
65+
"@types/node": "22.7.7",
6666
"@typescript-eslint/eslint-plugin": "8.4.0",
6767
"@typescript-eslint/parser": "8.4.0",
6868
"c8": "10.1.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
6+
7+
import type { DocumentNode } from '../../language/ast.js';
8+
import { parse } from '../../language/parser.js';
9+
10+
import { buildSchema } from '../../utilities/buildASTSchema.js';
11+
12+
import { execute, experimentalExecuteIncrementally } from '../execute.js';
13+
import type {
14+
InitialIncrementalExecutionResult,
15+
SubsequentIncrementalExecutionResult,
16+
} from '../types.js';
17+
18+
async function complete(
19+
document: DocumentNode,
20+
rootValue: unknown,
21+
abortSignal: AbortSignal,
22+
) {
23+
const result = await experimentalExecuteIncrementally({
24+
schema,
25+
document,
26+
rootValue,
27+
abortSignal,
28+
});
29+
30+
if ('initialResult' in result) {
31+
const results: Array<
32+
InitialIncrementalExecutionResult | SubsequentIncrementalExecutionResult
33+
> = [result.initialResult];
34+
for await (const patch of result.subsequentResults) {
35+
results.push(patch);
36+
}
37+
return results;
38+
}
39+
}
40+
41+
const schema = buildSchema(`
42+
type Todo {
43+
id: ID
44+
text: String
45+
author: User
46+
}
47+
48+
type User {
49+
id: ID
50+
name: String
51+
}
52+
53+
type Query {
54+
todo: Todo
55+
}
56+
57+
type Mutation {
58+
foo: String
59+
bar: String
60+
}
61+
`);
62+
63+
describe('Execute: Cancellation', () => {
64+
it('should stop the execution when aborted', async () => {
65+
const abortController = new AbortController();
66+
const document = parse(`
67+
query {
68+
todo {
69+
id
70+
author {
71+
id
72+
}
73+
}
74+
}
75+
`);
76+
77+
const resultPromise = execute({
78+
document,
79+
schema,
80+
abortSignal: abortController.signal,
81+
rootValue: {
82+
todo: async () =>
83+
Promise.resolve({
84+
id: '1',
85+
text: 'Hello, World!',
86+
/* c8 ignore next */
87+
author: () => expect.fail('Should not be called'),
88+
}),
89+
},
90+
});
91+
92+
abortController.abort('Aborted');
93+
94+
const result = await resultPromise;
95+
96+
expectJSON(result).toDeepEqual({
97+
data: {
98+
todo: null,
99+
},
100+
errors: [
101+
{
102+
message: 'Aborted',
103+
path: ['todo'],
104+
locations: [{ line: 3, column: 9 }],
105+
},
106+
],
107+
});
108+
});
109+
110+
it('should stop the execution when aborted during nested object field completion', async () => {
111+
const abortController = new AbortController();
112+
const document = parse(`
113+
query {
114+
todo {
115+
id
116+
author {
117+
id
118+
}
119+
}
120+
}
121+
`);
122+
123+
const resultPromise = execute({
124+
document,
125+
schema,
126+
abortSignal: abortController.signal,
127+
rootValue: {
128+
todo: {
129+
id: '1',
130+
text: 'Hello, World!',
131+
/* c8 ignore next 3 */
132+
author: async () =>
133+
Promise.resolve(() => expect.fail('Should not be called')),
134+
},
135+
},
136+
});
137+
138+
abortController.abort('Aborted');
139+
140+
const result = await resultPromise;
141+
142+
expectJSON(result).toDeepEqual({
143+
data: {
144+
todo: {
145+
id: '1',
146+
author: null,
147+
},
148+
},
149+
errors: [
150+
{
151+
message: 'Aborted',
152+
path: ['todo', 'author'],
153+
locations: [{ line: 5, column: 11 }],
154+
},
155+
],
156+
});
157+
});
158+
159+
it('should stop deferred execution when aborted', async () => {
160+
const abortController = new AbortController();
161+
const document = parse(`
162+
query {
163+
todo {
164+
id
165+
... on Todo @defer {
166+
text
167+
author {
168+
... on Author @defer {
169+
id
170+
}
171+
}
172+
}
173+
}
174+
}
175+
`);
176+
177+
const resultPromise = execute({
178+
document,
179+
schema,
180+
rootValue: {
181+
todo: async () =>
182+
Promise.resolve({
183+
id: '1',
184+
text: 'hello world',
185+
/* c8 ignore next */
186+
author: () => expect.fail('Should not be called'),
187+
}),
188+
},
189+
abortSignal: abortController.signal,
190+
});
191+
192+
abortController.abort('Aborted');
193+
194+
const result = await resultPromise;
195+
196+
expectJSON(result).toDeepEqual({
197+
data: {
198+
todo: null,
199+
},
200+
errors: [
201+
{
202+
message: 'Aborted',
203+
path: ['todo'],
204+
locations: [{ line: 3, column: 9 }],
205+
},
206+
],
207+
});
208+
});
209+
210+
it('should stop deferred execution when aborted mid-execution', async () => {
211+
const abortController = new AbortController();
212+
const document = parse(`
213+
query {
214+
todo {
215+
id
216+
... on Todo @defer {
217+
text
218+
author {
219+
... on Author @defer {
220+
id
221+
}
222+
}
223+
}
224+
}
225+
}
226+
`);
227+
228+
const resultPromise = complete(
229+
document,
230+
{
231+
todo: async () =>
232+
Promise.resolve({
233+
id: '1',
234+
text: 'hello world',
235+
/* c8 ignore next 2 */
236+
author: async () =>
237+
Promise.resolve(() => expect.fail('Should not be called')),
238+
}),
239+
},
240+
abortController.signal,
241+
);
242+
243+
await resolveOnNextTick();
244+
await resolveOnNextTick();
245+
await resolveOnNextTick();
246+
247+
abortController.abort('Aborted');
248+
249+
const result = await resultPromise;
250+
251+
expectJSON(result).toDeepEqual([
252+
{
253+
data: {
254+
todo: {
255+
id: '1',
256+
},
257+
},
258+
pending: [{ id: '0', path: ['todo'] }],
259+
hasNext: true,
260+
},
261+
{
262+
completed: [
263+
{
264+
errors: [
265+
{
266+
message: 'Aborted',
267+
},
268+
],
269+
id: '0',
270+
},
271+
],
272+
hasNext: false,
273+
},
274+
]);
275+
});
276+
277+
it('should stop the execution when aborted mid-mutation', async () => {
278+
const abortController = new AbortController();
279+
const document = parse(`
280+
mutation {
281+
foo
282+
bar
283+
}
284+
`);
285+
286+
const resultPromise = execute({
287+
document,
288+
schema,
289+
abortSignal: abortController.signal,
290+
rootValue: {
291+
foo: async () => Promise.resolve('baz'),
292+
/* c8 ignore next */
293+
bar: () => expect.fail('Should not be called'),
294+
},
295+
});
296+
297+
abortController.abort('Aborted');
298+
299+
const result = await resultPromise;
300+
301+
expectJSON(result).toDeepEqual({
302+
data: {
303+
foo: 'baz',
304+
bar: null,
305+
},
306+
errors: [
307+
{
308+
message: 'Aborted',
309+
path: ['bar'],
310+
locations: [{ line: 4, column: 9 }],
311+
},
312+
],
313+
});
314+
});
315+
316+
it('should stop the execution when aborted pre-execute', async () => {
317+
const abortController = new AbortController();
318+
const document = parse(`
319+
query {
320+
todo {
321+
id
322+
author {
323+
id
324+
}
325+
}
326+
}
327+
`);
328+
abortController.abort('Aborted');
329+
const result = await execute({
330+
document,
331+
schema,
332+
abortSignal: abortController.signal,
333+
rootValue: {
334+
/* c8 ignore next */
335+
todo: () => expect.fail('Should not be called'),
336+
},
337+
});
338+
339+
expectJSON(result).toDeepEqual({
340+
errors: [
341+
{
342+
message: 'Aborted',
343+
},
344+
],
345+
});
346+
});
347+
});

0 commit comments

Comments
 (0)