Skip to content

Commit 3455728

Browse files
committed
stop resolvers after execution ends
addresses: #3792
1 parent 7dea80e commit 3455728

File tree

2 files changed

+131
-23
lines changed

2 files changed

+131
-23
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: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,12 @@ export interface ValidatedExecutionArgs {
163163

164164
export interface ExecutionContext {
165165
validatedExecutionArgs: ValidatedExecutionArgs;
166+
completed: boolean;
166167
cancellableStreams: Set<CancellableStreamRecord> | undefined;
167168
}
168169

169170
interface IncrementalContext {
171+
completed: boolean;
170172
deferUsageSet?: DeferUsageSet | undefined;
171173
}
172174

@@ -312,6 +314,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
312314
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
313315
const exeContext: ExecutionContext = {
314316
validatedExecutionArgs,
317+
completed: false,
315318
cancellableStreams: undefined,
316319
};
317320
try {
@@ -362,15 +365,23 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
362365

363366
if (isPromise(graphqlWrappedResult)) {
364367
return graphqlWrappedResult.then(
365-
(resolved) => buildDataResponse(exeContext, resolved),
366-
(error: unknown) => ({
367-
data: null,
368-
errors: [error as GraphQLError],
369-
}),
368+
(resolved) => {
369+
exeContext.completed = true;
370+
return buildDataResponse(exeContext, resolved);
371+
},
372+
(error: unknown) => {
373+
exeContext.completed = true;
374+
return {
375+
data: null,
376+
errors: [error as GraphQLError],
377+
};
378+
},
370379
);
371380
}
381+
exeContext.completed = true;
372382
return buildDataResponse(exeContext, graphqlWrappedResult);
373383
} catch (error) {
384+
exeContext.completed = true;
374385
return { data: null, errors: [error] };
375386
}
376387
}
@@ -1019,6 +1030,14 @@ function completeValue(
10191030
incrementalContext: IncrementalContext | undefined,
10201031
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
10211032
): PromiseOrValue<GraphQLWrappedResult<unknown>> {
1033+
if ((incrementalContext ?? exeContext).completed) {
1034+
return {
1035+
rawResult: null,
1036+
incrementalDataRecords: undefined,
1037+
errors: undefined,
1038+
};
1039+
}
1040+
10221041
// If result is an Error, throw a located error.
10231042
if (result instanceof Error) {
10241043
throw result;
@@ -2279,6 +2298,7 @@ function collectExecutionGroups(
22792298
path,
22802299
groupedFieldSet,
22812300
{
2301+
completed: false,
22822302
deferUsageSet,
22832303
},
22842304
deferMap,
@@ -2338,6 +2358,7 @@ function executeExecutionGroup(
23382358
deferMap,
23392359
);
23402360
} catch (error) {
2361+
incrementalContext.completed = true;
23412362
return {
23422363
pendingExecutionGroup,
23432364
path: pathToArray(path),
@@ -2347,16 +2368,26 @@ function executeExecutionGroup(
23472368

23482369
if (isPromise(result)) {
23492370
return result.then(
2350-
(resolved) =>
2351-
buildCompletedExecutionGroup(pendingExecutionGroup, path, resolved),
2352-
(error: unknown) => ({
2353-
pendingExecutionGroup,
2354-
path: pathToArray(path),
2355-
errors: [error as GraphQLError],
2356-
}),
2371+
(resolved) => {
2372+
incrementalContext.completed = true;
2373+
return buildCompletedExecutionGroup(
2374+
pendingExecutionGroup,
2375+
path,
2376+
resolved,
2377+
);
2378+
},
2379+
(error: unknown) => {
2380+
incrementalContext.completed = true;
2381+
return {
2382+
pendingExecutionGroup,
2383+
path: pathToArray(path),
2384+
errors: [error as GraphQLError],
2385+
};
2386+
},
23572387
);
23582388
}
23592389

2390+
incrementalContext.completed = true;
23602391
return buildCompletedExecutionGroup(pendingExecutionGroup, path, result);
23612392
}
23622393

@@ -2405,7 +2436,7 @@ function buildSyncStreamItemQueue(
24052436
initialPath,
24062437
initialItem,
24072438
exeContext,
2408-
{},
2439+
{ completed: false },
24092440
fieldDetailsList,
24102441
info,
24112442
itemType,
@@ -2436,7 +2467,7 @@ function buildSyncStreamItemQueue(
24362467
itemPath,
24372468
value,
24382469
exeContext,
2439-
{},
2470+
{ completed: false },
24402471
fieldDetailsList,
24412472
info,
24422473
itemType,
@@ -2528,7 +2559,7 @@ async function getNextAsyncStreamItemResult(
25282559
itemPath,
25292560
iteration.value,
25302561
exeContext,
2531-
{},
2562+
{ completed: false },
25322563
fieldDetailsList,
25332564
info,
25342565
itemType,
@@ -2575,10 +2606,16 @@ function completeStreamItem(
25752606
incrementalContext,
25762607
new Map(),
25772608
).then(
2578-
(resolvedItem) => buildStreamItemResult(resolvedItem),
2579-
(error: unknown) => ({
2580-
errors: [error as GraphQLError],
2581-
}),
2609+
(resolvedItem) => {
2610+
incrementalContext.completed = true;
2611+
return buildStreamItemResult(resolvedItem);
2612+
},
2613+
(error: unknown) => {
2614+
incrementalContext.completed = true;
2615+
return {
2616+
errors: [error as GraphQLError],
2617+
};
2618+
},
25822619
);
25832620
}
25842621

@@ -2605,6 +2642,7 @@ function completeStreamItem(
26052642
};
26062643
}
26072644
} catch (error) {
2645+
incrementalContext.completed = true;
26082646
return {
26092647
errors: [error],
26102648
};
@@ -2620,13 +2658,20 @@ function completeStreamItem(
26202658
],
26212659
}))
26222660
.then(
2623-
(resolvedItem) => buildStreamItemResult(resolvedItem),
2624-
(error: unknown) => ({
2625-
errors: [error as GraphQLError],
2626-
}),
2661+
(resolvedItem) => {
2662+
incrementalContext.completed = true;
2663+
return buildStreamItemResult(resolvedItem);
2664+
},
2665+
(error: unknown) => {
2666+
incrementalContext.completed = true;
2667+
return {
2668+
errors: [error as GraphQLError],
2669+
};
2670+
},
26272671
);
26282672
}
26292673

2674+
incrementalContext.completed = true;
26302675
return buildStreamItemResult(result);
26312676
}
26322677

0 commit comments

Comments
 (0)