Skip to content

Commit 57552ab

Browse files
committed
stop resolvers after execution ends
addresses: #3792
1 parent 99edfea commit 57552ab

File tree

2 files changed

+124
-24
lines changed

2 files changed

+124
-24
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: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,12 @@ export interface ValidatedExecutionArgs {
164164
export interface ExecutionContext {
165165
validatedExecutionArgs: ValidatedExecutionArgs;
166166
aborts: Set<() => void>;
167+
completed: boolean;
167168
cancellableStreams: Set<CancellableStreamRecord> | undefined;
168169
}
169170

170171
interface IncrementalContext {
172+
completed: boolean;
171173
deferUsageSet?: DeferUsageSet | undefined;
172174
}
173175

@@ -314,6 +316,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
314316
const exeContext: ExecutionContext = {
315317
validatedExecutionArgs,
316318
aborts: new Set(),
319+
completed: false,
317320
cancellableStreams: undefined,
318321
};
319322

@@ -375,15 +378,23 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
375378

376379
if (isPromise(graphqlWrappedResult)) {
377380
return graphqlWrappedResult.then(
378-
(resolved) => buildDataResponse(exeContext, resolved),
379-
(error: unknown) => ({
380-
data: null,
381-
errors: [error as GraphQLError],
382-
}),
381+
(resolved) => {
382+
exeContext.completed = true;
383+
return buildDataResponse(exeContext, resolved);
384+
},
385+
(error: unknown) => {
386+
exeContext.completed = true;
387+
return {
388+
data: null,
389+
errors: [error as GraphQLError],
390+
};
391+
},
383392
);
384393
}
394+
exeContext.completed = true;
385395
return buildDataResponse(exeContext, graphqlWrappedResult);
386396
} catch (error) {
397+
exeContext.completed = true;
387398
return { data: null, errors: [error] };
388399
}
389400
}
@@ -831,7 +842,7 @@ function executeField(
831842
validatedExecutionArgs;
832843
const fieldName = fieldDetailsList[0].node.name.value;
833844
const fieldDef = schema.getField(parentType, fieldName);
834-
if (!fieldDef) {
845+
if (!fieldDef || (incrementalContext ?? exeContext).completed) {
835846
return;
836847
}
837848

@@ -2277,6 +2288,7 @@ function collectExecutionGroups(
22772288
path,
22782289
groupedFieldSet,
22792290
{
2291+
completed: false,
22802292
deferUsageSet,
22812293
},
22822294
deferMap,
@@ -2336,6 +2348,7 @@ function executeExecutionGroup(
23362348
deferMap,
23372349
);
23382350
} catch (error) {
2351+
incrementalContext.completed = true;
23392352
return {
23402353
pendingExecutionGroup,
23412354
path: pathToArray(path),
@@ -2345,16 +2358,26 @@ function executeExecutionGroup(
23452358

23462359
if (isPromise(result)) {
23472360
return result.then(
2348-
(resolved) =>
2349-
buildCompletedExecutionGroup(pendingExecutionGroup, path, resolved),
2350-
(error: unknown) => ({
2351-
pendingExecutionGroup,
2352-
path: pathToArray(path),
2353-
errors: [error as GraphQLError],
2354-
}),
2361+
(resolved) => {
2362+
incrementalContext.completed = true;
2363+
return buildCompletedExecutionGroup(
2364+
pendingExecutionGroup,
2365+
path,
2366+
resolved,
2367+
);
2368+
},
2369+
(error: unknown) => {
2370+
incrementalContext.completed = true;
2371+
return {
2372+
pendingExecutionGroup,
2373+
path: pathToArray(path),
2374+
errors: [error as GraphQLError],
2375+
};
2376+
},
23552377
);
23562378
}
23572379

2380+
incrementalContext.completed = true;
23582381
return buildCompletedExecutionGroup(pendingExecutionGroup, path, result);
23592382
}
23602383

@@ -2403,7 +2426,7 @@ function buildSyncStreamItemQueue(
24032426
initialPath,
24042427
initialItem,
24052428
exeContext,
2406-
{},
2429+
{ completed: false },
24072430
fieldDetailsList,
24082431
info,
24092432
itemType,
@@ -2434,7 +2457,7 @@ function buildSyncStreamItemQueue(
24342457
itemPath,
24352458
value,
24362459
exeContext,
2437-
{},
2460+
{ completed: false },
24382461
fieldDetailsList,
24392462
info,
24402463
itemType,
@@ -2526,7 +2549,7 @@ async function getNextAsyncStreamItemResult(
25262549
itemPath,
25272550
iteration.value,
25282551
exeContext,
2529-
{},
2552+
{ completed: false },
25302553
fieldDetailsList,
25312554
info,
25322555
itemType,
@@ -2573,10 +2596,16 @@ function completeStreamItem(
25732596
incrementalContext,
25742597
new Map(),
25752598
).then(
2576-
(resolvedItem) => buildStreamItemResult(resolvedItem),
2577-
(error: unknown) => ({
2578-
errors: [error as GraphQLError],
2579-
}),
2599+
(resolvedItem) => {
2600+
incrementalContext.completed = true;
2601+
return buildStreamItemResult(resolvedItem);
2602+
},
2603+
(error: unknown) => {
2604+
incrementalContext.completed = true;
2605+
return {
2606+
errors: [error as GraphQLError],
2607+
};
2608+
},
25802609
);
25812610
}
25822611

@@ -2603,6 +2632,7 @@ function completeStreamItem(
26032632
};
26042633
}
26052634
} catch (error) {
2635+
incrementalContext.completed = true;
26062636
return {
26072637
errors: [error],
26082638
};
@@ -2618,13 +2648,20 @@ function completeStreamItem(
26182648
],
26192649
}))
26202650
.then(
2621-
(resolvedItem) => buildStreamItemResult(resolvedItem),
2622-
(error: unknown) => ({
2623-
errors: [error as GraphQLError],
2624-
}),
2651+
(resolvedItem) => {
2652+
incrementalContext.completed = true;
2653+
return buildStreamItemResult(resolvedItem);
2654+
},
2655+
(error: unknown) => {
2656+
incrementalContext.completed = true;
2657+
return {
2658+
errors: [error as GraphQLError],
2659+
};
2660+
},
26252661
);
26262662
}
26272663

2664+
incrementalContext.completed = true;
26282665
return buildStreamItemResult(result);
26292666
}
26302667

0 commit comments

Comments
 (0)