Skip to content

Commit b42febb

Browse files
committed
abort without a pending next
1 parent 21b4165 commit b42febb

File tree

3 files changed

+72
-76
lines changed

3 files changed

+72
-76
lines changed

src/execution/PromiseCanceller.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,14 @@ export class PromiseCanceller {
2828
this.abortSignal.removeEventListener('abort', this.abort);
2929
}
3030

31-
cancellablePromise<T>(
32-
originalPromise: Promise<T>,
33-
onCancel?: (() => unknown) | undefined,
34-
): Promise<T> {
31+
cancellablePromise<T>(originalPromise: Promise<T>): Promise<T> {
3532
if (this.abortSignal.aborted) {
36-
onCancel?.();
3733
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
3834
return Promise.reject(this.abortSignal.reason);
3935
}
4036

4137
const { promise, resolve, reject } = promiseWithResolvers<T>();
4238
const abort = () => {
43-
onCancel?.();
4439
reject(this.abortSignal.reason);
4540
};
4641
this._aborts.add(abort);
@@ -62,20 +57,45 @@ export class PromiseCanceller {
6257
const iterator = iterable[Symbol.asyncIterator]();
6358

6459
if (iterator.return) {
60+
const _next = iterator.next.bind(iterator);
6561
const _return = iterator.return.bind(iterator);
66-
const _returnIgnoringErrors = async (): Promise<IteratorResult<T>> => {
62+
63+
const abort = () => {
6764
_return().catch(() => {
6865
/* c8 ignore next */
6966
// ignore
7067
});
71-
return Promise.resolve({ value: undefined, done: true });
7268
};
7369

70+
if (this.abortSignal.aborted) {
71+
abort();
72+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
73+
const onMethod = () => Promise.reject(this.abortSignal.reason);
74+
return {
75+
[Symbol.asyncIterator]: () => ({
76+
next: onMethod,
77+
return: onMethod,
78+
}),
79+
};
80+
}
81+
82+
this._aborts.add(abort);
83+
const on = (method: () => Promise<IteratorResult<T>>) => async () => {
84+
try {
85+
const iteration = await this.cancellablePromise(method());
86+
if (iteration.done) {
87+
this._aborts.delete(abort);
88+
}
89+
return iteration;
90+
} catch (error) {
91+
this._aborts.delete(abort);
92+
throw error;
93+
}
94+
};
7495
return {
7596
[Symbol.asyncIterator]: () => ({
76-
next: () =>
77-
this.cancellablePromise(iterator.next(), _returnIgnoringErrors),
78-
return: () => this.cancellablePromise(_return()),
97+
next: on(_next),
98+
return: on(_return),
7999
}),
80100
};
81101
}

src/execution/__tests__/PromiseCanceller-test.ts

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -70,62 +70,6 @@ describe('PromiseCanceller', () => {
7070

7171
await expectPromise(withCancellation).toRejectWith('Cancelled!');
7272
});
73-
74-
it('works to trigger onCancel when cancelling a hanging promise', async () => {
75-
const abortController = new AbortController();
76-
const abortSignal = abortController.signal;
77-
78-
const promiseCanceller = new PromiseCanceller(abortSignal);
79-
80-
const promise = new Promise(() => {
81-
/* never resolves */
82-
});
83-
84-
let onCancelCalled = false;
85-
const onCancel = () => {
86-
onCancelCalled = true;
87-
};
88-
89-
const withCancellation = promiseCanceller.cancellablePromise(
90-
promise,
91-
onCancel,
92-
);
93-
94-
expect(onCancelCalled).to.equal(false);
95-
96-
abortController.abort(new Error('Cancelled!'));
97-
98-
expect(onCancelCalled).to.equal(true);
99-
100-
await expectPromise(withCancellation).toRejectWith('Cancelled!');
101-
});
102-
103-
it('works to trigger onCancel when cancelling a hanging promise created after abort signal triggered', async () => {
104-
const abortController = new AbortController();
105-
const abortSignal = abortController.signal;
106-
107-
abortController.abort(new Error('Cancelled!'));
108-
109-
const promiseCanceller = new PromiseCanceller(abortSignal);
110-
111-
const promise = new Promise(() => {
112-
/* never resolves */
113-
});
114-
115-
let onCancelCalled = false;
116-
const onCancel = () => {
117-
onCancelCalled = true;
118-
};
119-
120-
const withCancellation = promiseCanceller.cancellablePromise(
121-
promise,
122-
onCancel,
123-
);
124-
125-
expect(onCancelCalled).to.equal(true);
126-
127-
await expectPromise(withCancellation).toRejectWith('Cancelled!');
128-
});
12973
});
13074

13175
describe('cancellableAsyncIterable', () => {
@@ -197,13 +141,11 @@ describe('PromiseCanceller', () => {
197141

198142
abortController.abort(new Error('Cancelled!'));
199143

200-
expect(returned).to.equal(false);
144+
expect(returned).to.equal(true);
201145

202146
const nextPromise =
203147
cancellableAsyncIterable[Symbol.asyncIterator]().next();
204148

205-
expect(returned).to.equal(true);
206-
207149
await expectPromise(nextPromise).toRejectWith('Cancelled!');
208150
});
209151

@@ -218,7 +160,8 @@ describe('PromiseCanceller', () => {
218160
let returned = false;
219161
const asyncIterable = {
220162
[Symbol.asyncIterator]: () => ({
221-
next: () => Promise.resolve({ value: 1, done: false }),
163+
/* c8 ignore next */
164+
next: () => Promise.resolve({ value: undefined, done: true }),
222165
return: () => {
223166
returned = true;
224167
return Promise.resolve({ value: undefined, done: true });
@@ -229,13 +172,11 @@ describe('PromiseCanceller', () => {
229172
const cancellableAsyncIterable =
230173
promiseCanceller.cancellableIterable(asyncIterable);
231174

232-
expect(returned).to.equal(false);
175+
expect(returned).to.equal(true);
233176

234177
const nextPromise =
235178
cancellableAsyncIterable[Symbol.asyncIterator]().next();
236179

237-
expect(returned).to.equal(true);
238-
239180
await expectPromise(nextPromise).toRejectWith('Cancelled!');
240181
});
241182
});

src/execution/__tests__/abort-signal-test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ describe('Execute: Cancellation', () => {
795795
});
796796
});
797797

798-
it('should stop the execution when aborted during subscription', async () => {
798+
it('should stop the execution when aborted prior to return of the subscription resolver', async () => {
799799
const abortController = new AbortController();
800800
const document = parse(`
801801
subscription {
@@ -830,7 +830,7 @@ describe('Execute: Cancellation', () => {
830830
});
831831
});
832832

833-
it('should stop the execution when aborted during subscription', async () => {
833+
it('should successfully wrap the subscription', async () => {
834834
const abortController = new AbortController();
835835
const document = parse(`
836836
subscription {
@@ -845,6 +845,41 @@ describe('Execute: Cancellation', () => {
845845
rootValue: {
846846
async *foo() {
847847
yield await Promise.resolve({ foo: 'foo' });
848+
},
849+
},
850+
});
851+
852+
assert(isAsyncIterable(subscription));
853+
854+
expectJSON(await subscription.next()).toDeepEqual({
855+
value: {
856+
data: {
857+
foo: 'foo',
858+
},
859+
},
860+
done: false,
861+
});
862+
863+
expectJSON(await subscription.next()).toDeepEqual({
864+
value: undefined,
865+
done: true,
866+
});
867+
});
868+
869+
it('should stop the execution when aborted during subscription', async () => {
870+
const abortController = new AbortController();
871+
const document = parse(`
872+
subscription {
873+
foo
874+
}
875+
`);
876+
877+
const subscription = subscribe({
878+
document,
879+
schema,
880+
abortSignal: abortController.signal,
881+
rootValue: {
882+
async *foo() {
848883
yield await Promise.resolve({ foo: 'foo' }); /* c8 ignore start */
849884
} /* c8 ignore stop */,
850885
},

0 commit comments

Comments
 (0)