Skip to content

Commit 670c26c

Browse files
authored
stop resolvers after execution ends (#4263)
depends on #4267 addresses: #3792
1 parent 308647b commit 670c26c

File tree

2 files changed

+118
-22
lines changed

2 files changed

+118
-22
lines changed

src/execution/__tests__/nonnull-test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
56

67
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';
78

@@ -526,6 +527,68 @@ describe('Execute: handles non-nullable types', () => {
526527
});
527528
});
528529

530+
describe('cancellation with null bubbling', () => {
531+
function nestedPromise(n: number): string {
532+
return n > 0 ? `promiseNest { ${nestedPromise(n - 1)} }` : 'promise';
533+
}
534+
535+
it('returns an single error without cancellation', async () => {
536+
const query = `
537+
{
538+
promiseNonNull,
539+
${nestedPromise(4)}
540+
}
541+
`;
542+
543+
const result = await executeQuery(query, throwingData);
544+
expectJSON(result).toDeepEqual({
545+
data: null,
546+
errors: [
547+
// does not include syncNullError because result returns prior to it being added
548+
{
549+
message: 'promiseNonNull',
550+
path: ['promiseNonNull'],
551+
locations: [{ line: 3, column: 11 }],
552+
},
553+
],
554+
});
555+
});
556+
557+
it('stops running despite error', async () => {
558+
const query = `
559+
{
560+
promiseNonNull,
561+
${nestedPromise(10)}
562+
}
563+
`;
564+
565+
let counter = 0;
566+
const rootValue = {
567+
...throwingData,
568+
promiseNest() {
569+
return new Promise((resolve) => {
570+
counter++;
571+
resolve(rootValue);
572+
});
573+
},
574+
};
575+
const result = await executeQuery(query, rootValue);
576+
expectJSON(result).toDeepEqual({
577+
data: null,
578+
errors: [
579+
{
580+
message: 'promiseNonNull',
581+
path: ['promiseNonNull'],
582+
locations: [{ line: 3, column: 11 }],
583+
},
584+
],
585+
});
586+
const counterAtExecutionEnd = counter;
587+
await resolveOnNextTick();
588+
expect(counter).to.equal(counterAtExecutionEnd);
589+
});
590+
});
591+
529592
describe('Handles non-null argument', () => {
530593
const schemaWithNonNullArg = new GraphQLSchema({
531594
query: new GraphQLObjectType({

src/execution/execute.ts

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,13 @@ export interface ExecutionContext {
165165
validatedExecutionArgs: ValidatedExecutionArgs;
166166
errors: Array<GraphQLError> | undefined;
167167
promiseCanceller: PromiseCanceller | undefined;
168+
completed: boolean;
168169
cancellableStreams: Set<CancellableStreamRecord> | undefined;
169170
}
170171

171172
interface IncrementalContext {
172173
errors: Array<GraphQLError> | undefined;
174+
completed: boolean;
173175
deferUsageSet?: DeferUsageSet | undefined;
174176
}
175177

@@ -319,6 +321,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
319321
promiseCanceller: abortSignal
320322
? new PromiseCanceller(abortSignal)
321323
: undefined,
324+
completed: false,
322325
cancellableStreams: undefined,
323326
};
324327
try {
@@ -359,8 +362,12 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
359362

360363
if (isPromise(graphqlWrappedResult)) {
361364
return graphqlWrappedResult.then(
362-
(resolved) => buildDataResponse(exeContext, resolved),
365+
(resolved) => {
366+
exeContext.completed = true;
367+
return buildDataResponse(exeContext, resolved);
368+
},
363369
(error: unknown) => {
370+
exeContext.completed = true;
364371
exeContext.promiseCanceller?.disconnect();
365372
return {
366373
data: null,
@@ -369,8 +376,10 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
369376
},
370377
);
371378
}
379+
exeContext.completed = true;
372380
return buildDataResponse(exeContext, graphqlWrappedResult);
373381
} catch (error) {
382+
exeContext.completed = true;
374383
// TODO: add test case for synchronous null bubbling to root with cancellation
375384
/* c8 ignore next */
376385
exeContext.promiseCanceller?.disconnect();
@@ -1760,6 +1769,10 @@ function completeObjectValue(
17601769
incrementalContext: IncrementalContext | undefined,
17611770
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
17621771
): PromiseOrValue<GraphQLWrappedResult<ObjMap<unknown>>> {
1772+
if ((incrementalContext ?? exeContext).completed) {
1773+
throw new Error('Completed, aborting.');
1774+
}
1775+
17631776
// If there is an isTypeOf predicate function, call it with the
17641777
// current result. If isTypeOf returns false, then raise an error rather
17651778
// than continuing execution.
@@ -2324,6 +2337,7 @@ function collectExecutionGroups(
23242337
groupedFieldSet,
23252338
{
23262339
errors: undefined,
2340+
completed: false,
23272341
deferUsageSet,
23282342
},
23292343
deferMap,
@@ -2383,6 +2397,7 @@ function executeExecutionGroup(
23832397
deferMap,
23842398
);
23852399
} catch (error) {
2400+
incrementalContext.completed = true;
23862401
return {
23872402
pendingExecutionGroup,
23882403
path: pathToArray(path),
@@ -2392,21 +2407,27 @@ function executeExecutionGroup(
23922407

23932408
if (isPromise(result)) {
23942409
return result.then(
2395-
(resolved) =>
2396-
buildCompletedExecutionGroup(
2410+
(resolved) => {
2411+
incrementalContext.completed = true;
2412+
return buildCompletedExecutionGroup(
23972413
incrementalContext.errors,
23982414
pendingExecutionGroup,
23992415
path,
24002416
resolved,
2401-
),
2402-
(error: unknown) => ({
2403-
pendingExecutionGroup,
2404-
path: pathToArray(path),
2405-
errors: withError(incrementalContext.errors, error as GraphQLError),
2406-
}),
2417+
);
2418+
},
2419+
(error: unknown) => {
2420+
incrementalContext.completed = true;
2421+
return {
2422+
pendingExecutionGroup,
2423+
path: pathToArray(path),
2424+
errors: withError(incrementalContext.errors, error as GraphQLError),
2425+
};
2426+
},
24072427
);
24082428
}
24092429

2430+
incrementalContext.completed = true;
24102431
return buildCompletedExecutionGroup(
24112432
incrementalContext.errors,
24122433
pendingExecutionGroup,
@@ -2461,7 +2482,7 @@ function buildSyncStreamItemQueue(
24612482
initialPath,
24622483
initialItem,
24632484
exeContext,
2464-
{ errors: undefined },
2485+
{ errors: undefined, completed: false },
24652486
fieldDetailsList,
24662487
info,
24672488
itemType,
@@ -2492,7 +2513,7 @@ function buildSyncStreamItemQueue(
24922513
itemPath,
24932514
value,
24942515
exeContext,
2495-
{ errors: undefined },
2516+
{ errors: undefined, completed: false },
24962517
fieldDetailsList,
24972518
info,
24982519
itemType,
@@ -2584,7 +2605,7 @@ async function getNextAsyncStreamItemResult(
25842605
itemPath,
25852606
iteration.value,
25862607
exeContext,
2587-
{ errors: undefined },
2608+
{ errors: undefined, completed: false },
25882609
fieldDetailsList,
25892610
info,
25902611
itemType,
@@ -2631,11 +2652,16 @@ function completeStreamItem(
26312652
incrementalContext,
26322653
new Map(),
26332654
).then(
2634-
(resolvedItem) =>
2635-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2636-
(error: unknown) => ({
2637-
errors: withError(incrementalContext.errors, error as GraphQLError),
2638-
}),
2655+
(resolvedItem) => {
2656+
incrementalContext.completed = true;
2657+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2658+
},
2659+
(error: unknown) => {
2660+
incrementalContext.completed = true;
2661+
return {
2662+
errors: withError(incrementalContext.errors, error as GraphQLError),
2663+
};
2664+
},
26392665
);
26402666
}
26412667

@@ -2664,6 +2690,7 @@ function completeStreamItem(
26642690
result = { rawResult: null, incrementalDataRecords: undefined };
26652691
}
26662692
} catch (error) {
2693+
incrementalContext.completed = true;
26672694
return {
26682695
errors: withError(incrementalContext.errors, error),
26692696
};
@@ -2683,14 +2710,20 @@ function completeStreamItem(
26832710
return { rawResult: null, incrementalDataRecords: undefined };
26842711
})
26852712
.then(
2686-
(resolvedItem) =>
2687-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2688-
(error: unknown) => ({
2689-
errors: withError(incrementalContext.errors, error as GraphQLError),
2690-
}),
2713+
(resolvedItem) => {
2714+
incrementalContext.completed = true;
2715+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2716+
},
2717+
(error: unknown) => {
2718+
incrementalContext.completed = true;
2719+
return {
2720+
errors: withError(incrementalContext.errors, error as GraphQLError),
2721+
};
2722+
},
26912723
);
26922724
}
26932725

2726+
incrementalContext.completed = true;
26942727
return buildStreamItemResult(incrementalContext.errors, result);
26952728
}
26962729

0 commit comments

Comments
 (0)