Skip to content

Commit 3ab05ef

Browse files
committed
stop resolvers after execution ends
addresses: #3792
1 parent 9f634a8 commit 3ab05ef

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
canceller: Canceller | 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

@@ -317,6 +319,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
317319
validatedExecutionArgs,
318320
errors: undefined,
319321
canceller: abortSignal ? new Canceller(abortSignal) : undefined,
322+
completed: false,
320323
cancellableStreams: undefined,
321324
};
322325
try {
@@ -367,8 +370,12 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
367370

368371
if (isPromise(graphqlWrappedResult)) {
369372
return graphqlWrappedResult.then(
370-
(resolved) => buildDataResponse(exeContext, resolved),
373+
(resolved) => {
374+
exeContext.completed = true;
375+
return buildDataResponse(exeContext, resolved);
376+
},
371377
(error: unknown) => {
378+
exeContext.completed = true;
372379
exeContext.canceller?.unsubscribe();
373380
return {
374381
data: null,
@@ -377,8 +384,10 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
377384
},
378385
);
379386
}
387+
exeContext.completed = true;
380388
return buildDataResponse(exeContext, graphqlWrappedResult);
381389
} catch (error) {
390+
exeContext.completed = true;
382391
// TODO: add test case for synchronous null bubbling to root with cancellation
383392
/* c8 ignore next */
384393
exeContext.canceller?.unsubscribe();
@@ -1757,6 +1766,10 @@ function completeObjectValue(
17571766
incrementalContext: IncrementalContext | undefined,
17581767
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
17591768
): PromiseOrValue<GraphQLWrappedResult<ObjMap<unknown>>> {
1769+
if ((incrementalContext ?? exeContext).completed) {
1770+
throw new Error('Completed, aborting.');
1771+
}
1772+
17601773
// If there is an isTypeOf predicate function, call it with the
17611774
// current result. If isTypeOf returns false, then raise an error rather
17621775
// than continuing execution.
@@ -2274,6 +2287,7 @@ function collectExecutionGroups(
22742287
groupedFieldSet,
22752288
{
22762289
errors: undefined,
2290+
completed: false,
22772291
deferUsageSet,
22782292
},
22792293
deferMap,
@@ -2333,6 +2347,7 @@ function executeExecutionGroup(
23332347
deferMap,
23342348
);
23352349
} catch (error) {
2350+
incrementalContext.completed = true;
23362351
return {
23372352
pendingExecutionGroup,
23382353
path: pathToArray(path),
@@ -2342,21 +2357,27 @@ function executeExecutionGroup(
23422357

23432358
if (isPromise(result)) {
23442359
return result.then(
2345-
(resolved) =>
2346-
buildCompletedExecutionGroup(
2360+
(resolved) => {
2361+
incrementalContext.completed = true;
2362+
return buildCompletedExecutionGroup(
23472363
incrementalContext.errors,
23482364
pendingExecutionGroup,
23492365
path,
23502366
resolved,
2351-
),
2352-
(error: unknown) => ({
2353-
pendingExecutionGroup,
2354-
path: pathToArray(path),
2355-
errors: withError(incrementalContext.errors, error as GraphQLError),
2356-
}),
2367+
);
2368+
},
2369+
(error: unknown) => {
2370+
incrementalContext.completed = true;
2371+
return {
2372+
pendingExecutionGroup,
2373+
path: pathToArray(path),
2374+
errors: withError(incrementalContext.errors, error as GraphQLError),
2375+
};
2376+
},
23572377
);
23582378
}
23592379

2380+
incrementalContext.completed = true;
23602381
return buildCompletedExecutionGroup(
23612382
incrementalContext.errors,
23622383
pendingExecutionGroup,
@@ -2411,7 +2432,7 @@ function buildSyncStreamItemQueue(
24112432
initialPath,
24122433
initialItem,
24132434
exeContext,
2414-
{ errors: undefined },
2435+
{ errors: undefined, completed: false },
24152436
fieldDetailsList,
24162437
info,
24172438
itemType,
@@ -2442,7 +2463,7 @@ function buildSyncStreamItemQueue(
24422463
itemPath,
24432464
value,
24442465
exeContext,
2445-
{ errors: undefined },
2466+
{ errors: undefined, completed: false },
24462467
fieldDetailsList,
24472468
info,
24482469
itemType,
@@ -2534,7 +2555,7 @@ async function getNextAsyncStreamItemResult(
25342555
itemPath,
25352556
iteration.value,
25362557
exeContext,
2537-
{ errors: undefined },
2558+
{ errors: undefined, completed: false },
25382559
fieldDetailsList,
25392560
info,
25402561
itemType,
@@ -2581,11 +2602,16 @@ function completeStreamItem(
25812602
incrementalContext,
25822603
new Map(),
25832604
).then(
2584-
(resolvedItem) =>
2585-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2586-
(error: unknown) => ({
2587-
errors: withError(incrementalContext.errors, error as GraphQLError),
2588-
}),
2605+
(resolvedItem) => {
2606+
incrementalContext.completed = true;
2607+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2608+
},
2609+
(error: unknown) => {
2610+
incrementalContext.completed = true;
2611+
return {
2612+
errors: withError(incrementalContext.errors, error as GraphQLError),
2613+
};
2614+
},
25892615
);
25902616
}
25912617

@@ -2614,6 +2640,7 @@ function completeStreamItem(
26142640
result = { rawResult: null, incrementalDataRecords: undefined };
26152641
}
26162642
} catch (error) {
2643+
incrementalContext.completed = true;
26172644
return {
26182645
errors: withError(incrementalContext.errors, error),
26192646
};
@@ -2633,14 +2660,20 @@ function completeStreamItem(
26332660
return { rawResult: null, incrementalDataRecords: undefined };
26342661
})
26352662
.then(
2636-
(resolvedItem) =>
2637-
buildStreamItemResult(incrementalContext.errors, resolvedItem),
2638-
(error: unknown) => ({
2639-
errors: withError(incrementalContext.errors, error as GraphQLError),
2640-
}),
2663+
(resolvedItem) => {
2664+
incrementalContext.completed = true;
2665+
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
2666+
},
2667+
(error: unknown) => {
2668+
incrementalContext.completed = true;
2669+
return {
2670+
errors: withError(incrementalContext.errors, error as GraphQLError),
2671+
};
2672+
},
26412673
);
26422674
}
26432675

2676+
incrementalContext.completed = true;
26442677
return buildStreamItemResult(incrementalContext.errors, result);
26452678
}
26462679

0 commit comments

Comments
 (0)