Skip to content

Commit

Permalink
feat(first): simplify interface
Browse files Browse the repository at this point in the history
- removes resultSelector argument
- updates tests

BREAKING CHANGE no longer supports `resultSelector` argument. The same functionality can be achieved by simply mapping either before or after `first` depending on your use case.
  • Loading branch information
benlesh committed Mar 2, 2018
1 parent 42589d0 commit a011338
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 136 deletions.
44 changes: 2 additions & 42 deletions spec/operators/first-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Observable.prototype.first', () => {
const expected = '-----(a|)';
const sub = '^ !';

expectObservable(e1.first(null, null, 'a')).toBe(expected);
expectObservable(e1.first(null, 'a')).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

Expand Down Expand Up @@ -145,7 +145,7 @@ describe('Observable.prototype.first', () => {
return value === 's';
};

expectObservable(e1.first(predicate, null, 'd')).toBe(expected);
expectObservable(e1.first(predicate, 'd')).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

Expand Down Expand Up @@ -189,34 +189,6 @@ describe('Observable.prototype.first', () => {
expectSubscriptions(e1.subscriptions).toBe(sub);
});

it('should support a result selector argument', () => {
const e1 = hot('--a--^---b---c---d---e--|');
const expected = '--------(x|)';
const sub = '^ !';
const predicate = function (x) { return x === 'c'; };
const resultSelector = function (x, i) {
expect(i).to.equal(1);
expect(x).to.equal('c');
return 'x';
};

expectObservable(e1.first(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

it('should raise error when result selector throws', () => {
const e1 = hot('--a--^---b---c---d---e--|');
const expected = '--------#';
const sub = '^ !';
const predicate = function (x) { return x === 'c'; };
const resultSelector = function (x, i) {
throw 'error';
};

expectObservable(e1.first(predicate, resultSelector)).toBe(expected);
expectSubscriptions(e1.subscriptions).toBe(sub);
});

it('should support type guards without breaking previous behavior', () => {
// tslint:disable no-unused-variable

Expand Down Expand Up @@ -267,22 +239,10 @@ describe('Observable.prototype.first', () => {
// After the type guard `first` predicates, the type is narrowed to string
xs.first(isString)
.subscribe(s => s.length); // s is string
xs.first(isString, s => s.substr(0)) // s is string in predicate
.subscribe(s => s.length); // s is string

// boolean predicates preserve the type
xs.first(x => typeof x === 'string')
.subscribe(x => x); // x is still string | number
xs.first(x => !!x, x => x)
.subscribe(x => x); // x is still string | number
xs.first(x => typeof x === 'string', x => x, '') // default is string; x remains string | number
.subscribe(x => x); // x is still string | number

// `first` still uses the `resultSelector` return type, if it exists.
xs.first(x => typeof x === 'string', x => ({ str: `${x}` })) // x remains string | number
.subscribe(o => o.str); // o is { str: string }
xs.first(x => typeof x === 'string', x => ({ str: `${x}` }), { str: '' })
.subscribe(o => o.str); // o is { str: string }
}

// tslint:disable enable
Expand Down
78 changes: 18 additions & 60 deletions src/internal/operators/first.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
import { Observable } from '../Observable';
import { Operator } from '../Operator';
import { Subscriber } from '../Subscriber';
import { EmptyError } from '../util/EmptyError';
import { OperatorFunction, MonoTypeOperatorFunction } from '../types';
/* tslint:disable:max-line-length */
export function first<T, S extends T>(predicate: (value: T, index: number, source: Observable<T>) => value is S): OperatorFunction<T, S>;
export function first<T, S extends T, R>(predicate: (value: T | S, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => R, defaultValue?: R): OperatorFunction<T, R>;
export function first<T, S extends T>(predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: void,
defaultValue?: S): OperatorFunction<T, S>;
export function first<T>(predicate?: (value: T, index: number, source: Observable<T>) => boolean): MonoTypeOperatorFunction<T>;
export function first<T, R>(predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: (value: T, index: number) => R,
defaultValue?: R): OperatorFunction<T, R>;
export function first<T>(predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector: void,
defaultValue?: T): MonoTypeOperatorFunction<T>;
import { EmptyError } from '..//util/EmptyError';
import { MonoTypeOperatorFunction } from '../../internal/types';

/**
* Emits only the first value (or the first value that meets some condition)
Expand Down Expand Up @@ -54,34 +40,26 @@ export function first<T>(predicate: (value: T, index: number, source: Observable
*
* @param {function(value: T, index: number, source: Observable<T>): boolean} [predicate]
* An optional function called with each item to test for condition matching.
* @param {function(value: T, index: number): R} [resultSelector] A function to
* produce the value on the output Observable based on the values
* and the indices of the source Observable. The arguments passed to this
* function are:
* - `value`: the value that was emitted on the source.
* - `index`: the "index" of the value from the source.
* @param {R} [defaultValue] The default value emitted in case no valid value
* was found on the source.
* @return {Observable<T|R>} An Observable of the first item that matches the
* condition.
* @method first
* @owner Observable
*/
export function first<T, R>(predicate?: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: ((value: T, index: number) => R) | void,
defaultValue?: R): OperatorFunction<T, T | R> {
return (source: Observable<T>) => source.lift(new FirstOperator(predicate, resultSelector, defaultValue, source));
}
export function first<T>(predicate?: (value: T, index: number, source: Observable<T>) => boolean,
defaultValue?: T): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => source.lift(new FirstOperator(predicate, defaultValue, source));
}

class FirstOperator<T, R> implements Operator<T, R> {
class FirstOperator<T> implements Operator<T, T> {
constructor(private predicate?: (value: T, index: number, source: Observable<T>) => boolean,
private resultSelector?: ((value: T, index: number) => R) | void,
private defaultValue?: any,
private source?: Observable<T>) {
}

call(observer: Subscriber<R>, source: any): any {
return source.subscribe(new FirstSubscriber(observer, this.predicate, this.resultSelector, this.defaultValue, this.source));
call(observer: Subscriber<T>, source: any): any {
return source.subscribe(new FirstSubscriber(observer, this.predicate, this.defaultValue, this.source));
}
}

Expand All @@ -90,14 +68,13 @@ class FirstOperator<T, R> implements Operator<T, R> {
* @ignore
* @extends {Ignored}
*/
class FirstSubscriber<T, R> extends Subscriber<T> {
private index: number = 0;
private hasCompleted: boolean = false;
private _emitted: boolean = false;
class FirstSubscriber<T> extends Subscriber<T> {
private index = 0;
private hasCompleted = false;
private _emitted = false;

constructor(destination: Subscriber<R>,
constructor(destination: Subscriber<T>,
private predicate?: (value: T, index: number, source: Observable<T>) => boolean,
private resultSelector?: ((value: T, index: number) => R) | void,
private defaultValue?: any,
private source?: Observable<T>) {
super(destination);
Expand All @@ -108,7 +85,7 @@ class FirstSubscriber<T, R> extends Subscriber<T> {
if (this.predicate) {
this._tryPredicate(value, index);
} else {
this._emit(value, index);
this._emit(value);
}
}

Expand All @@ -121,30 +98,11 @@ class FirstSubscriber<T, R> extends Subscriber<T> {
return;
}
if (result) {
this._emit(value, index);
}
}

private _emit(value: any, index: number) {
if (this.resultSelector) {
this._tryResultSelector(value, index);
return;
}
this._emitFinal(value);
}

private _tryResultSelector(value: T, index: number) {
let result: any;
try {
result = (<any>this).resultSelector(value, index);
} catch (err) {
this.destination.error(err);
return;
this._emit(value);
}
this._emitFinal(result);
}

private _emitFinal(value: any) {
private _emit(value: T) {
const destination = this.destination;
if (!this._emitted) {
this._emitted = true;
Expand All @@ -160,7 +118,7 @@ class FirstSubscriber<T, R> extends Subscriber<T> {
destination.next(this.defaultValue);
destination.complete();
} else if (!this.hasCompleted) {
destination.error(new EmptyError);
destination.error(new EmptyError());
}
}
}
40 changes: 6 additions & 34 deletions src/internal/patching/operator/first.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,6 @@
import { Observable } from '../../Observable';
import { first as higherOrder } from '../../operators/first';

/* tslint:disable:max-line-length */
export function first<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S): Observable<S>;
export function first<T, S extends T, R>(this: Observable<T>,
predicate: (value: T | S, index: number, source: Observable<T>) => value is S,
resultSelector: (value: S, index: number) => R, defaultValue?: R): Observable<R>;
export function first<T, S extends T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => value is S,
resultSelector: void,
defaultValue?: S): Observable<S>;
export function first<T>(this: Observable<T>,
predicate?: (value: T, index: number, source: Observable<T>) => boolean): Observable<T>;
export function first<T, R>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: (value: T, index: number) => R,
defaultValue?: R): Observable<R>;
export function first<T>(this: Observable<T>,
predicate: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector: void,
defaultValue?: T): Observable<T>;

/**
* Emits only the first value (or the first value that meets some condition)
* emitted by the source Observable.
Expand Down Expand Up @@ -58,21 +37,14 @@ export function first<T>(this: Observable<T>,
*
* @param {function(value: T, index: number, source: Observable<T>): boolean} [predicate]
* An optional function called with each item to test for condition matching.
* @param {function(value: T, index: number): R} [resultSelector] A function to
* produce the value on the output Observable based on the values
* and the indices of the source Observable. The arguments passed to this
* function are:
* - `value`: the value that was emitted on the source.
* - `index`: the "index" of the value from the source.
* @param {R} [defaultValue] The default value emitted in case no valid value
* @param {T} [defaultValue] The default value emitted in case no valid value
* was found on the source.
* @return {Observable<T|R>} An Observable of the first item that matches the
* @return {Observable<T>} An Observable of the first item that matches the
* condition.
* @method first
* @owner Observable
*/
export function first<T, R>(this: Observable<T>, predicate?: (value: T, index: number, source: Observable<T>) => boolean,
resultSelector?: ((value: T, index: number) => R) | void,
defaultValue?: R): Observable<T | R> {
return higherOrder(predicate, resultSelector as any, defaultValue)(this);
}
export function first<T>(this: Observable<T>, predicate?: (value: T, index: number, source: Observable<T>) => boolean,
defaultValue?: T): Observable<T> {
return higherOrder(predicate, defaultValue)(this);
}

0 comments on commit a011338

Please sign in to comment.