Skip to content

Commit cd2a4fa

Browse files
committed
enable early abort from incremental delivery despite pending promises
without `Promise.race()`!
1 parent 8963c8b commit cd2a4fa

2 files changed

Lines changed: 88 additions & 5 deletions

File tree

packages/executor/src/execution/IncrementalPublisher.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class IncrementalPublisher {
115115
let isDone = false;
116116

117117
this._context.signal?.addEventListener('abort', () => {
118-
isDone = true;
118+
this._incrementalGraph.completedIncrementalData().return();
119119
});
120120

121121
const _next = async (): Promise<
@@ -138,10 +138,6 @@ class IncrementalPublisher {
138138
const asyncIterator = completedIncrementalData[Symbol.asyncIterator]();
139139
let iteration = await asyncIterator.next();
140140
while (!iteration.done) {
141-
if (this._context.signal?.aborted) {
142-
throw this._context.signal.reason;
143-
}
144-
145141
for (const completedResult of iteration.value) {
146142
this._handleCompletedIncrementalData(completedResult, context);
147143
}
@@ -176,6 +172,10 @@ class IncrementalPublisher {
176172
iteration = await asyncIterator.next();
177173
}
178174

175+
if (this._context.signal?.aborted) {
176+
throw this._context.signal.reason;
177+
}
178+
179179
await this._returnAsyncIteratorsIgnoringErrors();
180180
return { value: undefined, done: true };
181181
};

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,89 @@ describe('Abort Signal', () => {
469469
await expect(next$).rejects.toThrow('This operation was aborted');
470470
expect(bResolverGotInvoked).toBe(false);
471471
});
472+
it('stops pending stream execution for never-returning incremental delivery (@defer)', async () => {
473+
const aResolverGotInvokedD = createDeferred();
474+
const requestGotCancelledD = createDeferred();
475+
let bResolverGotInvoked = false;
476+
477+
const schema = makeExecutableSchema({
478+
typeDefs: /* GraphQL */ `
479+
type Query {
480+
root: A!
481+
}
482+
type A {
483+
a: B!
484+
}
485+
type B {
486+
b: String
487+
}
488+
`,
489+
resolvers: {
490+
Query: {
491+
async root() {
492+
return {};
493+
},
494+
},
495+
A: {
496+
async a() {
497+
aResolverGotInvokedD.resolve();
498+
await requestGotCancelledD.promise;
499+
return {};
500+
},
501+
},
502+
B: {
503+
b() {
504+
bResolverGotInvoked = true;
505+
return new Promise(() => {});
506+
},
507+
},
508+
},
509+
});
510+
const controller = new AbortController();
511+
const result = await normalizedExecutor({
512+
schema,
513+
document: parse(/* GraphQL */ `
514+
query {
515+
root {
516+
... @defer {
517+
a {
518+
b
519+
}
520+
}
521+
}
522+
}
523+
`),
524+
signal: controller.signal,
525+
});
526+
527+
if (!isAsyncIterable(result)) {
528+
throw new Error('Result is not an async iterable');
529+
}
530+
531+
const iterator = result[Symbol.asyncIterator]();
532+
const next = await iterator.next();
533+
expect(next.value).toMatchInlineSnapshot(`
534+
{
535+
"data": {
536+
"root": {},
537+
},
538+
"hasNext": true,
539+
"pending": [
540+
{
541+
"id": "0",
542+
"path": [
543+
"root",
544+
],
545+
},
546+
],
547+
}
548+
`);
549+
const next$ = iterator.next();
550+
await aResolverGotInvokedD.promise;
551+
controller.abort();
552+
await expect(next$).rejects.toThrow('This operation was aborted');
553+
expect(bResolverGotInvoked).toBe(false);
554+
});
472555
it('stops promise execution', async () => {
473556
const controller = new AbortController();
474557
const d = createDeferred();

0 commit comments

Comments
 (0)