Skip to content

Commit 26bcf69

Browse files
committed
WIP
1 parent f531737 commit 26bcf69

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { expectJSON } from '../../__testUtils__/expectJSON.js';
5+
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
6+
7+
import { parse } from '../../language/parser.js';
8+
9+
import {
10+
GraphQLObjectType,
11+
GraphQLSchema,
12+
GraphQLString,
13+
} from '../../type/index.js';
14+
15+
import { buildSchema } from '../../utilities/buildASTSchema.js';
16+
17+
import { execute } from '../execute.js';
18+
19+
const schema = buildSchema(/* GraphQL */ `
20+
type Todo {
21+
id: ID!
22+
text: String!
23+
completed: Boolean!
24+
author: User
25+
}
26+
27+
type User {
28+
id: ID!
29+
name: String!
30+
}
31+
32+
type Query {
33+
todo: Todo
34+
}
35+
36+
type Mutation {
37+
foo: String
38+
bar: String
39+
}
40+
`);
41+
42+
describe('Abort Signal', () => {
43+
it('should stop the execution when aborted in resolver', async () => {
44+
const abortController = new AbortController();
45+
const document = parse(/* GraphQL */ `
46+
query {
47+
todo {
48+
id
49+
author {
50+
id
51+
}
52+
}
53+
}
54+
`);
55+
const result = await execute({
56+
document,
57+
schema,
58+
abortSignal: abortController.signal,
59+
rootValue: {
60+
todo() {
61+
abortController.abort('Aborted');
62+
return {
63+
id: '1',
64+
text: 'Hello, World!',
65+
completed: false,
66+
author: () => {
67+
expect.fail('Should not be called');
68+
},
69+
};
70+
},
71+
},
72+
});
73+
74+
expectJSON(result).toDeepEqual({
75+
data: {
76+
todo: null,
77+
},
78+
errors: [
79+
{
80+
locations: [
81+
{
82+
column: 9,
83+
line: 3,
84+
},
85+
],
86+
message: 'Aborted',
87+
path: ['todo'],
88+
},
89+
],
90+
});
91+
});
92+
93+
it('should stop the for serial mutation execution', async () => {
94+
const abortController = new AbortController();
95+
const document = parse(/* GraphQL */ `
96+
mutation {
97+
foo
98+
bar
99+
}
100+
`);
101+
const result = await execute({
102+
document,
103+
schema,
104+
abortSignal: abortController.signal,
105+
rootValue: {
106+
foo() {
107+
abortController.abort('Aborted');
108+
return 'baz';
109+
},
110+
bar() {
111+
expect.fail('Should not be called');
112+
},
113+
},
114+
});
115+
116+
expectJSON(result).toDeepEqual({
117+
data: null,
118+
errors: [
119+
{
120+
message: 'Aborted',
121+
},
122+
],
123+
});
124+
});
125+
126+
it('should stop the execution when aborted pre-execute', async () => {
127+
const abortController = new AbortController();
128+
const document = parse(/* GraphQL */ `
129+
query {
130+
todo {
131+
id
132+
author {
133+
id
134+
}
135+
}
136+
}
137+
`);
138+
abortController.abort('Aborted');
139+
const result = await execute({
140+
document,
141+
schema,
142+
abortSignal: abortController.signal,
143+
rootValue: {
144+
todo() {
145+
abortController.abort('Aborted');
146+
return {
147+
id: '1',
148+
text: 'Hello, World!',
149+
completed: false,
150+
author: () => {
151+
expect.fail('Should not be called');
152+
},
153+
};
154+
},
155+
},
156+
});
157+
158+
expectJSON(result).toDeepEqual({
159+
data: null,
160+
errors: [
161+
{
162+
message: 'Aborted',
163+
},
164+
],
165+
});
166+
});
167+
168+
it('exits early on abort mid-execution', async () => {
169+
const asyncObjectType = new GraphQLObjectType({
170+
name: 'AsyncObject',
171+
fields: {
172+
field: {
173+
type: GraphQLString,
174+
/* c8 ignore next 3 */
175+
resolve() {
176+
expect.fail('Should not be called');
177+
},
178+
},
179+
},
180+
});
181+
182+
const newSchema = new GraphQLSchema({
183+
query: new GraphQLObjectType({
184+
name: 'Query',
185+
fields: {
186+
asyncObject: {
187+
type: asyncObjectType,
188+
async resolve() {
189+
await resolveOnNextTick();
190+
return {};
191+
},
192+
},
193+
},
194+
}),
195+
});
196+
197+
const document = parse(`
198+
{
199+
asyncObject {
200+
field
201+
}
202+
}
203+
`);
204+
205+
const abortController = new AbortController();
206+
207+
const result = execute({
208+
schema: newSchema,
209+
document,
210+
abortSignal: abortController.signal,
211+
});
212+
213+
abortController.abort();
214+
215+
expectJSON(await result).toDeepEqual({
216+
data: { asyncObject: null },
217+
errors: [
218+
{
219+
message: 'AbortError: This operation was aborted',
220+
locations: [{ line: 3, column: 9 }],
221+
path: ['asyncObject'],
222+
},
223+
],
224+
});
225+
});
226+
});

