Skip to content

Commit

Permalink
feat(throwIfEmpty): adds throwIfEmpty operator (#3368)
Browse files Browse the repository at this point in the history
* 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`.

* docs(throwIfEmpty): Fix minor typo in example
  • Loading branch information
benlesh committed Mar 8, 2018
1 parent 2cebbcc commit 9b21458
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 0 deletions.
134 changes: 134 additions & 0 deletions spec/operators/throwIfEmpty-spec.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
});
});
41 changes: 41 additions & 0 deletions src/internal/operators/throwIfEmpty.ts
Original file line number Diff line number Diff line change
@@ -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, 'click');
*
* 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 =
<T>(errorFactory: (() => any) = defaultErrorFactory) => tap<T>({
hasValue: false,
next() { this.hasValue = true; },
complete() {
if (!this.hasValue) {
throw errorFactory();
}
}
} as any);

function defaultErrorFactory() {
return new EmptyError();
}
1 change: 1 addition & 0 deletions src/operators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 9b21458

Please sign in to comment.