Skip to content

Commit 46643cd

Browse files
committed
stop resolvers after execution ends
addresses: #3792
1 parent e4d7e85 commit 46643cd

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 {
@@ -369,8 +372,12 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
369372

370373
if (isPromise(graphqlWrappedResult)) {
371374
return graphqlWrappedResult.then(
372-
(resolved) => buildDataResponse(exeContext, resolved),
375+
(resolved) => {
376+
exeContext.completed = true;
377+
return buildDataResponse(exeContext, resolved);
378+
},
373379
(error: unknown) => {
380+
exeContext.completed = true;
374381
exeContext.promiseCanceller?.disconnect();
375382
return {
376383
data: null,
@@ -379,8 +386,10 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
379386
},
380387
);
381388
}
389+
exeContext.completed = true;
382390
return buildDataResponse(exeContext, graphqlWrappedResult);
383391
} catch (error) {
392+
exeContext.completed = true;
384393
// TODO: add test case for synchronous null bubbling to root with cancellation
385394
/* c8 ignore next */
386395
exeContext.promiseCanceller?.disconnect();
@@ -1761,6 +1770,10 @@ function completeObjectValue(
17611770
incrementalContext: IncrementalContext | undefined,
17621771
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
17631772
): PromiseOrValue<GraphQLWrappedResult<ObjMap<unknown>>> {
1773+
if ((incrementalContext ?? exeContext).completed) {
1774+
throw new Error('Completed, aborting.');
1775+
}
1776+
17641777
// If there is an isTypeOf predicate function, call it with the
17651778
// current result. If isTypeOf returns false, then raise an error rather
17661779
// than continuing execution.
@@ -2280,6 +2293,7 @@ function collectExecutionGroups(
22802293
groupedFieldSet,
22812294
{
22822295
errors: undefined,
2296+
completed: false,
22832297
deferUsageSet,
22842298
},
22852299
deferMap,
@@ -2339,6 +2353,7 @@ function executeExecutionGroup(
23392353
deferMap,
23402354
);
23412355
} catch (error) {
2356+
incrementalContext.completed = true;
23422357
return {
23432358
pendingExecutionGroup,
23442359
path: pathToArray(path),
@@ -2348,21 +2363,27 @@ function executeExecutionGroup(
23482363

23492364
if (isPromise(result)) {
23502365
return result.then(
2351-
(resolved) =>
2352-
buildCompletedExecutionGroup(
2366+
(resolved) => {
2367+
incrementalContext.completed = true;
2368+
return buildCompletedExecutionGroup(
23532369
incrementalContext.errors,
23542370
pendingExecutionGroup,
23552371
path,
23562372
resolved,
2357-
),
2358-
(error: unknown) => ({
2359-
pendingExecutionGroup,
2360-
path: pathToArray(path),
2361-
errors: withError(incrementalContext.errors, error as GraphQLError),
2362-
}),
2373+
);
2374+
},
2375+
(error: unknown) => {
2376+
incrementalContext.completed = true;
2377+
return {
2378+
pendingExecutionGroup,
2379+
path: pathToArray(path),
2380+
errors: withError(incrementalContext.errors, error as GraphQLError),
2381+
};
2382+
},
23632383
);
23642384
}
23652385

2386+
incrementalContext.completed = true;
23662387
return buildCompletedExecutionGroup(
23672388
incrementalContext.errors,
23682389
pendingExecutionGroup,
@@ -2417,7 +2438,7 @@ function buildSyncStreamItemQueue(
24172438
initialPath,
24182439
initialItem,
24192440
exeContext,
2420-
{ errors: undefined },
2441+
{ errors: undefined, completed: false },
24212442
fieldDetailsList,
24222443
info,
24232444
itemType,
@@ -2448,7 +2469,7 @@ function buildSyncStreamItemQueue(
24482469
itemPath,
24492470
value,
24502471
exeContext,
2451-
{ errors: undefined },
2472+
{ errors: undefined, completed: false },
24522473
fieldDetailsList,
24532474
info,
24542475
itemType,
@@ -2540,7 +2561,7 @@ async function getNextAsyncStreamItemResult(
25402561
itemPath,
25412562
iteration.value,
25422563
exeContext,
2543-
{ errors: undefined },
2564+
{ errors: undefined, completed: false },
25442565
fieldDetailsList,
25452566
info,
25462567
itemType,
@@ -2587,11 +2608,16 @@ function completeStreamItem(
25872608
incrementalContext,
25882609
new Map(),
25892610
).then(
2590-
(resolvedItem) =>
2591-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2592-
(error: unknown) => ({
2593-
errors: withError(incrementalContext.errors, error as GraphQLError),
2594-
}),
2611+
(resolvedItem) => {
2612+
incrementalContext.completed = true;
2613+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2614+
},
2615+
(error: unknown) => {
2616+
incrementalContext.completed = true;
2617+
return {
2618+
errors: withError(incrementalContext.errors, error as GraphQLError),
2619+
};
2620+
},
25952621
);
25962622
}
25972623

@@ -2620,6 +2646,7 @@ function completeStreamItem(
26202646
result = { rawResult: null, incrementalDataRecords: undefined };
26212647
}
26222648
} catch (error) {
2649+
incrementalContext.completed = true;
26232650
return {
26242651
errors: withError(incrementalContext.errors, error),
26252652
};
@@ -2639,14 +2666,20 @@ function completeStreamItem(
26392666
return { rawResult: null, incrementalDataRecords: undefined };
26402667
})
26412668
.then(
2642-
(resolvedItem) =>
2643-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2644-
(error: unknown) => ({
2645-
errors: withError(incrementalContext.errors, error as GraphQLError),
2646-
}),
2669+
(resolvedItem) => {
2670+
incrementalContext.completed = true;
2671+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2672+
},
2673+
(error: unknown) => {
2674+
incrementalContext.completed = true;
2675+
return {
2676+
errors: withError(incrementalContext.errors, error as GraphQLError),
2677+
};
2678+
},
26472679
);
26482680
}
26492681

2682+
incrementalContext.completed = true;
26502683
return buildStreamItemResult(incrementalContext.errors, result);
26512684
}
26522685

0 commit comments

Comments
 (0)