src/execution/execute.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,14 @@ export interface ValidatedExecutionArgs {
157157
) => PromiseOrValue<ExecutionResult>;
158158
enableEarlyExecution: boolean;
159159
hideSuggestions: boolean;
160+
abortSignal: AbortSignal | undefined;
160161
}
161162

162163
export interface ExecutionContext {
163164
validatedExecutionArgs: ValidatedExecutionArgs;
164165
errors: Array<GraphQLError> | undefined;
165166
cancellableStreams: Set<CancellableStreamRecord> | undefined;
167+
abortSignal: AbortSignal | undefined;
166168
}
167169

168170
interface IncrementalContext {
@@ -187,6 +189,7 @@ export interface ExecutionArgs {
187189
>;
188190
enableEarlyExecution?: Maybe<boolean>;
189191
hideSuggestions?: Maybe<boolean>;
192+
abortSignal?: AbortSignal | undefined;
190193
}
191194

192195
export interface StreamUsage {
@@ -309,6 +312,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
309312
validatedExecutionArgs,
310313
errors: undefined,
311314
cancellableStreams: undefined,
315+
abortSignal: validatedExecutionArgs.abortSignal,
312316
};
313317
try {
314318
const {
@@ -318,7 +322,13 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
318322
operation,
319323
variableValues,
320324
hideSuggestions,
325+
abortSignal,
321326
} = validatedExecutionArgs;
327+
328+
if (abortSignal?.aborted) {
329+
throw new GraphQLError(abortSignal.reason);
330+
}
331+
322332
const rootType = schema.getRootType(operation.operation);
323333
if (rootType == null) {
324334
throw new GraphQLError(
@@ -592,6 +602,7 @@ export function validateExecutionArgs(
592602
perEventExecutor: perEventExecutor ?? executeSubscriptionEvent,
593603
enableEarlyExecution: enableEarlyExecution === true,
594604
hideSuggestions,
605+
abortSignal: args.abortSignal,
595606
};
596607
}
597608

@@ -656,6 +667,9 @@ function executeFieldsSerially(
656667
groupedFieldSet,
657668
(graphqlWrappedResult, [responseName, fieldDetailsList]) => {
658669
const fieldPath = addPath(path, responseName, parentType.name);
670+
if (exeContext.abortSignal?.aborted) {
671+
throw new GraphQLError(exeContext.abortSignal.reason);
672+
}
659673
const result = executeField(
660674
exeContext,
661675
parentType,
@@ -706,6 +720,12 @@ function executeFields(
706720
try {
707721
for (const [responseName, fieldDetailsList] of groupedFieldSet) {
708722
const fieldPath = addPath(path, responseName, parentType.name);
723+
724+
if (exeContext.abortSignal?.aborted) {
725+
// We might want to leverage a GraphQL error here
726+
throw new GraphQLError(exeContext.abortSignal.reason);
727+
}
728+
709729
const result = executeField(
710730
exeContext,
711731
parentType,
@@ -1069,6 +1089,11 @@ async function completePromisedValue(
10691089
if (isPromise(completed)) {
10701090
completed = await completed;
10711091
}
1092+
1093+
if (exeContext.abortSignal?.aborted) {
1094+
throw new GraphQLError(exeContext.abortSignal.reason);
1095+
}
1096+
10721097
return completed;
10731098
} catch (rawError) {
10741099
handleFieldError(

0 commit comments

Comments
 (0)