UNPKG

abortable-rx

Version:

Drop-in replacements for RxJS Observable methods and operators that work with AbortSignal

272 lines 12.4 kB
Object.assign(global, require('abort-controller')); import { AssertionError } from 'assert'; import { assert } from 'chai'; import { Observable, of, Subject, throwError } from 'rxjs'; import { delay } from 'rxjs/operators'; import * as sinon from 'sinon'; import { concatMap, create, defer, forEach, mergeMap, switchMap, toPromise } from '.'; describe('Observable factories', () => { describe('create()', () => { it('should abort the passed AbortSignal when the returned Observable is unsubscribed from', () => { const subscribe = sinon.spy(); const obs = create(subscribe); assert.instanceOf(obs, Observable); sinon.assert.notCalled(subscribe); const onnext = sinon.spy(); const onerror = sinon.spy(); const oncomplete = sinon.spy(); const subscription = obs.subscribe(onnext, onerror, oncomplete); sinon.assert.calledOnce(subscribe); sinon.assert.notCalled(onnext); const [subscriber, signal] = subscribe.args[0]; assert.instanceOf(signal, AbortSignal); assert.isFalse(signal.aborted); const onabort = sinon.spy(); signal.onabort = onabort; subscriber.next(1); sinon.assert.calledOnce(onnext); sinon.assert.calledWith(onnext, 1); subscription.unsubscribe(); sinon.assert.calledOnce(onabort); subscriber.error(new Error()); // Simulate error that might be thrown on abort sinon.assert.notCalled(onerror); sinon.assert.notCalled(oncomplete); assert.isTrue(signal.aborted); }); }); describe('defer()', () => { it('should abort the passed AbortSignal when the returned Observable is unsubscribed from', () => { const subject = new Subject(); const factory = sinon.spy(() => subject); const obs = defer(factory); assert.instanceOf(obs, Observable); sinon.assert.notCalled(factory); const onnext = sinon.stub(); const onerror = sinon.stub(); const oncomplete = sinon.stub(); const subscription = obs.subscribe(onnext, onerror, oncomplete); sinon.assert.calledOnce(factory); sinon.assert.notCalled(onnext); const [signal] = factory.args[0]; assert.instanceOf(signal, AbortSignal); assert.isFalse(signal.aborted); const onabort = sinon.stub(); signal.onabort = onabort; subject.next(1); sinon.assert.calledOnce(onnext); sinon.assert.calledWith(onnext, 1); subscription.unsubscribe(); sinon.assert.calledOnce(onabort); subject.error(new Error()); // Simulate error that might be thrown on abort sinon.assert.notCalled(onerror); sinon.assert.notCalled(oncomplete); assert.isTrue(signal.aborted); }); }); }); describe('Observable consumers', () => { describe('toPromise()', () => { it('should unsubscribe from the given Observable when the AbortSignal is aborted', async () => { const teardown = sinon.spy(); const subscribe = sinon.spy((subscriber) => teardown); const obs = new Observable(subscribe); const abortController = new AbortController(); const promise = toPromise(obs, abortController.signal); sinon.assert.notCalled(teardown); abortController.abort(); sinon.assert.calledOnce(teardown); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.instanceOf(err, Error); assert.propertyVal(err, 'name', 'AbortError'); } }); it('should never subscribe to the Observable if the AbortSignal is already aborted', async () => { const subscribe = sinon.spy(); const obs = new Observable(subscribe); const abortController = new AbortController(); abortController.abort(); const promise = toPromise(obs, abortController.signal); sinon.assert.notCalled(subscribe); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.instanceOf(err, Error); assert.propertyVal(err, 'name', 'AbortError'); } }); it('should resolve with the last value emitted', async () => { const obs = of(1, 2, 3); const abortController = new AbortController(); const value = await toPromise(obs, abortController.signal); assert.strictEqual(value, 3); }); it('should reject if the Observable errors', async () => { const obs = throwError(123); const abortController = new AbortController(); const promise = toPromise(obs, abortController.signal); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.strictEqual(err, 123); } }); }); describe('forEach()', () => { it('should unsubscribe from the given Observable when the AbortSignal is aborted', async () => { const teardown = sinon.spy(); const subscribe = sinon.spy((subscriber) => teardown); const obs = new Observable(subscribe); const abortController = new AbortController(); const onnext = sinon.spy(); const promise = forEach(obs, onnext, abortController.signal); sinon.assert.notCalled(onnext); const [subscriber] = subscribe.args[0]; subscriber.next(1); assert.deepStrictEqual(onnext.args[0], [1]); subscriber.next(2); assert.deepStrictEqual(onnext.args[1], [2]); sinon.assert.notCalled(teardown); abortController.abort(); sinon.assert.calledOnce(teardown); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.instanceOf(err, Error); assert.propertyVal(err, 'name', 'AbortError'); } }); it('should never subscribe to the Observable when the AbortSignal is already aborted', async () => { const subscribe = sinon.spy(); const obs = new Observable(subscribe); const abortController = new AbortController(); abortController.abort(); const onnext = sinon.spy(); const promise = forEach(obs, onnext, abortController.signal); sinon.assert.notCalled(subscribe); sinon.assert.notCalled(onnext); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.instanceOf(err, Error); assert.propertyVal(err, 'name', 'AbortError'); } }); it('should resolve the Promise when the Observable completes', async () => { const obs = of(1, 2, 3); const abortController = new AbortController(); const onnext = sinon.spy(); await forEach(obs, onnext, abortController.signal); assert.deepStrictEqual(onnext.args, [[1], [2], [3]]); }); it('should reject the Promise when the Observable errors', async () => { const error = new Error(); const obs = throwError(error); const abortController = new AbortController(); try { await forEach(obs, () => undefined, abortController.signal); throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.strictEqual(err, error); } }); it('should reject the Promise when the next function throws and unsubscribe the Observable', async () => { const error = new Error(); const teardown = sinon.spy(); const subscribe = sinon.spy((subscriber) => teardown); const obs = new Observable(subscribe).pipe(delay(1)); const abortController = new AbortController(); const promise = forEach(obs, () => { throw error; }, abortController.signal); sinon.assert.notCalled(teardown); const [subscriber] = subscribe.args[0]; subscriber.next(1); try { await promise; throw new AssertionError({ message: 'Expected Promise to be rejected' }); } catch (err) { assert.strictEqual(err, error); } sinon.assert.calledOnce(teardown); }); }); }); describe('Observable operators', () => { describe('switchMap()', () => { it('should abort the passed AbortSignal when the source emits a new item', () => { const source = new Subject(); const project = sinon.spy(() => new Subject()); const obs = source.pipe(switchMap(project)); sinon.assert.notCalled(project); const onnext = sinon.spy(); const onerror = sinon.spy(); const oncomplete = sinon.spy(); obs.subscribe(onnext, onerror, oncomplete); source.next('a'); sinon.assert.calledOnce(project); const [value, index, signal] = project.args[0]; assert.strictEqual(value, 'a'); assert.strictEqual(index, 0); assert.instanceOf(signal, AbortSignal); assert.isFalse(signal.aborted); const onabort = sinon.spy(); signal.onabort = onabort; source.next('b'); sinon.assert.calledOnce(onabort); assert.isTrue(signal.aborted); const returnedSubject = project.returnValues[0]; returnedSubject.next('a1'); sinon.assert.notCalled(onnext); returnedSubject.error(new Error()); // Simulate error that might be thrown on abort sinon.assert.notCalled(onerror); sinon.assert.notCalled(oncomplete); }); }); for (const [name, operator] of new Map([['concatMap()', concatMap], ['mergeMap()', mergeMap]])) { describe(name, () => { it('should abort the passed AbortSignal when the returned Observable is unsubscribed from', () => { const source = new Subject(); const project = sinon.spy(() => new Subject()); const obs = source.pipe(operator(project)); sinon.assert.notCalled(project); const onnext = sinon.spy(); const onerror = sinon.spy(); const oncomplete = sinon.spy(); const subscription = obs.subscribe(onnext, onerror, oncomplete); source.next('a'); sinon.assert.calledOnce(project); const [value, index, signal] = project.args[0]; assert.strictEqual(value, 'a'); assert.strictEqual(index, 0); assert.instanceOf(signal, AbortSignal); assert.isFalse(signal.aborted); const onabort = sinon.spy(); signal.onabort = onabort; subscription.unsubscribe(); sinon.assert.calledOnce(onabort); assert.isTrue(signal.aborted); const returnedSubject = project.returnValues[0]; returnedSubject.next('a1'); sinon.assert.notCalled(onnext); returnedSubject.error(new Error()); // Simulate error that might be thrown on abort sinon.assert.notCalled(onerror); sinon.assert.notCalled(oncomplete); }); }); } }); //# sourceMappingURL=index.test.js.map