Skip to content

Commit a5364eb

Browse files
authored
fix: incremental delivery cancelation unhandled rejection (#6006)
1 parent 9f96dd7 commit a5364eb

File tree

5 files changed

+219
-104
lines changed

5 files changed

+219
-104
lines changed

.changeset/pink-fishes-punch.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+
fix rejecting when canceling async iterable returned from normalized executor

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { parse } from 'graphql';
22
import { makeExecutableSchema } from '@graphql-tools/schema';
3+
import { isAsyncIterable } from '@graphql-tools/utils';
34
import { Repeater } from '@repeaterjs/repeater';
45
import { assertAsyncIterable } from '../../../../loaders/url/tests/test-utils';
56
import { normalizedExecutor } from '../normalizedExecutor';
67

8+
type Deferred<T = void> = {
9+
resolve: (value: T) => void;
10+
reject: (value: unknown) => void;
11+
promise: Promise<T>;
12+
};
13+
14+
function createDeferred<T = void>(): Deferred<T> {
15+
const d = {} as Deferred<T>;
16+
d.promise = new Promise<T>((resolve, reject) => {
17+
d.resolve = resolve;
18+
d.reject = reject;
19+
});
20+
return d;
21+
}
22+
723
describe('Abort Signal', () => {
824
it('should stop the subscription', async () => {
925
expect.assertions(2);
@@ -241,4 +257,66 @@ describe('Abort Signal', () => {
241257
});
242258
expect(isAborted).toEqual(true);
243259
});
260+
it('stops pending stream execution for incremental delivery', async () => {
261+
const controller = new AbortController();
262+
const d = createDeferred();
263+
let isReturnInvoked = false;
264+
265+
const schema = makeExecutableSchema({
266+
typeDefs: /* GraphQL */ `
267+
type Query {
268+
counter: [Int!]!
269+
}
270+
`,
271+
resolvers: {
272+
Query: {
273+
counter: () => ({
274+
[Symbol.asyncIterator]() {
275+
return this;
276+
},
277+
next() {
278+
return d.promise.then(() => ({ done: true }));
279+
},
280+
return() {
281+
isReturnInvoked = true;
282+
d.resolve();
283+
return Promise.resolve({ done: true });
284+
},
285+
}),
286+
},
287+
},
288+
});
289+
290+
const result = await normalizedExecutor({
291+
schema,
292+
document: parse(/* GraphQL */ `
293+
query {
294+
counter @stream
295+
}
296+
`),
297+
signal: controller.signal,
298+
});
299+
300+
if (!isAsyncIterable(result)) {
301+
throw new Error('Result is not an async iterable');
302+
}
303+
304+
const iter = result[Symbol.asyncIterator]();
305+
306+
const next = await iter.next();
307+
expect(next).toEqual({
308+
done: false,
309+
value: {
310+
data: {
311+
counter: [],
312+
},
313+
hasNext: true,
314+
},
315+
});
316+
317+
const next$ = iter.next();
318+
controller.abort();
319+
await expect(next$).rejects.toMatchInlineSnapshot(`DOMException {}`);
320+
expect(isReturnInvoked).toEqual(true);
321+
});
244322
});

0 commit comments

Comments
 (0)