Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stop resolvers after execution ends #4263

Merged
merged 1 commit into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/execution/__tests__/nonnull-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

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

Expand Down Expand Up @@ -526,6 +527,68 @@ describe('Execute: handles non-nullable types', () => {
});
});

describe('cancellation with null bubbling', () => {
function nestedPromise(n: number): string {
return n > 0 ? `promiseNest { ${nestedPromise(n - 1)} }` : 'promise';
}

it('returns an single error without cancellation', async () => {
const query = `
{
promiseNonNull,
${nestedPromise(4)}
}
`;

const result = await executeQuery(query, throwingData);
expectJSON(result).toDeepEqual({
data: null,
errors: [
// does not include syncNullError because result returns prior to it being added
{
message: 'promiseNonNull',
path: ['promiseNonNull'],
locations: [{ line: 3, column: 11 }],
},
],
});
});

it('stops running despite error', async () => {
const query = `
{
promiseNonNull,
${nestedPromise(10)}
}
`;

let counter = 0;
const rootValue = {
...throwingData,
promiseNest() {
return new Promise((resolve) => {
counter++;
resolve(rootValue);
});
},
};
const result = await executeQuery(query, rootValue);
expectJSON(result).toDeepEqual({
data: null,
errors: [
{
message: 'promiseNonNull',
path: ['promiseNonNull'],
locations: [{ line: 3, column: 11 }],
},
],
});
const counterAtExecutionEnd = counter;
await resolveOnNextTick();
expect(counter).to.equal(counterAtExecutionEnd);
});
});

describe('Handles non-null argument', () => {
const schemaWithNonNullArg = new GraphQLSchema({
query: new GraphQLObjectType({
Expand Down
77 changes: 55 additions & 22 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,13 @@ export interface ExecutionContext {
validatedExecutionArgs: ValidatedExecutionArgs;
errors: Array<GraphQLError> | undefined;
promiseCanceller: PromiseCanceller | undefined;
completed: boolean;
cancellableStreams: Set<CancellableStreamRecord> | undefined;
}

interface IncrementalContext {
errors: Array<GraphQLError> | undefined;
completed: boolean;
deferUsageSet?: DeferUsageSet | undefined;
}

Expand Down Expand Up @@ -319,6 +321,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
promiseCanceller: abortSignal
? new PromiseCanceller(abortSignal)
: undefined,
completed: false,
cancellableStreams: undefined,
};
try {
Expand Down Expand Up @@ -369,8 +372,12 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(

if (isPromise(graphqlWrappedResult)) {
return graphqlWrappedResult.then(
(resolved) => buildDataResponse(exeContext, resolved),
(resolved) => {
exeContext.completed = true;
return buildDataResponse(exeContext, resolved);
},
(error: unknown) => {
exeContext.completed = true;
exeContext.promiseCanceller?.disconnect();
return {
data: null,
Expand All @@ -379,8 +386,10 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
},
);
}
exeContext.completed = true;
return buildDataResponse(exeContext, graphqlWrappedResult);
} catch (error) {
exeContext.completed = true;
// TODO: add test case for synchronous null bubbling to root with cancellation
/* c8 ignore next */
exeContext.promiseCanceller?.disconnect();
Expand Down Expand Up @@ -1761,6 +1770,10 @@ function completeObjectValue(
incrementalContext: IncrementalContext | undefined,
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
): PromiseOrValue<GraphQLWrappedResult<ObjMap<unknown>>> {
if ((incrementalContext ?? exeContext).completed) {
throw new Error('Completed, aborting.');
}

// If there is an isTypeOf predicate function, call it with the
// current result. If isTypeOf returns false, then raise an error rather
// than continuing execution.
Expand Down Expand Up @@ -2280,6 +2293,7 @@ function collectExecutionGroups(
groupedFieldSet,
{
errors: undefined,
completed: false,
deferUsageSet,
},
deferMap,
Expand Down Expand Up @@ -2339,6 +2353,7 @@ function executeExecutionGroup(
deferMap,
);
} catch (error) {
incrementalContext.completed = true;
return {
pendingExecutionGroup,
path: pathToArray(path),
Expand All @@ -2348,21 +2363,27 @@ function executeExecutionGroup(

if (isPromise(result)) {
return result.then(
(resolved) =>
buildCompletedExecutionGroup(
(resolved) => {
incrementalContext.completed = true;
return buildCompletedExecutionGroup(
incrementalContext.errors,
pendingExecutionGroup,
path,
resolved,
),
(error: unknown) => ({
pendingExecutionGroup,
path: pathToArray(path),
errors: withError(incrementalContext.errors, error as GraphQLError),
}),
);
},
(error: unknown) => {
incrementalContext.completed = true;
return {
pendingExecutionGroup,
path: pathToArray(path),
errors: withError(incrementalContext.errors, error as GraphQLError),
};
},
);
}

incrementalContext.completed = true;
return buildCompletedExecutionGroup(
incrementalContext.errors,
pendingExecutionGroup,
Expand Down Expand Up @@ -2417,7 +2438,7 @@ function buildSyncStreamItemQueue(
initialPath,
initialItem,
exeContext,
{ errors: undefined },
{ errors: undefined, completed: false },
fieldDetailsList,
info,
itemType,
Expand Down Expand Up @@ -2448,7 +2469,7 @@ function buildSyncStreamItemQueue(
itemPath,
value,
exeContext,
{ errors: undefined },
{ errors: undefined, completed: false },
fieldDetailsList,
info,
itemType,
Expand Down Expand Up @@ -2540,7 +2561,7 @@ async function getNextAsyncStreamItemResult(
itemPath,
iteration.value,
exeContext,
{ errors: undefined },
{ errors: undefined, completed: false },
fieldDetailsList,
info,
itemType,
Expand Down Expand Up @@ -2587,11 +2608,16 @@ function completeStreamItem(
incrementalContext,
new Map(),
).then(
(resolvedItem) =>
buildStreamItemResult(incrementalContext.errors, resolvedItem),
(error: unknown) => ({
errors: withError(incrementalContext.errors, error as GraphQLError),
}),
(resolvedItem) => {
incrementalContext.completed = true;
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
},
(error: unknown) => {
incrementalContext.completed = true;
return {
errors: withError(incrementalContext.errors, error as GraphQLError),
};
},
);
}

Expand Down Expand Up @@ -2620,6 +2646,7 @@ function completeStreamItem(
result = { rawResult: null, incrementalDataRecords: undefined };
}
} catch (error) {
incrementalContext.completed = true;
return {
errors: withError(incrementalContext.errors, error),
};
Expand All @@ -2639,14 +2666,20 @@ function completeStreamItem(
return { rawResult: null, incrementalDataRecords: undefined };
})
.then(
(resolvedItem) =>
buildStreamItemResult(incrementalContext.errors, resolvedItem),
(error: unknown) => ({
errors: withError(incrementalContext.errors, error as GraphQLError),
}),
(resolvedItem) => {
incrementalContext.completed = true;
return buildStreamItemResult(incrementalContext.errors, resolvedItem);
},
(error: unknown) => {
incrementalContext.completed = true;
return {
errors: withError(incrementalContext.errors, error as GraphQLError),
};
},
);
}

incrementalContext.completed = true;
return buildStreamItemResult(incrementalContext.errors, result);
}

Expand Down
Loading