Skip to content

Commit b07be2b

Browse files
authored
test: cancellation of subscription execution and stream execution (#6020)
* test: cancellation of subscription execution and stream execution * fix: cancelation logic * add changelog * fix implementation
1 parent 0672405 commit b07be2b

File tree

5 files changed

+254
-163
lines changed

5 files changed

+254
-163
lines changed

.changeset/eight-clocks-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/executor": patch
3+
---
4+
5+
Correctly raise `AbortError` for defer payloads

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

Lines changed: 223 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -77,123 +77,115 @@ describe('Abort Signal', () => {
7777
expect(stopped).toBe(true);
7878
expect(results).toEqual([0, 1, 2, 3, 4]);
7979
});
80-
it('should stop the serial mutation execution', async () => {
80+
it('pending subscription execution is canceled', async () => {
8181
const controller = new AbortController();
82-
const firstFn = jest.fn(() => true);
83-
const secondFn = jest.fn(() => {
84-
controller.abort();
85-
return true;
86-
});
87-
const thirdFn = jest.fn(() => true);
82+
const rootResolverGotInvokedD = createDeferred();
83+
const requestGotCancelledD = createDeferred();
84+
let aResolverGotInvoked = false;
85+
8886
const schema = makeExecutableSchema({
8987
typeDefs: /* GraphQL */ `
9088
type Query {
9189
_: Boolean
9290
}
93-
type Mutation {
94-
first: Boolean
95-
second: Boolean
96-
third: Boolean
91+
type Subscription {
92+
a: A!
93+
}
94+
95+
type A {
96+
a: String!
9797
}
9898
`,
9999
resolvers: {
100-
Mutation: {
101-
first: firstFn,
102-
second: secondFn,
103-
third: thirdFn,
100+
Subscription: {
101+
a: {
102+
async *subscribe() {
103+
yield 1;
104+
},
105+
async resolve() {
106+
rootResolverGotInvokedD.resolve();
107+
await requestGotCancelledD.promise;
108+
return { a: 'a' };
109+
},
110+
},
111+
},
112+
A: {
113+
a() {
114+
aResolverGotInvoked = true;
115+
return 'a';
116+
},
104117
},
105118
},
106119
});
107120
const result = await normalizedExecutor({
108121
schema,
109122
document: parse(/* GraphQL */ `
110-
mutation {
111-
first
112-
second
113-
third
123+
subscription {
124+
a {
125+
a
126+
}
114127
}
115128
`),
116129
signal: controller.signal,
117130
});
118-
expect(firstFn).toHaveBeenCalledTimes(1);
119-
expect(secondFn).toHaveBeenCalledTimes(1);
120-
expect(thirdFn).toHaveBeenCalledTimes(0);
121-
expect(result).toMatchObject({
122-
data: {
123-
first: true,
124-
second: true,
125-
third: null,
126-
},
127-
errors: [
128-
{
129-
message: 'Execution aborted',
130-
path: ['second'],
131-
locations: [
132-
{
133-
line: 4,
134-
column: 11,
135-
},
136-
],
137-
},
138-
],
139-
});
131+
assertAsyncIterable(result);
132+
const iterator = result![Symbol.asyncIterator]();
133+
const $next = iterator.next();
134+
await rootResolverGotInvokedD.promise;
135+
controller.abort();
136+
await expect($next).rejects.toMatchInlineSnapshot(`DOMException {}`);
137+
expect(aResolverGotInvoked).toEqual(false);
140138
});
141-
it('should stop the parallel query execution', async () => {
142-
let resolve$: (value: any) => void = () => {};
139+
it('should stop the serial mutation execution', async () => {
143140
const controller = new AbortController();
141+
142+
let didInvokeFirstFn = false;
143+
let didInvokeSecondFn = false;
144+
let didInvokeThirdFn = false;
144145
const schema = makeExecutableSchema({
145146
typeDefs: /* GraphQL */ `
146147
type Query {
148+
_: Boolean
149+
}
150+
type Mutation {
147151
first: Boolean
148152
second: Boolean
149153
third: Boolean
150154
}
151155
`,
152156
resolvers: {
153-
Query: {
154-
first: async () => true,
155-
second: async () => {
157+
Mutation: {
158+
first() {
159+
didInvokeFirstFn = true;
160+
return true;
161+
},
162+
second() {
163+
didInvokeSecondFn = true;
156164
controller.abort();
157165
return true;
158166
},
159-
third: () =>
160-
new Promise(resolve => {
161-
resolve$ = resolve;
162-
}),
167+
third() {
168+
didInvokeThirdFn = true;
169+
return true;
170+
},
163171
},
164172
},
165173
});
166-
const result = await normalizedExecutor({
174+
const result$ = normalizedExecutor({
167175
schema,
168176
document: parse(/* GraphQL */ `
169-
query {
177+
mutation {
170178
first
171179
second
172180
third
173181
}
174182
`),
175183
signal: controller.signal,
176184
});
177-
resolve$?.(true);
178-
expect(result).toMatchObject({
179-
data: {
180-
first: true,
181-
second: true,
182-
third: null,
183-
},
184-
errors: [
185-
{
186-
message: 'Execution aborted',
187-
path: ['second'],
188-
locations: [
189-
{
190-
line: 4,
191-
column: 11,
192-
},
193-
],
194-
},
195-
],
196-
});
185+
expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
186+
expect(didInvokeFirstFn).toBe(true);
187+
expect(didInvokeSecondFn).toBe(true);
188+
expect(didInvokeThirdFn).toBe(false);
197189
});
198190
it('should stop stream execution', async () => {
199191
const controller = new AbortController();
@@ -244,7 +236,7 @@ describe('Abort Signal', () => {
244236
await expect(result$).rejects.toMatchInlineSnapshot(`DOMException {}`);
245237
expect(isAborted).toEqual(true);
246238
});
247-
it('stops pending stream execution for incremental delivery', async () => {
239+
it('stops pending stream execution for incremental delivery (@stream)', async () => {
248240
const controller = new AbortController();
249241
const d = createDeferred();
250242
let isReturnInvoked = false;
@@ -306,6 +298,164 @@ describe('Abort Signal', () => {
306298
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
307299
expect(isReturnInvoked).toEqual(true);
308300
});
301+
it('stops pending stream execution for parallel sources incremental delivery (@stream)', async () => {
302+
const controller = new AbortController();
303+
const d1 = createDeferred();
304+
const d2 = createDeferred();
305+
306+
let isReturn1Invoked = false;
307+
let isReturn2Invoked = false;
308+
309+
const schema = makeExecutableSchema({
310+
typeDefs: /* GraphQL */ `
311+
type Query {
312+
counter1: [Int!]!
313+
counter2: [Int!]!
314+
}
315+
`,
316+
resolvers: {
317+
Query: {
318+
counter1: () => ({
319+
[Symbol.asyncIterator]() {
320+
return this;
321+
},
322+
next() {
323+
return d1.promise.then(() => ({ done: true }));
324+
},
325+
return() {
326+
isReturn1Invoked = true;
327+
d1.resolve();
328+
return Promise.resolve({ done: true });
329+
},
330+
}),
331+
counter2: () => ({
332+
[Symbol.asyncIterator]() {
333+
return this;
334+
},
335+
next() {
336+
return d2.promise.then(() => ({ done: true }));
337+
},
338+
return() {
339+
isReturn2Invoked = true;
340+
d2.resolve();
341+
return Promise.resolve({ done: true });
342+
},
343+
}),
344+
},
345+
},
346+
});
347+
348+
const result = await normalizedExecutor({
349+
schema,
350+
document: parse(/* GraphQL */ `
351+
query {
352+
counter1 @stream
353+
counter2 @stream
354+
}
355+
`),
356+
signal: controller.signal,
357+
});
358+
359+
if (!isAsyncIterable(result)) {
360+
throw new Error('Result is not an async iterable');
361+
}
362+
363+
const iter = result[Symbol.asyncIterator]();
364+
365+
const next = await iter.next();
366+
expect(next).toEqual({
367+
done: false,
368+
value: {
369+
data: {
370+
counter1: [],
371+
counter2: [],
372+
},
373+
hasNext: true,
374+
},
375+
});
376+
377+
const next$ = iter.next();
378+
controller.abort();
379+
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
380+
expect(isReturn1Invoked).toEqual(true);
381+
expect(isReturn2Invoked).toEqual(true);
382+
});
383+
it('stops pending stream execution for incremental delivery (@defer)', async () => {
384+
const aResolverGotInvokedD = createDeferred();
385+
const requestGotCancelledD = createDeferred();
386+
let bResolverGotInvoked = false;
387+
388+
const schema = makeExecutableSchema({
389+
typeDefs: /* GraphQL */ `
390+
type Query {
391+
root: A!
392+
}
393+
type A {
394+
a: B!
395+
}
396+
type B {
397+
b: String
398+
}
399+
`,
400+
resolvers: {
401+
Query: {
402+
async root() {
403+
return { a: 'a' };
404+
},
405+
},
406+
A: {
407+
async a() {
408+
aResolverGotInvokedD.resolve();
409+
await requestGotCancelledD.promise;
410+
return { b: 'b' };
411+
},
412+
},
413+
B: {
414+
b: obj => {
415+
bResolverGotInvoked = true;
416+
return obj.b;
417+
},
418+
},
419+
},
420+
});
421+
const controller = new AbortController();
422+
const result = await normalizedExecutor({
423+
schema,
424+
document: parse(/* GraphQL */ `
425+
query {
426+
root {
427+
... @defer {
428+
a {
429+
b
430+
}
431+
}
432+
}
433+
}
434+
`),
435+
signal: controller.signal,
436+
});
437+
438+
if (!isAsyncIterable(result)) {
439+
throw new Error('Result is not an async iterable');
440+
}
441+
442+
const iterator = result[Symbol.asyncIterator]();
443+
const next = await iterator.next();
444+
expect(next.value).toMatchInlineSnapshot(`
445+
{
446+
"data": {
447+
"root": {},
448+
},
449+
"hasNext": true,
450+
}
451+
`);
452+
const next$ = iterator.next();
453+
await aResolverGotInvokedD.promise;
454+
controller.abort();
455+
requestGotCancelledD.resolve();
456+
await expect(next$).rejects.toThrow('This operation was aborted');
457+
expect(bResolverGotInvoked).toBe(false);
458+
});
309459
it('stops promise execution', async () => {
310460
const controller = new AbortController();
311461
const d = createDeferred();

0 commit comments

Comments
 (0)