From 8d8216a84fe1fb4b4db71cf9016c970a721c63cf Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 26 Feb 2020 15:52:02 -0600 Subject: [PATCH 1/4] chore: update tests to use TestScheduler --- spec/operators/single-spec.ts | 273 ++++++++++++++++++---------------- 1 file changed, 144 insertions(+), 129 deletions(-) diff --git a/spec/operators/single-spec.ts b/spec/operators/single-spec.ts index 033abdb860..64de21f3df 100644 --- a/spec/operators/single-spec.ts +++ b/spec/operators/single-spec.ts @@ -1,173 +1,188 @@ import { expect } from 'chai'; -import { hot, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; import { single, mergeMap, tap } from 'rxjs/operators'; import { of, EmptyError } from 'rxjs'; - -declare function asDiagram(arg: string): Function; +import { TestScheduler } from 'rxjs/testing'; +import { assertDeepEquals } from '../helpers/test-helper'; /** @test {single} */ describe('single operator', () => { - asDiagram('single')('should raise error from empty predicate if observable emits multiple time', () => { - const e1 = hot('--a--b--c--|'); - const e1subs = '^ ! '; - const expected = '-----# '; - const errorMsg = 'Sequence contains more than one element'; - - expectObservable(e1.pipe(single())).toBe(expected, null, errorMsg); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + let rxTest: TestScheduler; + + beforeEach(() => { + rxTest = new TestScheduler(assertDeepEquals); }); - it('should raise error from empty predicate if observable does not emit', () => { - const e1 = hot('--a--^--|'); - const e1subs = '^ !'; - const expected = '---#'; + it('should raise error from empty predicate if observable emits multiple time', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const e1subs = ' ^----! '; + const expected = '-----# '; + const errorMsg = 'Sequence contains more than one element'; - expectObservable(e1.pipe(single())).toBe(expected, null, new EmptyError()); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectObservable(e1.pipe(single())).toBe(expected, null, errorMsg); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); - it('should return only element from empty predicate if observable emits only once', () => { - const e1 = hot('--a--|'); - const e1subs = '^ !'; - const expected = '-----(a|)'; + it('should raise error from empty predicate if observable does not emit', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--|'); + const e1subs = ' ^--!'; + const expected = ' ---#'; + + expectObservable(e1.pipe(single())).toBe(expected, null, new EmptyError()); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); + }); - expectObservable(e1.pipe(single())).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + it('should return only element from empty predicate if observable emits only once', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--|'); + const e1subs = ' ^----!'; + const expected = '-----(a|)'; + + expectObservable(e1.pipe(single())).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should allow unsubscribing explicitly and early', () => { - const e1 = hot('--a--b--c--|'); - const unsub = ' ! '; - const e1subs = '^ ! '; - const expected = '---- '; - - expectObservable(e1.pipe(single()), unsub).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const unsub = ' ----! '; + const e1subs = ' ^---! '; + const expected = '------------'; + + expectObservable(e1.pipe(single()), unsub).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should not break unsubscription chains when result is unsubscribed explicitly', () => { - const e1 = hot('--a--b--c--|'); - const e1subs = '^ ! '; - const expected = '---- '; - const unsub = ' ! '; - - const result = e1.pipe( - mergeMap((x: string) => of(x)), - single(), - mergeMap((x: string) => of(x)) - ); - - expectObservable(result, unsub).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const e1subs = ' ^--! '; + const expected = '---- '; + const unsub = ' ---! '; + + const result = e1.pipe( + mergeMap(x => of(x)), + single(), + mergeMap(x => of(x)) + ); + + expectObservable(result, unsub).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should raise error from empty predicate if observable emits error', () => { - const e1 = hot('--a--b^--#'); - const e1subs = '^ !'; - const expected = '---#'; - - expectObservable(e1.pipe(single())).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b^--#'); + const e1subs = ' ^--!'; + const expected = '---#'; + + expectObservable(e1.pipe(single())).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should raise error from predicate if observable emits error', () => { - const e1 = hot('--a--b^--#'); - const e1subs = '^ !'; - const expected = '---#'; - - const predicate = function (value: string) { - return value === 'c'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--b^--#'); + const e1subs = ' ^--!'; + const expected = ' ---#'; + + expectObservable(e1.pipe(single(v => v === 'c'))).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should raise error if predicate throws error', () => { - const e1 = hot('--a--b--c--d--|'); - const e1subs = '^ ! '; - const expected = '-----------# '; - - const predicate = function (value: string) { - if (value !== 'd') { - return false; - } - throw 'error'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--d--|'); + const e1subs = ' ^----------! '; + const expected = '-----------# '; + + expectObservable( + e1.pipe( + single(v => { + if (v !== 'd') { + return false; + } + throw 'error'; + }) + ) + ).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should return element from predicate if observable have single matching element', () => { - const e1 = hot('--a--b--c--|'); - const e1subs = '^ !'; - const expected = '-----------(b|)'; - - const predicate = function (value: string) { - return value === 'b'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const e1subs = ' ^----------!'; + const expected = '-----------(b|)'; + + expectObservable(e1.pipe(single(v => v === 'b'))).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should raise error from predicate if observable have multiple matching element', () => { - const e1 = hot('--a--b--a--b--b--|'); - const e1subs = '^ ! '; - const expected = '-----------# '; - - const predicate = function (value: string) { - return value === 'b'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected, null, 'Sequence contains more than one element'); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--a--b--b--|'); + const e1subs = ' ^----------! '; + const expected = '-----------# '; + + expectObservable(e1.pipe(single(v => v === 'b'))).toBe(expected, null, 'Sequence contains more than one element'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should raise error from predicate if observable does not emit', () => { - const e1 = hot('--a--^--|'); - const e1subs = '^ !'; - const expected = '---#'; - - const predicate = function (value: string) { - return value === 'a'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected, null, new EmptyError()); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--|'); + const e1subs = ' ^--!'; + const expected = ' ---#'; + + expectObservable(e1.pipe(single(v => v === 'a'))).toBe(expected, null, new EmptyError()); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should return undefined from predicate if observable does not contain matching element', () => { - const e1 = hot('--a--b--c--|'); - const e1subs = '^ !'; - const expected = '-----------(z|)'; - - const predicate = function (value: string) { - return value === 'x'; - }; - - expectObservable(e1.pipe(single(predicate))).toBe(expected, {z: undefined}); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const e1subs = ' ^----------!'; + const expected = '-----------(z|)'; + + expectObservable(e1.pipe(single(v => v === 'x'))).toBe(expected, { z: undefined }); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); it('should call predicate with indices starting at 0', () => { - const e1 = hot('--a--b--c--|'); - const e1subs = '^ !'; - const expected = '-----------(b|)'; - - let indices: number[] = []; - const predicate = function(value: string, index: number) { - indices.push(index); - return value === 'b'; - }; - - expectObservable(e1.pipe( - single(predicate), - tap(null, null, () => { - expect(indices).to.deep.equal([0, 1, 2]); - })) - ).toBe(expected); - expectSubscriptions(e1.subscriptions).toBe(e1subs); + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b--c--|'); + const e1subs = ' ^----------!'; + const expected = '-----------(b|)'; + + let indices: number[] = []; + const predicate = function(value: string, index: number) { + indices.push(index); + return value === 'b'; + }; + + expectObservable( + e1.pipe( + single(predicate), + tap(null, null, () => { + expect(indices).to.deep.equal([0, 1, 2]); + }) + ) + ).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + }); }); }); From b1908ff2bb3ae980dff8ce350f1db4cb755df79e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 26 Feb 2020 16:43:55 -0600 Subject: [PATCH 2/4] fix(single): single throws appropriately - Will now throw new NotFoundError and SequenceError types in appropriate scenarios - Updates documentation BREAKING CHANGE: Single will now throw for scenarios where values coming in are either not present, or do not match the provided predicate. Error types have thrown have also been updated, please check documentation for changes. --- spec/operators/single-spec.ts | 155 ++++++++++++++++++++++- src/index.ts | 4 +- src/internal/operators/single.ts | 196 ++++++++++++++++------------- src/internal/util/NotFoundError.ts | 29 +++++ src/internal/util/SequenceError.ts | 29 +++++ 5 files changed, 316 insertions(+), 97 deletions(-) create mode 100644 src/internal/util/NotFoundError.ts create mode 100644 src/internal/util/SequenceError.ts diff --git a/spec/operators/single-spec.ts b/spec/operators/single-spec.ts index 64de21f3df..8229e6f7f7 100644 --- a/spec/operators/single-spec.ts +++ b/spec/operators/single-spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { single, mergeMap, tap } from 'rxjs/operators'; -import { of, EmptyError } from 'rxjs'; +import { of, EmptyError, SequenceError, NotFoundError } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { assertDeepEquals } from '../helpers/test-helper'; @@ -17,9 +17,8 @@ describe('single operator', () => { const e1 = hot(' --a--b--c--|'); const e1subs = ' ^----! '; const expected = '-----# '; - const errorMsg = 'Sequence contains more than one element'; - expectObservable(e1.pipe(single())).toBe(expected, null, errorMsg); + expectObservable(e1.pipe(single())).toBe(expected, null, new SequenceError('Too many matching values')); expectSubscriptions(e1.subscriptions).toBe(e1subs); }); }); @@ -135,7 +134,7 @@ describe('single operator', () => { const e1subs = ' ^----------! '; const expected = '-----------# '; - expectObservable(e1.pipe(single(v => v === 'b'))).toBe(expected, null, 'Sequence contains more than one element'); + expectObservable(e1.pipe(single(v => v === 'b'))).toBe(expected, null, new SequenceError('Too many matching values')); expectSubscriptions(e1.subscriptions).toBe(e1subs); }); }); @@ -155,9 +154,9 @@ describe('single operator', () => { rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { const e1 = hot(' --a--b--c--|'); const e1subs = ' ^----------!'; - const expected = '-----------(z|)'; + const expected = '-----------#'; - expectObservable(e1.pipe(single(v => v === 'x'))).toBe(expected, { z: undefined }); + expectObservable(e1.pipe(single(v => v === 'x'))).toBe(expected, undefined, new NotFoundError('No matching values')); expectSubscriptions(e1.subscriptions).toBe(e1subs); }); }); @@ -185,4 +184,148 @@ describe('single operator', () => { expectSubscriptions(e1.subscriptions).toBe(e1subs); }); }); + + it('should error for synchronous empty observables when no arguments are provided', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('|'); + const expected = ' #'; + const subs = [' (^!)']; + const result = source.pipe(single()); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for async empty observables when no arguments are provided', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('-------|'); + const expected = ' -------#'; + const subs = [' ^------!']; + const result = source.pipe(single()); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for hot observables that do not emit while active when no arguments are provided', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const source = hot('--a--b--^----|'); + const expected = ' -----#'; + const subs = [' ^----!']; + const result = source.pipe(single()); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for synchronous empty observables when predicate never passes', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('|'); + const expected = ' #'; + const subs = [' (^!)']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for async empty observables when predicate never passes', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('-------|'); + const expected = ' -------#'; + const subs = [' ^------!']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for hot observables that do not emit while active when predicate never passes', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const source = hot('--a--b--^----|'); + const expected = ' -----#'; + const subs = [' ^----!']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new EmptyError()); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for synchronous observables that emit when predicate never passes', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('(a|)'); + const expected = ' #'; + const subs = [' (^!)']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new NotFoundError('No matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for async observables that emit when predicate never passes', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('--a--b-|'); + const expected = ' -------#'; + const subs = [' ^------!']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new NotFoundError('No matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for hot observables that emit while active when predicate never passes', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const source = hot('--a--b--^--c--d--|'); + const expected = ' ---------#'; + const subs = [' ^--------!']; + const result = source.pipe(single(() => false)); + + expectObservable(result).toBe(expected, undefined, new NotFoundError('No matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for synchronous observables when the predicate passes more than once', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('(axbxc|)'); + const expected = ' #'; + const subs = [' (^!)']; + const result = source.pipe(single(v => v === 'x')); + + expectObservable(result).toBe(expected, undefined, new SequenceError('Too many matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for async observables that emit when the predicate passes more than once', () => { + rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { + const source = cold('--a-x-b-x-c-|'); + const expected = ' --------#'; + const subs = [' ^-------!']; + const result = source.pipe(single(v => v === 'x')); + + expectObservable(result).toBe(expected, undefined, new SequenceError('Too many matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should error for hot observables that emit while active when the predicate passes more than once', () => { + rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { + const source = hot('--a--b--^--c--x--d--x--|'); + const expected = ' ------------#'; + const subs = [' ^-----------!']; + const result = source.pipe(single(v => v === 'x')); + + expectObservable(result).toBe(expected, undefined, new SequenceError('Too many matching values')); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); }); diff --git a/src/index.ts b/src/index.ts index 80f5c4f38b..bafa33eeec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,9 +40,11 @@ export { firstValueFrom } from './internal/firstValueFrom'; /* Error types */ export { ArgumentOutOfRangeError } from './internal/util/ArgumentOutOfRangeError'; export { EmptyError } from './internal/util/EmptyError'; +export { NotFoundError } from './internal/util/NotFoundError'; export { ObjectUnsubscribedError } from './internal/util/ObjectUnsubscribedError'; -export { UnsubscriptionError } from './internal/util/UnsubscriptionError'; +export { SequenceError } from './internal/util/SequenceError'; export { TimeoutError } from './internal/util/TimeoutError'; +export { UnsubscriptionError } from './internal/util/UnsubscriptionError'; /* Static observable creation exports */ export { bindCallback } from './internal/observable/bindCallback'; diff --git a/src/internal/operators/single.ts b/src/internal/operators/single.ts index cc2eab6be5..ac504300a3 100644 --- a/src/internal/operators/single.ts +++ b/src/internal/operators/single.ts @@ -4,37 +4,77 @@ import { Subscriber } from '../Subscriber'; import { EmptyError } from '../util/EmptyError'; import { Observer, MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { filter } from './filter'; +import { SequenceError } from '../util/SequenceError'; +import { throwIfEmpty } from './throwIfEmpty'; +import { NotFoundError } from '../util/NotFoundError'; + +const defaultPredicate = () => true; /** - * Returns an Observable that emits the single item emitted by the source Observable that matches a specified - * predicate, if that Observable emits one such item. If the source Observable emits more than one such item or no - * items, notify of an IllegalArgumentException or NoSuchElementException respectively. If the source Observable - * emits items but none match the specified predicate then `undefined` is emitted. + * Returns an observable that asserts that only one value is + * emitted from the observable that matches the predicate. If no + * predicate is provided, then it will assert that the observable + * only emits one value. + * + * In the event that the observable is empty, it will throw an + * {@link EmptyError}. * - * Like {@link first}, but emit with error notification if there is more than one value. - * ![](single.png) + * In the event that two values are found that match the predicate, + * or when there are two values emitted and no predicate, it will + * throw a {@link SequenceError} + * + * In the event that no values match the predicate, if one is provided, + * it will throw a {@link NotFoundError} * * ## Example - * emits 'error' - * ```ts - * import { range } from 'rxjs'; - * import { single } from 'rxjs/operators'; * - * const numbers = range(1,5).pipe(single()); - * numbers.subscribe(x => console.log('never get called'), e => console.log('error')); - * // result - * // 'error' - * ``` + * Expect only name beginning with 'B': * - * emits 'undefined' * ```ts - * import { range } from 'rxjs'; + * import { of } from 'rxjs'; * import { single } from 'rxjs/operators'; * - * const numbers = range(1,5).pipe(single(x => x === 10)); - * numbers.subscribe(x => console.log(x)); - * // result - * // 'undefined' + * const source1 = of( + * { name: 'Ben' }, + * { name: 'Tracy' }, + * { name: 'Laney' }, + * { name: 'Lily' } + * ); + * + * source1.pipe( + * single(x => x.name.startsWith('B')) + * ) + * .subscribe(x => console.log(x)); + * // Emits "Ben" + * + * + * const source2 = of( + * { name: 'Ben' }, + * { name: 'Tracy' }, + * { name: 'Bradley' }, + * { name: 'Lincoln' } + * ); + * + * source2.pipe( + * single(x => x.name.startsWith('B')) + * ) + * .subscribe(x => console.log(x)); + * // Error emitted: SequenceError('Too many values match') + * + * + * const source3 = of( + * { name: 'Laney' }, + * { name: 'Tracy' }, + * { name: 'Lily' }, + * { name: 'Lincoln' } + * ); + * + * source3.pipe( + * single(x => x.name.startsWith('B')) + * ) + * .subscribe(x => console.log(x)); + * // Error emitted: NotFoundError('No values match') * ``` * * @see {@link first} @@ -42,81 +82,57 @@ import { Observer, MonoTypeOperatorFunction, TeardownLogic } from '../types'; * @see {@link findIndex} * @see {@link elementAt} * - * @throws {EmptyError} Delivers an EmptyError to the Observer's `error` + * @throws {NotFoundError} Delivers an NotFoundError to the Observer's `error` * callback if the Observable completes before any `next` notification was sent. + * @throws {SequenceError} Delivers a SequenceError if more than one value is emitted that matches the + * provided predicate. If no predicate is provided, will deliver a SequenceError if more + * that one value comes from the source * @param {Function} predicate - A predicate function to evaluate items emitted by the source Observable. * @return {Observable} An Observable that emits the single item emitted by the source Observable that matches * the predicate or `undefined` when no items match. - * - * @name single */ -export function single(predicate?: (value: T, index: number, source: Observable) => boolean): MonoTypeOperatorFunction { - return (source: Observable) => source.lift(new SingleOperator(predicate, source)); -} - -class SingleOperator implements Operator { - constructor(private predicate: ((value: T, index: number, source: Observable) => boolean) | undefined, - private source: Observable) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SingleSubscriber(subscriber, this.predicate, this.source)); - } +export function single( + predicate: (value: T, index: number) => boolean = defaultPredicate +): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(singleOperator(predicate)); } -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SingleSubscriber extends Subscriber { - private seenValue: boolean = false; - private singleValue: T | undefined; - private index: number = 0; - - constructor(destination: Observer, - private predicate: ((value: T, index: number, source: Observable) => boolean) | undefined, - private source: Observable) { - super(destination); - } - - private applySingleValue(value: T): void { - if (this.seenValue) { - this.destination.error('Sequence contains more than one element'); - } else { - this.seenValue = true; - this.singleValue = value; - } - } - - protected _next(value: T): void { - const index = this.index++; - - if (this.predicate) { - this.tryNext(value, index); - } else { - this.applySingleValue(value); - } - } - - private tryNext(value: T, index: number): void { - try { - if (this.predicate!(value, index, this.source)) { - this.applySingleValue(value); - } - } catch (err) { - this.destination.error(err); - } - } - - protected _complete(): void { - const destination = this.destination; +function singleOperator(predicate: (value: T, index: number) => boolean) { + return function(this: Subscriber, source: Observable) { + let _hasValue = false; + let _seenValue = false; + let _value: T; + let _i = 0; + const _destination = this; - if (this.index > 0) { - destination.next(this.seenValue ? this.singleValue : undefined); - destination.complete(); - } else { - destination.error(new EmptyError); - } - } + return source.subscribe({ + next: value => { + _seenValue = true; + let match = false; + try { + match = predicate(value, _i++); + } catch (err) { + _destination.error(err); + return; + } + if (match) { + if (_hasValue) { + _destination.error(new SequenceError('Too many matching values')); + } else { + _hasValue = true; + _value = value; + } + } + }, + error: err => _destination.error(err), + complete: () => { + if (_hasValue) { + _destination.next(_value); + _destination.complete(); + } else { + _destination.error(_seenValue ? new NotFoundError('No matching values') : new EmptyError()); + } + }, + }); + }; } diff --git a/src/internal/util/NotFoundError.ts b/src/internal/util/NotFoundError.ts new file mode 100644 index 0000000000..f97c0ce8ed --- /dev/null +++ b/src/internal/util/NotFoundError.ts @@ -0,0 +1,29 @@ +export interface NotFoundError extends Error { +} + +export interface NotFoundErrorCtor { + new(message: string): NotFoundError; +} + +const NotFoundErrorImpl = (() => { + function NotFoundErrorImpl(this: Error, message: string) { + Error.call(this); + this.message = message; + this.name = 'NotFoundError'; + return this; + } + + NotFoundErrorImpl.prototype = Object.create(Error.prototype); + + return NotFoundErrorImpl; +})(); + +/** + * An error thrown when a value or values are missing from an + * observable sequence. + * + * @see {@link operators/single} + * + * @class NotFoundError + */ +export const NotFoundError: NotFoundErrorCtor = NotFoundErrorImpl as any; diff --git a/src/internal/util/SequenceError.ts b/src/internal/util/SequenceError.ts new file mode 100644 index 0000000000..01379d7e71 --- /dev/null +++ b/src/internal/util/SequenceError.ts @@ -0,0 +1,29 @@ +export interface SequenceError extends Error { +} + +export interface SequenceErrorCtor { + new(message: string): SequenceError; +} + +const SequenceErrorImpl = (() => { + function SequenceErrorImpl(this: Error, message: string) { + Error.call(this); + this.message = message; + this.name = 'SequenceError'; + return this; + } + + SequenceErrorImpl.prototype = Object.create(Error.prototype); + + return SequenceErrorImpl; +})(); + +/** + * An error thrown when something is wrong with the sequence of + * values arriving on the observable. + * + * @see {@link operators/single} + * + * @class SequenceError + */ +export const SequenceError: SequenceErrorCtor = SequenceErrorImpl as any; From f2654a1d66bc532f7fe145c5590f69beb6a478ce Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 26 Feb 2020 17:38:01 -0600 Subject: [PATCH 3/4] chore: readd missing source argument --- src/internal/operators/single.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/internal/operators/single.ts b/src/internal/operators/single.ts index ac504300a3..9a95ce5b9b 100644 --- a/src/internal/operators/single.ts +++ b/src/internal/operators/single.ts @@ -1,16 +1,22 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { EmptyError } from '../util/EmptyError'; -import { Observer, MonoTypeOperatorFunction, TeardownLogic } from '../types'; -import { filter } from './filter'; +import { MonoTypeOperatorFunction } from '../types'; import { SequenceError } from '../util/SequenceError'; -import { throwIfEmpty } from './throwIfEmpty'; import { NotFoundError } from '../util/NotFoundError'; const defaultPredicate = () => true; +export function single(): MonoTypeOperatorFunction; +export function single( + predicate: (value: T, index: number) => boolean +): MonoTypeOperatorFunction; +/** @deprecated Providing `source` via the third argument to the predicate will be removed in upcoming versions. Use a closure. */ +export function single( + predicate: (value: T, index: number, source: Observable) => boolean +): MonoTypeOperatorFunction; + /** * Returns an observable that asserts that only one value is * emitted from the observable that matches the predicate. If no @@ -92,12 +98,12 @@ const defaultPredicate = () => true; * the predicate or `undefined` when no items match. */ export function single( - predicate: (value: T, index: number) => boolean = defaultPredicate + predicate: (value: T, index: number, source: Observable) => boolean = defaultPredicate ): MonoTypeOperatorFunction { return (source: Observable) => source.lift(singleOperator(predicate)); } -function singleOperator(predicate: (value: T, index: number) => boolean) { +function singleOperator(predicate: (value: T, index: number, source: Observable) => boolean) { return function(this: Subscriber, source: Observable) { let _hasValue = false; let _seenValue = false; @@ -110,7 +116,7 @@ function singleOperator(predicate: (value: T, index: number) => boolean) { _seenValue = true; let match = false; try { - match = predicate(value, _i++); + match = predicate(value, _i++, source); } catch (err) { _destination.error(err); return; From 466295fb03ae431e1f2bb53e238faf1ee4c0ca2d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 26 Feb 2020 17:57:23 -0600 Subject: [PATCH 4/4] chore: fix dtslint --- spec/operators/single-spec.ts | 4 ++-- src/internal/operators/single.ts | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/spec/operators/single-spec.ts b/spec/operators/single-spec.ts index 8229e6f7f7..db7b720f3f 100644 --- a/spec/operators/single-spec.ts +++ b/spec/operators/single-spec.ts @@ -78,8 +78,8 @@ describe('single operator', () => { it('should raise error from empty predicate if observable emits error', () => { rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { const e1 = hot(' --a--b^--#'); - const e1subs = ' ^--!'; - const expected = '---#'; + const e1subs = ' ^--!'; + const expected = ' ---#'; expectObservable(e1.pipe(single())).toBe(expected); expectSubscriptions(e1.subscriptions).toBe(e1subs); diff --git a/src/internal/operators/single.ts b/src/internal/operators/single.ts index 9a95ce5b9b..cf6c28cb81 100644 --- a/src/internal/operators/single.ts +++ b/src/internal/operators/single.ts @@ -8,15 +8,6 @@ import { NotFoundError } from '../util/NotFoundError'; const defaultPredicate = () => true; -export function single(): MonoTypeOperatorFunction; -export function single( - predicate: (value: T, index: number) => boolean -): MonoTypeOperatorFunction; -/** @deprecated Providing `source` via the third argument to the predicate will be removed in upcoming versions. Use a closure. */ -export function single( - predicate: (value: T, index: number, source: Observable) => boolean -): MonoTypeOperatorFunction; - /** * Returns an observable that asserts that only one value is * emitted from the observable that matches the predicate. If no