diff --git a/spec/operators/single-spec.ts b/spec/operators/single-spec.ts
index 033abdb860..db7b720f3f 100644
--- a/spec/operators/single-spec.ts
+++ b/spec/operators/single-spec.ts
@@ -1,173 +1,331 @@
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 { of, EmptyError, SequenceError, NotFoundError } from 'rxjs';
+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';
+ let rxTest: TestScheduler;
- expectObservable(e1.pipe(single())).toBe(expected, null, errorMsg);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ 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 = '-----# ';
- expectObservable(e1.pipe(single())).toBe(expected, null, new EmptyError());
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(e1.pipe(single())).toBe(expected, null, new SequenceError('Too many matching values'));
+ 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 = ' ! ';
+ 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);
+ });
+ });
- const result = e1.pipe(
- mergeMap((x: string) => of(x)),
- single(),
- mergeMap((x: string) => of(x))
- );
+ 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 = ' ---#';
+
+ expectObservable(e1.pipe(single())).toBe(expected);
+ expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ });
+ });
- expectObservable(result, unsub).toBe(expected);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ it('should raise error from predicate if observable emits error', () => {
+ 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 from empty predicate if observable emits error', () => {
- const e1 = hot('--a--b^--#');
- const e1subs = '^ !';
- const expected = '---#';
+ it('should raise error if predicate throws error', () => {
+ 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);
+ });
+ });
- expectObservable(e1.pipe(single())).toBe(expected);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ it('should return element from predicate if observable have single matching element', () => {
+ 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 emits error', () => {
- const e1 = hot('--a--b^--#');
- const e1subs = '^ !';
- const expected = '---#';
+ it('should raise error from predicate if observable have multiple matching element', () => {
+ 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, new SequenceError('Too many matching values'));
+ expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ });
+ });
- const predicate = function (value: string) {
- return value === 'c';
- };
+ it('should raise error from predicate if observable does not emit', () => {
+ 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);
+ });
+ });
- expectObservable(e1.pipe(single(predicate))).toBe(expected);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ it('should return undefined from predicate if observable does not contain matching element', () => {
+ rxTest.run(({ hot, expectObservable, expectSubscriptions }) => {
+ const e1 = hot(' --a--b--c--|');
+ const e1subs = ' ^----------!';
+ const expected = '-----------#';
+
+ expectObservable(e1.pipe(single(v => v === 'x'))).toBe(expected, undefined, new NotFoundError('No matching values'));
+ expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ });
});
- it('should raise error if predicate throws error', () => {
- const e1 = hot('--a--b--c--d--|');
- const e1subs = '^ ! ';
- const expected = '-----------# ';
+ it('should call predicate with indices starting at 0', () => {
+ 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);
+ });
+ });
- const predicate = function (value: string) {
- if (value !== 'd') {
- return false;
- }
- throw 'error';
- };
+ 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(e1.pipe(single(predicate))).toBe(expected);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(result).toBe(expected, undefined, new EmptyError());
+ expectSubscriptions(source.subscriptions).toBe(subs);
+ });
});
- it('should return element from predicate if observable have single matching element', () => {
- const e1 = hot('--a--b--c--|');
- const e1subs = '^ !';
- const expected = '-----------(b|)';
+ 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());
- const predicate = function (value: string) {
- return value === 'b';
- };
+ 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(e1.pipe(single(predicate))).toBe(expected);
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(result).toBe(expected, undefined, new EmptyError());
+ expectSubscriptions(source.subscriptions).toBe(subs);
+ });
});
- it('should raise error from predicate if observable have multiple matching element', () => {
- const e1 = hot('--a--b--a--b--b--|');
- const e1subs = '^ ! ';
- const expected = '-----------# ';
+ 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));
- const predicate = function (value: string) {
- return value === 'b';
- };
+ 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(e1.pipe(single(predicate))).toBe(expected, null, 'Sequence contains more than one element');
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(result).toBe(expected, undefined, new EmptyError());
+ expectSubscriptions(source.subscriptions).toBe(subs);
+ });
});
- it('should raise error from predicate if observable does not emit', () => {
- const e1 = hot('--a--^--|');
- const e1subs = '^ !';
- const expected = '---#';
+ 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);
+ });
+ });
- const predicate = function (value: string) {
- return value === 'a';
- };
+ 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(e1.pipe(single(predicate))).toBe(expected, null, new EmptyError());
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(result).toBe(expected, undefined, new NotFoundError('No matching values'));
+ expectSubscriptions(source.subscriptions).toBe(subs);
+ });
});
- it('should return undefined from predicate if observable does not contain matching element', () => {
- const e1 = hot('--a--b--c--|');
- const e1subs = '^ !';
- const expected = '-----------(z|)';
+ 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);
+ });
+ });
- const predicate = function (value: string) {
- return value === 'x';
- };
+ 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(e1.pipe(single(predicate))).toBe(expected, {z: undefined});
- expectSubscriptions(e1.subscriptions).toBe(e1subs);
+ expectObservable(result).toBe(expected, undefined, new NotFoundError('No matching values'));
+ expectSubscriptions(source.subscriptions).toBe(subs);
+ });
});
- 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);
+ 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..cf6c28cb81 100644
--- a/src/internal/operators/single.ts
+++ b/src/internal/operators/single.ts
@@ -1,40 +1,77 @@
import { Observable } from '../Observable';
-import { Operator } from '../Operator';
import { Subscriber } from '../Subscriber';
import { EmptyError } from '../util/EmptyError';
-import { Observer, MonoTypeOperatorFunction, TeardownLogic } from '../types';
+import { MonoTypeOperatorFunction } from '../types';
+import { SequenceError } from '../util/SequenceError';
+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 +79,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, source: Observable) => 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, source: Observable) => 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++, source);
+ } 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;