Skip to content

Commit 7897e85

Browse files
authored
feat(from): support AbortSignal in from(observable) (#333)
1 parent 370ae91 commit 7897e85

File tree

2 files changed

+75
-1
lines changed

2 files changed

+75
-1
lines changed

spec/asynciterable/from-spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { hasNext, noNext, toObserver } from '../asynciterablehelpers';
22
import { setInterval, clearInterval } from 'timers';
33
import { PartialObserver } from '../../src/observer';
44
import { from } from 'ix/asynciterable';
5+
import { AbortError } from 'ix/Ix';
6+
import { withAbort } from 'ix/asynciterable/operators';
57

68
test('AsyncIterable#from from promise list', async () => {
79
const xs: Iterable<Promise<number>> = [
@@ -246,3 +248,59 @@ test('AsyncIterable#fromObservable with error', async () => {
246248
const it = ys[Symbol.asyncIterator]();
247249
await expect(it.next()).rejects.toThrow(err);
248250
});
251+
252+
test('AsyncIterable#fromObservable with abort while waiting', async () => {
253+
let unsubscribed = false;
254+
255+
const xs = new TestObservable<number>((obs) => {
256+
obs.next(0);
257+
258+
return {
259+
unsubscribe() {
260+
unsubscribed = true;
261+
},
262+
};
263+
});
264+
265+
const abortController = new AbortController();
266+
267+
const ys = from(xs).pipe(withAbort(abortController.signal));
268+
const it = ys[Symbol.asyncIterator]();
269+
270+
await hasNext(it, 0);
271+
272+
setTimeout(() => {
273+
abortController.abort();
274+
}, 100);
275+
276+
await expect(it.next()).rejects.toBeInstanceOf(AbortError);
277+
expect(unsubscribed).toBe(true);
278+
});
279+
280+
test('AsyncIterable#fromObservable with abort while queueing', async () => {
281+
let unsubscribed = false;
282+
283+
const xs = new TestObservable<number>((obs) => {
284+
obs.next(0);
285+
obs.next(1);
286+
obs.next(2);
287+
288+
return {
289+
unsubscribe() {
290+
unsubscribed = true;
291+
},
292+
};
293+
});
294+
295+
const abortController = new AbortController();
296+
297+
const ys = from(xs).pipe(withAbort(abortController.signal));
298+
const it = ys[Symbol.asyncIterator]();
299+
300+
await hasNext(it, 0);
301+
302+
abortController.abort();
303+
304+
await expect(it.next()).rejects.toBeInstanceOf(AbortError);
305+
expect(unsubscribed).toBe(true);
306+
});

src/asynciterable/from.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { Observable } from '../observer';
1313
import { toLength } from '../util/tolength';
1414
import { AsyncSink } from './asyncsink';
15+
import { AbortError, throwIfAborted } from '../aborterror';
1516

1617
export let from: <TSource, TResult = TSource>(
1718
source: AsyncIterableInput<TSource>,
@@ -149,7 +150,9 @@ export function _initialize(Ctor: typeof AsyncIterableX) {
149150
this._selector = selector;
150151
}
151152

152-
async *[Symbol.asyncIterator]() {
153+
async *[Symbol.asyncIterator](signal?: AbortSignal) {
154+
throwIfAborted(signal);
155+
153156
const sink: AsyncSink<TSource> = new AsyncSink<TSource>();
154157
const subscription = this._observable.subscribe({
155158
next(value: TSource) {
@@ -163,12 +166,25 @@ export function _initialize(Ctor: typeof AsyncIterableX) {
163166
}
164167
});
165168

169+
function onAbort() {
170+
sink.error(new AbortError());
171+
}
172+
173+
if (signal) {
174+
signal.addEventListener('abort', onAbort);
175+
}
176+
166177
let i = 0;
167178
try {
168179
for (let next; !(next = await sink.next()).done; ) {
180+
throwIfAborted(signal);
169181
yield await this._selector(next.value!, i++);
170182
}
171183
} finally {
184+
if (signal) {
185+
signal.removeEventListener('abort', onAbort);
186+
}
187+
172188
subscription.unsubscribe();
173189
}
174190
}

0 commit comments

Comments
 (0)