From cc060c7c53db364d51f9e33c19839810b2d953c7 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Fri, 2 Mar 2018 10:10:03 -0800 Subject: [PATCH 1/2] feat(throwIfEmpty): adds throwIfEmpty operator This is a new, simple, operator that will emit an error if the source observable completes without emitting a value. This primitive operator can be used to compose other operators such as `first` and `last`, and is a good compliment for `defaultIfEmpty`. --- spec/operators/throwIfEmpty-spec.ts | 134 +++++++++++++++++++++++++ src/internal/operators/throwIfEmpty.ts | 41 ++++++++ src/operators/index.ts | 1 + 3 files changed, 176 insertions(+) create mode 100644 spec/operators/throwIfEmpty-spec.ts create mode 100644 src/internal/operators/throwIfEmpty.ts diff --git a/spec/operators/throwIfEmpty-spec.ts b/spec/operators/throwIfEmpty-spec.ts new file mode 100644 index 0000000000..cadd98b8cb --- /dev/null +++ b/spec/operators/throwIfEmpty-spec.ts @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; +import { EMPTY, of } from '../../src'; +import { EmptyError } from '../../src/internal/util/EmptyError'; +import { throwIfEmpty } from '../../src/operators'; + +/** @test {timeout} */ +describe('throwIfEmpty', () => { + describe('with errorFactory', () => { + it('should throw if empty', () => { + const error = new Error('So empty inside'); + let thrown: any; + + EMPTY.pipe( + throwIfEmpty(() => error), + ) + .subscribe({ + error(err) { + thrown = err; + } + }); + + expect(thrown).to.equal(error); + }); + + it('should NOT throw if NOT empty', () => { + const error = new Error('So empty inside'); + let thrown: any; + + of('test').pipe( + throwIfEmpty(() => error), + ) + .subscribe({ + error(err) { + thrown = err; + } + }); + + expect(thrown).to.be.undefined; + }); + + it('should pass values through', () => { + const source = cold('----a---b---c---|'); + const sub1 = '^ !'; + const expected = '----a---b---c---|'; + expectObservable( + source.pipe(throwIfEmpty(() => new Error('test'))) + ).toBe(expected); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + + it('should never when never', () => { + const source = cold('-'); + const sub1 = '^'; + const expected = '-'; + expectObservable( + source.pipe(throwIfEmpty(() => new Error('test'))) + ).toBe(expected); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + + it('should error when empty', () => { + const source = cold('----|'); + const sub1 = '^ !'; + const expected = '----#'; + expectObservable( + source.pipe(throwIfEmpty(() => new Error('test'))) + ).toBe(expected, undefined, new Error('test')); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + }); + + describe('without errorFactory', () => { + it('should throw EmptyError if empty', () => { + let thrown: any; + + EMPTY.pipe( + throwIfEmpty(), + ) + .subscribe({ + error(err) { + thrown = err; + } + }); + + expect(thrown).to.be.instanceof(EmptyError); + }); + + it('should NOT throw if NOT empty', () => { + let thrown: any; + + of('test').pipe( + throwIfEmpty(), + ) + .subscribe({ + error(err) { + thrown = err; + } + }); + + expect(thrown).to.be.undefined; + }); + + it('should pass values through', () => { + const source = cold('----a---b---c---|'); + const sub1 = '^ !'; + const expected = '----a---b---c---|'; + expectObservable( + source.pipe(throwIfEmpty()) + ).toBe(expected); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + + it('should never when never', () => { + const source = cold('-'); + const sub1 = '^'; + const expected = '-'; + expectObservable( + source.pipe(throwIfEmpty()) + ).toBe(expected); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + + it('should error when empty', () => { + const source = cold('----|'); + const sub1 = '^ !'; + const expected = '----#'; + expectObservable( + source.pipe(throwIfEmpty()) + ).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe([sub1]); + }); + }); +}); diff --git a/src/internal/operators/throwIfEmpty.ts b/src/internal/operators/throwIfEmpty.ts new file mode 100644 index 0000000000..54487602b5 --- /dev/null +++ b/src/internal/operators/throwIfEmpty.ts @@ -0,0 +1,41 @@ +import { tap } from './tap'; +import { EmptyError } from '../util/EmptyError'; +import { MonoTypeOperatorFunction } from '../types'; + +/** + * If the source observable completes without emitting a value, it will emit + * an error. The error will be created at that time by the optional + * `errorFactory` argument, otherwise, the error will be {@link ErrorEmpty}. + * + * @example + * + * const click$ = fromEvent(button, 'clicks'); + * + * clicks$.pipe( + * takeUntil(timer(1000)), + * throwIfEmpty( + * () => new Error('the button was not clicked within 1 second') + * ), + * ) + * .subscribe({ + * next() { console.log('The button was clicked'); }, + * error(err) { console.error(err); }, + * }); + * @param {Function} [errorFactory] A factory function called to produce the + * error to be thrown when the source observable completes without emitting a + * value. + */ +export const throwIfEmpty = + (errorFactory: (() => any) = defaultErrorFactory) => tap({ + hasValue: false, + next() { this.hasValue = true; }, + complete() { + if (!this.hasValue) { + throw errorFactory(); + } + } + } as any); + +function defaultErrorFactory() { + return new EmptyError(); +} diff --git a/src/operators/index.ts b/src/operators/index.ts index 4f5dacbdec..a2ed912d6d 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -90,6 +90,7 @@ export { takeWhile } from '../internal/operators/takeWhile'; export { tap } from '../internal/operators/tap'; export { throttle } from '../internal/operators/throttle'; export { throttleTime } from '../internal/operators/throttleTime'; +export { throwIfEmpty } from '../internal/operators/throwIfEmpty'; export { timeInterval } from '../internal/operators/timeInterval'; export { timeout } from '../internal/operators/timeout'; export { timeoutWith } from '../internal/operators/timeoutWith'; From 574901969e450f178a3dd192ab8ec9aa3d7c61ef Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 7 Mar 2018 16:32:14 -0800 Subject: [PATCH 2/2] docs(throwIfEmpty): Fix minor typo in example --- src/internal/operators/throwIfEmpty.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/operators/throwIfEmpty.ts b/src/internal/operators/throwIfEmpty.ts index 54487602b5..872c604d9c 100644 --- a/src/internal/operators/throwIfEmpty.ts +++ b/src/internal/operators/throwIfEmpty.ts @@ -9,7 +9,7 @@ import { MonoTypeOperatorFunction } from '../types'; * * @example * - * const click$ = fromEvent(button, 'clicks'); + * const click$ = fromEvent(button, 'click'); * * clicks$.pipe( * takeUntil(timer(1000)),