Skip to content

Commit 9550c71

Browse files
committed
feedback and additional tests
1 parent 9f634a8 commit 9550c71

File tree

4 files changed

+84
-19
lines changed

4 files changed

+84
-19
lines changed

src/execution/IncrementalPublisher.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { pathToArray } from '../jsutils/Path.js';
44

55
import type { GraphQLError } from '../error/GraphQLError.js';
66

7-
import type { Canceller } from './Canceller.js';
87
import { IncrementalGraph } from './IncrementalGraph.js';
8+
import type { PromiseCanceller } from './PromiseCanceller.js';
99
import type {
1010
CancellableStreamRecord,
1111
CompletedExecutionGroup,
@@ -44,7 +44,7 @@ export function buildIncrementalResponse(
4444
}
4545

4646
interface IncrementalPublisherContext {
47-
canceller: Canceller | undefined;
47+
promiseCanceller: PromiseCanceller | undefined;
4848
cancellableStreams: Set<CancellableStreamRecord> | undefined;
4949
}
5050

@@ -127,7 +127,7 @@ class IncrementalPublisher {
127127
IteratorResult<SubsequentIncrementalExecutionResult, void>
128128
> => {
129129
if (isDone) {
130-
this._context.canceller?.unsubscribe();
130+
this._context.promiseCanceller?.disconnect();
131131
await this._returnAsyncIteratorsIgnoringErrors();
132132
return { value: undefined, done: true };
133133
}
@@ -176,7 +176,7 @@ class IncrementalPublisher {
176176

177177
// TODO: add test for this case
178178
/* c8 ignore next */
179-
this._context.canceller?.unsubscribe();
179+
this._context.promiseCanceller?.disconnect();
180180
await this._returnAsyncIteratorsIgnoringErrors();
181181
return { value: undefined, done: true };
182182
};

src/execution/Canceller.ts renamed to src/execution/PromiseCanceller.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';
22

33
/**
4-
* A Canceller object that can be used to cancel multiple promises
4+
* A PromiseCanceller object can be used to cancel multiple promises
55
* using a single AbortSignal.
66
*
77
* @internal
88
*/
9-
export class Canceller {
9+
export class PromiseCanceller {
1010
abortSignal: AbortSignal;
1111
abort: () => void;
1212

@@ -24,11 +24,16 @@ export class Canceller {
2424
abortSignal.addEventListener('abort', this.abort);
2525
}
2626

27-
unsubscribe(): void {
27+
disconnect(): void {
2828
this.abortSignal.removeEventListener('abort', this.abort);
2929
}
3030

3131
withCancellation<T>(originalPromise: Promise<T>): Promise<T> {
32+
if (this.abortSignal.aborted) {
33+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
34+
return Promise.reject(this.abortSignal.reason);
35+
}
36+
3237
const { promise, resolve, reject } = promiseWithResolvers<T>();
3338
const abort = () => reject(this.abortSignal.reason);
3439
this._aborts.add(abort);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { expectPromise } from '../../__testUtils__/expectPromise.js';
4+
5+
import { PromiseCanceller } from '../PromiseCanceller.js';
6+
7+
describe('PromiseCanceller', () => {
8+
it('works to cancel an already resolved promise', async () => {
9+
const abortController = new AbortController();
10+
const abortSignal = abortController.signal;
11+
12+
const promiseCanceller = new PromiseCanceller(abortSignal);
13+
14+
const promise = Promise.resolve(1);
15+
16+
const withCancellation = promiseCanceller.withCancellation(promise);
17+
18+
abortController.abort(new Error('Cancelled!'));
19+
20+
await expectPromise(withCancellation).toRejectWith('Cancelled!');
21+
});
22+
23+
it('works to cancel a hanging promise', async () => {
24+
const abortController = new AbortController();
25+
const abortSignal = abortController.signal;
26+
27+
const promiseCanceller = new PromiseCanceller(abortSignal);
28+
29+
const promise = new Promise(() => {
30+
/* never resolves */
31+
});
32+
33+
const withCancellation = promiseCanceller.withCancellation(promise);
34+
35+
abortController.abort(new Error('Cancelled!'));
36+
37+
await expectPromise(withCancellation).toRejectWith('Cancelled!');
38+
});
39+
40+
it('works to cancel a hanging promise created after abort signal triggered', async () => {
41+
const abortController = new AbortController();
42+
const abortSignal = abortController.signal;
43+
44+
abortController.abort(new Error('Cancelled!'));
45+
46+
const promiseCanceller = new PromiseCanceller(abortSignal);
47+
48+
const promise = new Promise(() => {
49+
/* never resolves */
50+
});
51+
52+
const withCancellation = promiseCanceller.withCancellation(promise);
53+
54+
await expectPromise(withCancellation).toRejectWith('Cancelled!');
55+
});
56+
});

src/execution/execute.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import { assertValidSchema } from '../type/validate.js';
5050

5151
import type { DeferUsageSet, ExecutionPlan } from './buildExecutionPlan.js';
5252
import { buildExecutionPlan } from './buildExecutionPlan.js';
53-
import { Canceller } from './Canceller.js';
5453
import type {
5554
DeferUsage,
5655
FieldDetailsList,
@@ -64,6 +63,7 @@ import {
6463
import { getVariableSignature } from './getVariableSignature.js';
6564
import { buildIncrementalResponse } from './IncrementalPublisher.js';
6665
import { mapAsyncIterable } from './mapAsyncIterable.js';
66+
import { PromiseCanceller } from './PromiseCanceller.js';
6767
import type {
6868
CancellableStreamRecord,
6969
CompletedExecutionGroup,
@@ -164,7 +164,7 @@ export interface ValidatedExecutionArgs {
164164
export interface ExecutionContext {
165165
validatedExecutionArgs: ValidatedExecutionArgs;
166166
errors: Array<GraphQLError> | undefined;
167-
canceller: Canceller | undefined;
167+
promiseCanceller: PromiseCanceller | undefined;
168168
cancellableStreams: Set<CancellableStreamRecord> | undefined;
169169
}
170170

@@ -316,7 +316,9 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
316316
const exeContext: ExecutionContext = {
317317
validatedExecutionArgs,
318318
errors: undefined,
319-
canceller: abortSignal ? new Canceller(abortSignal) : undefined,
319+
promiseCanceller: abortSignal
320+
? new PromiseCanceller(abortSignal)
321+
: undefined,
320322
cancellableStreams: undefined,
321323
};
322324
try {
@@ -369,7 +371,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
369371
return graphqlWrappedResult.then(
370372
(resolved) => buildDataResponse(exeContext, resolved),
371373
(error: unknown) => {
372-
exeContext.canceller?.unsubscribe();
374+
exeContext.promiseCanceller?.disconnect();
373375
return {
374376
data: null,
375377
errors: withError(exeContext.errors, error as GraphQLError),
@@ -381,7 +383,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
381383
} catch (error) {
382384
// TODO: add test case for synchronous null bubbling to root with cancellation
383385
/* c8 ignore next */
384-
exeContext.canceller?.unsubscribe();
386+
exeContext.promiseCanceller?.disconnect();
385387
return { data: null, errors: withError(exeContext.errors, error) };
386388
}
387389
}
@@ -472,7 +474,7 @@ function buildDataResponse(
472474
const { rawResult: data, incrementalDataRecords } = graphqlWrappedResult;
473475
const errors = exeContext.errors;
474476
if (incrementalDataRecords === undefined) {
475-
exeContext.canceller?.unsubscribe();
477+
exeContext.promiseCanceller?.disconnect();
476478
return errors !== undefined ? { errors, data } : { data };
477479
}
478480

@@ -823,7 +825,7 @@ function executeField(
823825
incrementalContext: IncrementalContext | undefined,
824826
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
825827
): PromiseOrValue<GraphQLWrappedResult<unknown>> | undefined {
826-
const { validatedExecutionArgs, canceller } = exeContext;
828+
const { validatedExecutionArgs, promiseCanceller } = exeContext;
827829
const { schema, contextValue, variableValues, hideSuggestions, abortSignal } =
828830
validatedExecutionArgs;
829831
const fieldName = fieldDetailsList[0].node.name.value;
@@ -868,7 +870,7 @@ function executeField(
868870
fieldDetailsList,
869871
info,
870872
path,
871-
canceller?.withCancellation(result) ?? result,
873+
promiseCanceller?.withCancellation(result) ?? result,
872874
incrementalContext,
873875
deferMap,
874876
);
@@ -2203,17 +2205,19 @@ function executeSubscription(
22032205
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);
22042206

22052207
if (isPromise(result)) {
2206-
const canceller = abortSignal ? new Canceller(abortSignal) : undefined;
2207-
const promise = canceller?.withCancellation(result) ?? result;
2208+
const promiseCanceller = abortSignal
2209+
? new PromiseCanceller(abortSignal)
2210+
: undefined;
2211+
const promise = promiseCanceller?.withCancellation(result) ?? result;
22082212
return promise.then(assertEventStream).then(
22092213
(resolved) => {
22102214
// TODO: add test case
22112215
/* c8 ignore next */
2212-
canceller?.unsubscribe();
2216+
promiseCanceller?.disconnect();
22132217
return resolved;
22142218
},
22152219
(error: unknown) => {
2216-
canceller?.unsubscribe();
2220+
promiseCanceller?.disconnect();
22172221
throw locatedError(error, fieldNodes, pathToArray(path));
22182222
},
22192223
);

0 commit comments

Comments
 (0)