diff --git a/spec/Observable-spec.ts b/spec/Observable-spec.ts index 9ce73c4f63..41a8dcddbc 100644 --- a/spec/Observable-spec.ts +++ b/spec/Observable-spec.ts @@ -3,6 +3,7 @@ import * as sinon from 'sinon'; import * as Rx from '../dist/cjs/Rx'; import {TeardownLogic} from '../dist/cjs/Subscription'; import marbleTestingSignature = require('./helpers/marble-testing'); // tslint:disable-line:no-require-imports +import { map } from '../dist/cjs/operators'; declare const { asDiagram, rxTestScheduler }; declare const cold: typeof marbleTestingSignature.cold; @@ -621,6 +622,34 @@ describe('Observable', () => { }); }); }); + + describe('pipe', () => { + it('should exist', () => { + const source = Observable.of('test'); + expect(source.pipe).to.be.a('function'); + }); + + it('should pipe multiple operations', (done) => { + Observable.of('test') + .pipe( + map((x: string) => x + x), + map((x: string) => x + '!!!') + ) + .subscribe( + x => { + expect(x).to.equal('testtest!!!'); + }, + null, + done + ); + }); + + it('should return the same observable if there are no arguments', () => { + const source = Observable.of('test'); + const result = source.pipe(); + expect(result).to.equal(source); + }); + }); }); /** @test {Observable} */ diff --git a/spec/operators/groupBy-spec.ts b/spec/operators/groupBy-spec.ts index 6e4adf2771..6f1eb8bdae 100644 --- a/spec/operators/groupBy-spec.ts +++ b/spec/operators/groupBy-spec.ts @@ -1,6 +1,6 @@ import {expect} from 'chai'; import * as Rx from '../../dist/cjs/Rx'; -import {GroupedObservable} from '../../dist/cjs/operator/groupBy'; +import {GroupedObservable} from '../../dist/cjs/operators/groupBy'; import marbleTestingSignature = require('../helpers/marble-testing'); // tslint:disable-line:no-require-imports declare const { asDiagram }; diff --git a/spec/util/pipe-spec.ts b/spec/util/pipe-spec.ts new file mode 100644 index 0000000000..63288c7fc5 --- /dev/null +++ b/spec/util/pipe-spec.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { pipe } from '../../dist/cjs/util/pipe'; + +describe('pipe', () => { + it('should exist', () => { + expect(pipe).to.be.a('function'); + }); + + it('should pipe two functions together', () => { + const a = x => x + x; + const b = x => x - 1; + + const c = pipe(a, b); + expect(c).to.be.a('function'); + expect(c(1)).to.equal(1); + expect(c(10)).to.equal(19); + }); + + it('should return the same function if only one is passed', () => { + const a = x => x; + const c = pipe(a); + + expect(c).to.equal(a); + }); + + it('should return a noop if not passed a function', () => { + const c = pipe(); + + expect(c('whatever')).to.equal('whatever'); + const someObj = {}; + expect(c(someObj)).to.equal(someObj); + }); +}); diff --git a/src/Observable.ts b/src/Observable.ts index 586c1dcca9..41ab5faf5c 100644 --- a/src/Observable.ts +++ b/src/Observable.ts @@ -7,6 +7,8 @@ import { toSubscriber } from './util/toSubscriber'; import { IfObservable } from './observable/IfObservable'; import { ErrorObservable } from './observable/ErrorObservable'; import { observable as Symbol_observable } from './symbol/observable'; +import { OperatorFunction } from './interfaces'; +import { pipeFromArray } from './util/pipe'; export interface Subscribable { subscribe(observerOrNext?: PartialObserver | ((value: T) => void), @@ -286,4 +288,43 @@ export class Observable implements Subscribable { [Symbol_observable]() { return this; } + + /* tslint:disable:max-line-length */ + pipe(): Observable + pipe(op1: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction, op8: OperatorFunction): Observable + pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction, op8: OperatorFunction, op9: OperatorFunction): Observable + /* tslint:enable:max-line-length */ + + /** + * Used to stitch together functional operators into a chain. + * @method pipe + * @return {Observable} the Observable result of all of the operators having + * been called in the order they were passed in. + * + * @example + * + * import { map, filter, scan } from 'rxjs/operators'; + * + * Rx.Observable.interval(1000) + * .pipe( + * filter(x => x % 2 === 0), + * map(x => x + x), + * scan((acc, x) => acc + x) + * ) + * .subscribe(x => console.log(x)) + */ + pipe(...operations: OperatorFunction[]): Observable { + if (operations.length === 0) { + return this as any; + } + + return pipeFromArray(operations)(this); + } } diff --git a/src/ReplaySubject.ts b/src/ReplaySubject.ts index 4161a6cfb7..862633b392 100644 --- a/src/ReplaySubject.ts +++ b/src/ReplaySubject.ts @@ -3,7 +3,7 @@ import { IScheduler } from './Scheduler'; import { queue } from './scheduler/queue'; import { Subscriber } from './Subscriber'; import { Subscription } from './Subscription'; -import { ObserveOnSubscriber } from './operator/observeOn'; +import { ObserveOnSubscriber } from './operators/observeOn'; import { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError'; import { SubjectSubscription } from './SubjectSubscription'; /** diff --git a/src/Rx.ts b/src/Rx.ts index ab87adb145..5b486675d1 100644 --- a/src/Rx.ts +++ b/src/Rx.ts @@ -158,10 +158,11 @@ export {ObjectUnsubscribedError} from './util/ObjectUnsubscribedError'; export {TimeoutError} from './util/TimeoutError'; export {UnsubscriptionError} from './util/UnsubscriptionError'; export {TimeInterval} from './operator/timeInterval'; -export {Timestamp} from './operator/timestamp'; +export {Timestamp} from './operators/timestamp'; export {TestScheduler} from './testing/TestScheduler'; export {VirtualTimeScheduler} from './scheduler/VirtualTimeScheduler'; export {AjaxRequest, AjaxResponse, AjaxError, AjaxTimeoutError} from './observable/dom/AjaxObservable'; +export { pipe } from './util/pipe'; import { asap } from './scheduler/asap'; import { async } from './scheduler/async'; diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000000..5d825f231d --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,9 @@ +import { Observable } from './Observable'; + +export type UnaryFunction = (source: T) => R; + +export type OperatorFunction = UnaryFunction, Observable>; + +export type FactoryOrValue = T | (() => T); + +export type MonoTypeOperatorFunction = OperatorFunction; diff --git a/src/observable/ConnectableObservable.ts b/src/observable/ConnectableObservable.ts index a53eb1e319..ee570f2dcc 100644 --- a/src/observable/ConnectableObservable.ts +++ b/src/observable/ConnectableObservable.ts @@ -3,6 +3,7 @@ import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Subscription, TeardownLogic } from '../Subscription'; +import { refCount as higherOrderRefCount } from '../operators/refCount'; /** * @class ConnectableObservable @@ -49,7 +50,7 @@ export class ConnectableObservable extends Observable { } refCount(): Observable { - return this.lift(new RefCountOperator(this)); + return higherOrderRefCount()(this); } } diff --git a/src/observable/FromObservable.ts b/src/observable/FromObservable.ts index 5585d20ce5..fb8e5a6806 100644 --- a/src/observable/FromObservable.ts +++ b/src/observable/FromObservable.ts @@ -10,7 +10,7 @@ import { IScheduler } from '../Scheduler'; import { iterator as Symbol_iterator } from '../symbol/iterator'; import { Observable, ObservableInput } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { ObserveOnSubscriber } from '../operator/observeOn'; +import { ObserveOnSubscriber } from '../operators/observeOn'; import { observable as Symbol_observable } from '../symbol/observable'; /** diff --git a/src/observable/concat.ts b/src/observable/concat.ts index 804f719c41..556a285da5 100644 --- a/src/observable/concat.ts +++ b/src/observable/concat.ts @@ -1,3 +1,116 @@ -import { concatStatic } from '../operator/concat'; +import { Observable, ObservableInput } from '../Observable'; +import { IScheduler } from '../Scheduler'; +import { isScheduler } from '../util/isScheduler'; +import { of } from './of'; +import { from } from './from'; +import { concatAll } from '../operators/concatAll'; -export const concat = concatStatic; \ No newline at end of file +/* tslint:disable:max-line-length */ +export function concat(v1: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(v1: ObservableInput, v2: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler?: IScheduler): Observable; +export function concat(...observables: (ObservableInput | IScheduler)[]): Observable; +export function concat(...observables: (ObservableInput | IScheduler)[]): Observable; +/* tslint:enable:max-line-length */ +/** + * Creates an output Observable which sequentially emits all values from given + * Observable and then moves on to the next. + * + * Concatenates multiple Observables together by + * sequentially emitting their values, one Observable after the other. + * + * + * + * `concat` joins multiple Observables together, by subscribing to them one at a time and + * merging their results into the output Observable. You can pass either an array of + * Observables, or put them directly as arguments. Passing an empty array will result + * in Observable that completes immediately. + * + * `concat` will subscribe to first input Observable and emit all its values, without + * changing or affecting them in any way. When that Observable completes, it will + * subscribe to then next Observable passed and, again, emit its values. This will be + * repeated, until the operator runs out of Observables. When last input Observable completes, + * `concat` will complete as well. At any given moment only one Observable passed to operator + * emits values. If you would like to emit values from passed Observables concurrently, check out + * {@link merge} instead, especially with optional `concurrent` parameter. As a matter of fact, + * `concat` is an equivalent of `merge` operator with `concurrent` parameter set to `1`. + * + * Note that if some input Observable never completes, `concat` will also never complete + * and Observables following the one that did not complete will never be subscribed. On the other + * hand, if some Observable simply completes immediately after it is subscribed, it will be + * invisible for `concat`, which will just move on to the next Observable. + * + * If any Observable in chain errors, instead of passing control to the next Observable, + * `concat` will error immediately as well. Observables that would be subscribed after + * the one that emitted error, never will. + * + * If you pass to `concat` the same Observable many times, its stream of values + * will be "replayed" on every subscription, which means you can repeat given Observable + * as many times as you like. If passing the same Observable to `concat` 1000 times becomes tedious, + * you can always use {@link repeat}. + * + * @example Concatenate a timer counting from 0 to 3 with a synchronous sequence from 1 to 10 + * var timer = Rx.Observable.interval(1000).take(4); + * var sequence = Rx.Observable.range(1, 10); + * var result = Rx.Observable.concat(timer, sequence); + * result.subscribe(x => console.log(x)); + * + * // results in: + * // 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 -immediate-> 1 ... 10 + * + * + * @example Concatenate an array of 3 Observables + * var timer1 = Rx.Observable.interval(1000).take(10); + * var timer2 = Rx.Observable.interval(2000).take(6); + * var timer3 = Rx.Observable.interval(500).take(10); + * var result = Rx.Observable.concat([timer1, timer2, timer3]); // note that array is passed + * result.subscribe(x => console.log(x)); + * + * // results in the following: + * // (Prints to console sequentially) + * // -1000ms-> 0 -1000ms-> 1 -1000ms-> ... 9 + * // -2000ms-> 0 -2000ms-> 1 -2000ms-> ... 5 + * // -500ms-> 0 -500ms-> 1 -500ms-> ... 9 + * + * + * @example Concatenate the same Observable to repeat it + * const timer = Rx.Observable.interval(1000).take(2); + * + * Rx.Observable.concat(timer, timer) // concating the same Observable! + * .subscribe( + * value => console.log(value), + * err => {}, + * () => console.log('...and it is done!') + * ); + * + * // Logs: + * // 0 after 1s + * // 1 after 2s + * // 0 after 3s + * // 1 after 4s + * // "...and it is done!" also after 4s + * + * @see {@link concatAll} + * @see {@link concatMap} + * @see {@link concatMapTo} + * + * @param {ObservableInput} input1 An input Observable to concatenate with others. + * @param {ObservableInput} input2 An input Observable to concatenate with others. + * More than one input Observables may be given as argument. + * @param {Scheduler} [scheduler=null] An optional IScheduler to schedule each + * Observable subscription on. + * @return {Observable} All values of each passed Observable merged into a + * single Observable, in order, in serial fashion. + * @static true + * @name concat + * @owner Observable + */ +export function concat(...observables: Array | IScheduler>): Observable { + if (observables.length === 1 || (observables.length === 2 && isScheduler(observables[1]))) { + return from(observables[0]); + } + return concatAll()(of(...observables)); +} diff --git a/src/observable/dom/AjaxObservable.ts b/src/observable/dom/AjaxObservable.ts index ec6b60bb3b..89647acbc9 100644 --- a/src/observable/dom/AjaxObservable.ts +++ b/src/observable/dom/AjaxObservable.ts @@ -4,7 +4,7 @@ import { errorObject } from '../../util/errorObject'; import { Observable } from '../../Observable'; import { Subscriber } from '../../Subscriber'; import { TeardownLogic } from '../../Subscription'; -import { MapOperator } from '../../operator/map'; +import { map } from '../../operators'; export interface AjaxRequest { url?: string; @@ -87,9 +87,17 @@ export function ajaxPatch(url: string, body?: any, headers?: Object): Observable return new AjaxObservable({ method: 'PATCH', url, body, headers }); }; +const mapResponse = map((x: AjaxResponse, index: number) => x.response); + export function ajaxGetJSON(url: string, headers?: Object): Observable { - return new AjaxObservable({ method: 'GET', url, responseType: 'json', headers }) - .lift(new MapOperator((x: AjaxResponse, index: number): T => x.response, null)); + return mapResponse( + new AjaxObservable({ + method: 'GET', + url, + responseType: 'json', + headers + }) + ); }; /** diff --git a/src/observable/race.ts b/src/observable/race.ts index 15541d03db..ef1b2feb00 100644 --- a/src/observable/race.ts +++ b/src/observable/race.ts @@ -1,3 +1,100 @@ -import { raceStatic } from '../operator/race'; +import { Observable } from '../Observable'; +import { isArray } from '../util/isArray'; +import { ArrayObservable } from '../observable/ArrayObservable'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; -export const race = raceStatic; +/** + * Returns an Observable that mirrors the first source Observable to emit an item. + * @param {...Observables} ...observables sources used to race for which Observable emits first. + * @return {Observable} an Observable that mirrors the output of the first Observable to emit an item. + * @static true + * @name race + * @owner Observable + */ +export function race(observables: Array>): Observable; +export function race(observables: Array>): Observable; +export function race(...observables: Array | Array>>): Observable; +export function race(...observables: Array | Array>>): Observable { + // if the only argument is an array, it was most likely called with + // `race([obs1, obs2, ...])` + if (observables.length === 1) { + if (isArray(observables[0])) { + observables = >>observables[0]; + } else { + return >observables[0]; + } + } + + return new ArrayObservable(observables).lift(new RaceOperator()); +} + +export class RaceOperator implements Operator { + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new RaceSubscriber(subscriber)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class RaceSubscriber extends OuterSubscriber { + private hasFirst: boolean = false; + private observables: Observable[] = []; + private subscriptions: Subscription[] = []; + + constructor(destination: Subscriber) { + super(destination); + } + + protected _next(observable: any): void { + this.observables.push(observable); + } + + protected _complete() { + const observables = this.observables; + const len = observables.length; + + if (len === 0) { + this.destination.complete(); + } else { + for (let i = 0; i < len && !this.hasFirst; i++) { + let observable = observables[i]; + let subscription = subscribeToResult(this, observable, observable, i); + + if (this.subscriptions) { + this.subscriptions.push(subscription); + } + this.add(subscription); + } + this.observables = null; + } + } + + notifyNext(outerValue: T, innerValue: T, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + if (!this.hasFirst) { + this.hasFirst = true; + + for (let i = 0; i < this.subscriptions.length; i++) { + if (i !== outerIndex) { + let subscription = this.subscriptions[i]; + + subscription.unsubscribe(); + this.remove(subscription); + } + } + + this.subscriptions = null; + } + + this.destination.next(innerValue); + } +} diff --git a/src/observable/zip.ts b/src/observable/zip.ts index 98ece64e87..b4615abe5f 100644 --- a/src/observable/zip.ts +++ b/src/observable/zip.ts @@ -1,3 +1,3 @@ -import { zipStatic } from '../operator/zip'; +import { zipStatic } from '../operators/zip'; -export const zip = zipStatic; \ No newline at end of file +export const zip = zipStatic; diff --git a/src/operator/audit.ts b/src/operator/audit.ts index c30cd58e0a..134b3773ef 100644 --- a/src/operator/audit.ts +++ b/src/operator/audit.ts @@ -1,12 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable, SubscribableOrPromise } from '../Observable'; -import { Subscription, TeardownLogic } from '../Subscription'; -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { audit as higherOrder } from '../operators'; /** * Ignores source values for a duration determined by another Observable, then @@ -49,71 +43,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function audit(this: Observable, durationSelector: (value: T) => SubscribableOrPromise): Observable { - return this.lift(new AuditOperator(durationSelector)); -} - -class AuditOperator implements Operator { - constructor(private durationSelector: (value: T) => SubscribableOrPromise) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new AuditSubscriber(subscriber, this.durationSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class AuditSubscriber extends OuterSubscriber { - - private value: T; - private hasValue: boolean = false; - private throttled: Subscription; - - constructor(destination: Subscriber, - private durationSelector: (value: T) => SubscribableOrPromise) { - super(destination); - } - - protected _next(value: T): void { - this.value = value; - this.hasValue = true; - if (!this.throttled) { - const duration = tryCatch(this.durationSelector)(value); - if (duration === errorObject) { - this.destination.error(errorObject.e); - } else { - const innerSubscription = subscribeToResult(this, duration); - if (innerSubscription.closed) { - this.clearThrottle(); - } else { - this.add(this.throttled = innerSubscription); - } - } - } - } - - clearThrottle() { - const { value, hasValue, throttled } = this; - if (throttled) { - this.remove(throttled); - this.throttled = null; - throttled.unsubscribe(); - } - if (hasValue) { - this.value = null; - this.hasValue = false; - this.destination.next(value); - } - } - - notifyNext(outerValue: T, innerValue: R, outerIndex: number, innerIndex: number): void { - this.clearThrottle(); - } - - notifyComplete(): void { - this.clearThrottle(); - } + return higherOrder(durationSelector)(this); } diff --git a/src/operator/auditTime.ts b/src/operator/auditTime.ts index dfcf78f93d..370b0c023b 100644 --- a/src/operator/auditTime.ts +++ b/src/operator/auditTime.ts @@ -1,9 +1,7 @@ import { async } from '../scheduler/async'; -import { Operator } from '../Operator'; import { IScheduler } from '../Scheduler'; -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription, TeardownLogic } from '../Subscription'; +import { auditTime as higherOrder } from '../operators'; /** * Ignores source values for `duration` milliseconds, then emits the most recent @@ -48,59 +46,5 @@ import { Subscription, TeardownLogic } from '../Subscription'; * @owner Observable */ export function auditTime(this: Observable, duration: number, scheduler: IScheduler = async): Observable { - return this.lift(new AuditTimeOperator(duration, scheduler)); -} - -class AuditTimeOperator implements Operator { - constructor(private duration: number, - private scheduler: IScheduler) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new AuditTimeSubscriber(subscriber, this.duration, this.scheduler)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class AuditTimeSubscriber extends Subscriber { - - private value: T; - private hasValue: boolean = false; - private throttled: Subscription; - - constructor(destination: Subscriber, - private duration: number, - private scheduler: IScheduler) { - super(destination); - } - - protected _next(value: T): void { - this.value = value; - this.hasValue = true; - if (!this.throttled) { - this.add(this.throttled = this.scheduler.schedule(dispatchNext, this.duration, this)); - } - } - - clearThrottle(): void { - const { value, hasValue, throttled } = this; - if (throttled) { - this.remove(throttled); - this.throttled = null; - throttled.unsubscribe(); - } - if (hasValue) { - this.value = null; - this.hasValue = false; - this.destination.next(value); - } - } -} - -function dispatchNext(subscriber: AuditTimeSubscriber): void { - subscriber.clearThrottle(); -} + return higherOrder(duration, scheduler)(this); +} \ No newline at end of file diff --git a/src/operator/buffer.ts b/src/operator/buffer.ts index ca43bec9f2..42e6da93e5 100644 --- a/src/operator/buffer.ts +++ b/src/operator/buffer.ts @@ -1,10 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { buffer as higherOrder } from '../operators'; /** * Buffers the source Observable values until `closingNotifier` emits. @@ -39,41 +35,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function buffer(this: Observable, closingNotifier: Observable): Observable { - return this.lift(new BufferOperator(closingNotifier)); -} - -class BufferOperator implements Operator { - - constructor(private closingNotifier: Observable) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new BufferSubscriber(subscriber, this.closingNotifier)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferSubscriber extends OuterSubscriber { - private buffer: T[] = []; - - constructor(destination: Subscriber, closingNotifier: Observable) { - super(destination); - this.add(subscribeToResult(this, closingNotifier)); - } - - protected _next(value: T) { - this.buffer.push(value); - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - const buffer = this.buffer; - this.buffer = []; - this.destination.next(buffer); - } + return higherOrder(closingNotifier)(this); } diff --git a/src/operator/bufferCount.ts b/src/operator/bufferCount.ts index f2a79089d5..509f3cb9dd 100644 --- a/src/operator/bufferCount.ts +++ b/src/operator/bufferCount.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; + import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; +import { bufferCount as higherOrder } from '../operators'; /** * Buffers the source Observable values until the size hits the maximum @@ -45,98 +44,5 @@ import { TeardownLogic } from '../Subscription'; * @owner Observable */ export function bufferCount(this: Observable, bufferSize: number, startBufferEvery: number = null): Observable { - return this.lift(new BufferCountOperator(bufferSize, startBufferEvery)); -} - -class BufferCountOperator implements Operator { - private subscriberClass: any; - - constructor(private bufferSize: number, private startBufferEvery: number) { - if (!startBufferEvery || bufferSize === startBufferEvery) { - this.subscriberClass = BufferCountSubscriber; - } else { - this.subscriberClass = BufferSkipCountSubscriber; - } - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new this.subscriberClass(subscriber, this.bufferSize, this.startBufferEvery)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferCountSubscriber extends Subscriber { - private buffer: T[] = []; - - constructor(destination: Subscriber, private bufferSize: number) { - super(destination); - } - - protected _next(value: T): void { - const buffer = this.buffer; - - buffer.push(value); - - if (buffer.length == this.bufferSize) { - this.destination.next(buffer); - this.buffer = []; - } - } - - protected _complete(): void { - const buffer = this.buffer; - if (buffer.length > 0) { - this.destination.next(buffer); - } - super._complete(); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferSkipCountSubscriber extends Subscriber { - private buffers: Array = []; - private count: number = 0; - - constructor(destination: Subscriber, private bufferSize: number, private startBufferEvery: number) { - super(destination); - } - - protected _next(value: T): void { - const { bufferSize, startBufferEvery, buffers, count } = this; - - this.count++; - if (count % startBufferEvery === 0) { - buffers.push([]); - } - - for (let i = buffers.length; i--; ) { - const buffer = buffers[i]; - buffer.push(value); - if (buffer.length === bufferSize) { - buffers.splice(i, 1); - this.destination.next(buffer); - } - } - } - - protected _complete(): void { - const { buffers, destination } = this; - - while (buffers.length > 0) { - let buffer = buffers.shift(); - if (buffer.length > 0) { - destination.next(buffer); - } - } - super._complete(); - } - + return higherOrder(bufferSize, startBufferEvery)(this); } diff --git a/src/operator/bufferTime.ts b/src/operator/bufferTime.ts index d15b8fc674..602b95a1f5 100644 --- a/src/operator/bufferTime.ts +++ b/src/operator/bufferTime.ts @@ -1,11 +1,8 @@ import { IScheduler } from '../Scheduler'; -import { Action } from '../scheduler/Action'; -import { Operator } from '../Operator'; import { async } from '../scheduler/async'; import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; import { isScheduler } from '../util/isScheduler'; +import { bufferTime as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function bufferTime(this: Observable, bufferTimeSpan: number, scheduler?: IScheduler): Observable; @@ -75,160 +72,5 @@ export function bufferTime(this: Observable, bufferTimeSpan: number): Obse maxBufferSize = arguments[2]; } - return this.lift(new BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler)); -} - -class BufferTimeOperator implements Operator { - constructor(private bufferTimeSpan: number, - private bufferCreationInterval: number, - private maxBufferSize: number, - private scheduler: IScheduler) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new BufferTimeSubscriber( - subscriber, this.bufferTimeSpan, this.bufferCreationInterval, this.maxBufferSize, this.scheduler - )); - } -} - -class Context { - buffer: T[] = []; - closeAction: Subscription; -} - -type CreationState = { - bufferTimeSpan: number; - bufferCreationInterval: number, - subscriber: BufferTimeSubscriber; - scheduler: IScheduler; -}; - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferTimeSubscriber extends Subscriber { - private contexts: Array> = []; - private timespanOnly: boolean; - - constructor(destination: Subscriber, - private bufferTimeSpan: number, - private bufferCreationInterval: number, - private maxBufferSize: number, - private scheduler: IScheduler) { - super(destination); - const context = this.openContext(); - this.timespanOnly = bufferCreationInterval == null || bufferCreationInterval < 0; - if (this.timespanOnly) { - const timeSpanOnlyState = { subscriber: this, context, bufferTimeSpan }; - this.add(context.closeAction = scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); - } else { - const closeState = { subscriber: this, context }; - const creationState: CreationState = { bufferTimeSpan, bufferCreationInterval, subscriber: this, scheduler }; - this.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, closeState)); - this.add(scheduler.schedule(dispatchBufferCreation, bufferCreationInterval, creationState)); - } - } - - protected _next(value: T) { - const contexts = this.contexts; - const len = contexts.length; - let filledBufferContext: Context; - for (let i = 0; i < len; i++) { - const context = contexts[i]; - const buffer = context.buffer; - buffer.push(value); - if (buffer.length == this.maxBufferSize) { - filledBufferContext = context; - } - } - - if (filledBufferContext) { - this.onBufferFull(filledBufferContext); - } - } - - protected _error(err: any) { - this.contexts.length = 0; - super._error(err); - } - - protected _complete() { - const { contexts, destination } = this; - while (contexts.length > 0) { - const context = contexts.shift(); - destination.next(context.buffer); - } - super._complete(); - } - - protected _unsubscribe() { - this.contexts = null; - } - - protected onBufferFull(context: Context) { - this.closeContext(context); - const closeAction = context.closeAction; - closeAction.unsubscribe(); - this.remove(closeAction); - - if (!this.closed && this.timespanOnly) { - context = this.openContext(); - const bufferTimeSpan = this.bufferTimeSpan; - const timeSpanOnlyState = { subscriber: this, context, bufferTimeSpan }; - this.add(context.closeAction = this.scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); - } - } - - openContext(): Context { - const context: Context = new Context(); - this.contexts.push(context); - return context; - } - - closeContext(context: Context) { - this.destination.next(context.buffer); - const contexts = this.contexts; - - const spliceIndex = contexts ? contexts.indexOf(context) : -1; - if (spliceIndex >= 0) { - contexts.splice(contexts.indexOf(context), 1); - } - } -} - -function dispatchBufferTimeSpanOnly(this: Action, state: any) { - const subscriber: BufferTimeSubscriber = state.subscriber; - - const prevContext = state.context; - if (prevContext) { - subscriber.closeContext(prevContext); - } - - if (!subscriber.closed) { - state.context = subscriber.openContext(); - state.context.closeAction = this.schedule(state, state.bufferTimeSpan); - } -} - -interface DispatchArg { - subscriber: BufferTimeSubscriber; - context: Context; -} - -function dispatchBufferCreation(this: Action>, state: CreationState) { - const { bufferCreationInterval, bufferTimeSpan, subscriber, scheduler } = state; - const context = subscriber.openContext(); - const action = >>this; - if (!subscriber.closed) { - subscriber.add(context.closeAction = scheduler.schedule>(dispatchBufferClose, bufferTimeSpan, { subscriber, context })); - action.schedule(state, bufferCreationInterval); - } -} - -function dispatchBufferClose(arg: DispatchArg) { - const { subscriber, context } = arg; - subscriber.closeContext(context); + return higherOrder(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler)(this); } diff --git a/src/operator/bufferToggle.ts b/src/operator/bufferToggle.ts index 1971029a10..e9283d0349 100644 --- a/src/operator/bufferToggle.ts +++ b/src/operator/bufferToggle.ts @@ -1,11 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable, SubscribableOrPromise } from '../Observable'; -import { Subscription } from '../Subscription'; -import { subscribeToResult } from '../util/subscribeToResult'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { bufferToggle as higherOrder } from '../operators'; /** * Buffers the source Observable values starting from an emission from @@ -47,124 +42,5 @@ import { InnerSubscriber } from '../InnerSubscriber'; */ export function bufferToggle(this: Observable, openings: SubscribableOrPromise, closingSelector: (value: O) => SubscribableOrPromise): Observable { - return this.lift(new BufferToggleOperator(openings, closingSelector)); -} - -class BufferToggleOperator implements Operator { - - constructor(private openings: SubscribableOrPromise, - private closingSelector: (value: O) => SubscribableOrPromise) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new BufferToggleSubscriber(subscriber, this.openings, this.closingSelector)); - } -} - -interface BufferContext { - buffer: T[]; - subscription: Subscription; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferToggleSubscriber extends OuterSubscriber { - private contexts: Array> = []; - - constructor(destination: Subscriber, - private openings: SubscribableOrPromise, - private closingSelector: (value: O) => SubscribableOrPromise | void) { - super(destination); - this.add(subscribeToResult(this, openings)); - } - - protected _next(value: T): void { - const contexts = this.contexts; - const len = contexts.length; - for (let i = 0; i < len; i++) { - contexts[i].buffer.push(value); - } - } - - protected _error(err: any): void { - const contexts = this.contexts; - while (contexts.length > 0) { - const context = contexts.shift(); - context.subscription.unsubscribe(); - context.buffer = null; - context.subscription = null; - } - this.contexts = null; - super._error(err); - } - - protected _complete(): void { - const contexts = this.contexts; - while (contexts.length > 0) { - const context = contexts.shift(); - this.destination.next(context.buffer); - context.subscription.unsubscribe(); - context.buffer = null; - context.subscription = null; - } - this.contexts = null; - super._complete(); - } - - notifyNext(outerValue: any, innerValue: O, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - outerValue ? this.closeBuffer(outerValue) : this.openBuffer(innerValue); - } - - notifyComplete(innerSub: InnerSubscriber): void { - this.closeBuffer(( innerSub).context); - } - - private openBuffer(value: O): void { - try { - const closingSelector = this.closingSelector; - const closingNotifier = closingSelector.call(this, value); - if (closingNotifier) { - this.trySubscribe(closingNotifier); - } - } catch (err) { - this._error(err); - } - } - - private closeBuffer(context: BufferContext): void { - const contexts = this.contexts; - - if (contexts && context) { - const { buffer, subscription } = context; - this.destination.next(buffer); - contexts.splice(contexts.indexOf(context), 1); - this.remove(subscription); - subscription.unsubscribe(); - } - } - - private trySubscribe(closingNotifier: any): void { - const contexts = this.contexts; - - const buffer: Array = []; - const subscription = new Subscription(); - const context = { buffer, subscription }; - contexts.push(context); - - const innerSubscription = subscribeToResult(this, closingNotifier, context); - - if (!innerSubscription || innerSubscription.closed) { - this.closeBuffer(context); - } else { - ( innerSubscription).context = context; - - this.add(innerSubscription); - subscription.add(innerSubscription); - } - } + return higherOrder(openings, closingSelector)(this); } diff --git a/src/operator/bufferWhen.ts b/src/operator/bufferWhen.ts index 0819c3c170..5592457504 100644 --- a/src/operator/bufferWhen.ts +++ b/src/operator/bufferWhen.ts @@ -1,13 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { bufferWhen as higherOrder } from '../operators'; /** * Buffers the source Observable values, using a factory function of closing @@ -43,92 +36,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function bufferWhen(this: Observable, closingSelector: () => Observable): Observable { - return this.lift(new BufferWhenOperator(closingSelector)); -} - -class BufferWhenOperator implements Operator { - - constructor(private closingSelector: () => Observable) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new BufferWhenSubscriber(subscriber, this.closingSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferWhenSubscriber extends OuterSubscriber { - private buffer: T[]; - private subscribing: boolean = false; - private closingSubscription: Subscription; - - constructor(destination: Subscriber, private closingSelector: () => Observable) { - super(destination); - this.openBuffer(); - } - - protected _next(value: T) { - this.buffer.push(value); - } - - protected _complete() { - const buffer = this.buffer; - if (buffer) { - this.destination.next(buffer); - } - super._complete(); - } - - protected _unsubscribe() { - this.buffer = null; - this.subscribing = false; - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.openBuffer(); - } - - notifyComplete(): void { - if (this.subscribing) { - this.complete(); - } else { - this.openBuffer(); - } - } - - openBuffer() { - - let { closingSubscription } = this; - - if (closingSubscription) { - this.remove(closingSubscription); - closingSubscription.unsubscribe(); - } - - const buffer = this.buffer; - if (this.buffer) { - this.destination.next(buffer); - } - - this.buffer = []; - - const closingNotifier = tryCatch(this.closingSelector)(); - - if (closingNotifier === errorObject) { - this.error(errorObject.e); - } else { - closingSubscription = new Subscription(); - this.closingSubscription = closingSubscription; - this.add(closingSubscription); - this.subscribing = true; - closingSubscription.add(subscribeToResult(this, closingNotifier)); - this.subscribing = false; - } - } + return higherOrder(closingSelector)(this); } diff --git a/src/operator/catch.ts b/src/operator/catch.ts index c244f07ebf..e39fc1e2b2 100644 --- a/src/operator/catch.ts +++ b/src/operator/catch.ts @@ -1,9 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable, ObservableInput } from '../Observable'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable, ObservableInput } from '../Observable'; +import { catchError as higherOrder } from '../operators'; /** * Catches errors on the observable to be handled by returning a new observable or throwing an error. @@ -65,50 +62,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function _catch(this: Observable, selector: (err: any, caught: Observable) => ObservableInput): Observable { - const operator = new CatchOperator(selector); - const caught = this.lift(operator); - return (operator.caught = caught); -} - -class CatchOperator implements Operator { - caught: Observable; - - constructor(private selector: (err: any, caught: Observable) => ObservableInput) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new CatchSubscriber(subscriber, this.selector, this.caught)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class CatchSubscriber extends OuterSubscriber { - constructor(destination: Subscriber, - private selector: (err: any, caught: Observable) => ObservableInput, - private caught: Observable) { - super(destination); - } - - // NOTE: overriding `error` instead of `_error` because we don't want - // to have this flag this subscriber as `isStopped`. We can mimic the - // behavior of the RetrySubscriber (from the `retry` operator), where - // we unsubscribe from our source chain, reset our Subscriber flags, - // then subscribe to the selector result. - error(err: any) { - if (!this.isStopped) { - let result: any; - try { - result = this.selector(err, this.caught); - } catch (err2) { - super.error(err2); - return; - } - this._unsubscribeAndRecycle(); - this.add(subscribeToResult(this, result)); - } - } + return higherOrder(selector)(this); } diff --git a/src/operator/concat.ts b/src/operator/concat.ts index f6648e7d38..339cf55610 100644 --- a/src/operator/concat.ts +++ b/src/operator/concat.ts @@ -1,8 +1,6 @@ import { Observable, ObservableInput } from '../Observable'; import { IScheduler } from '../Scheduler'; -import { isScheduler } from '../util/isScheduler'; -import { ArrayObservable } from '../observable/ArrayObservable'; -import { MergeAllOperator } from './mergeAll'; +import { concat as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function concat(this: Observable, scheduler?: IScheduler): Observable; @@ -65,122 +63,5 @@ export function concat(this: Observable, ...observables: Array(this: Observable, ...observables: Array | IScheduler>): Observable { - return this.lift.call(concatStatic(this, ...observables)); -} - -/* tslint:disable:max-line-length */ -export function concatStatic(v1: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(v1: ObservableInput, v2: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler?: IScheduler): Observable; -export function concatStatic(...observables: (ObservableInput | IScheduler)[]): Observable; -export function concatStatic(...observables: (ObservableInput | IScheduler)[]): Observable; -/* tslint:enable:max-line-length */ -/** - * Creates an output Observable which sequentially emits all values from given - * Observable and then moves on to the next. - * - * Concatenates multiple Observables together by - * sequentially emitting their values, one Observable after the other. - * - * - * - * `concat` joins multiple Observables together, by subscribing to them one at a time and - * merging their results into the output Observable. You can pass either an array of - * Observables, or put them directly as arguments. Passing an empty array will result - * in Observable that completes immediately. - * - * `concat` will subscribe to first input Observable and emit all its values, without - * changing or affecting them in any way. When that Observable completes, it will - * subscribe to then next Observable passed and, again, emit its values. This will be - * repeated, until the operator runs out of Observables. When last input Observable completes, - * `concat` will complete as well. At any given moment only one Observable passed to operator - * emits values. If you would like to emit values from passed Observables concurrently, check out - * {@link merge} instead, especially with optional `concurrent` parameter. As a matter of fact, - * `concat` is an equivalent of `merge` operator with `concurrent` parameter set to `1`. - * - * Note that if some input Observable never completes, `concat` will also never complete - * and Observables following the one that did not complete will never be subscribed. On the other - * hand, if some Observable simply completes immediately after it is subscribed, it will be - * invisible for `concat`, which will just move on to the next Observable. - * - * If any Observable in chain errors, instead of passing control to the next Observable, - * `concat` will error immediately as well. Observables that would be subscribed after - * the one that emitted error, never will. - * - * If you pass to `concat` the same Observable many times, its stream of values - * will be "replayed" on every subscription, which means you can repeat given Observable - * as many times as you like. If passing the same Observable to `concat` 1000 times becomes tedious, - * you can always use {@link repeat}. - * - * @example Concatenate a timer counting from 0 to 3 with a synchronous sequence from 1 to 10 - * var timer = Rx.Observable.interval(1000).take(4); - * var sequence = Rx.Observable.range(1, 10); - * var result = Rx.Observable.concat(timer, sequence); - * result.subscribe(x => console.log(x)); - * - * // results in: - * // 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 -immediate-> 1 ... 10 - * - * - * @example Concatenate an array of 3 Observables - * var timer1 = Rx.Observable.interval(1000).take(10); - * var timer2 = Rx.Observable.interval(2000).take(6); - * var timer3 = Rx.Observable.interval(500).take(10); - * var result = Rx.Observable.concat([timer1, timer2, timer3]); // note that array is passed - * result.subscribe(x => console.log(x)); - * - * // results in the following: - * // (Prints to console sequentially) - * // -1000ms-> 0 -1000ms-> 1 -1000ms-> ... 9 - * // -2000ms-> 0 -2000ms-> 1 -2000ms-> ... 5 - * // -500ms-> 0 -500ms-> 1 -500ms-> ... 9 - * - * - * @example Concatenate the same Observable to repeat it - * const timer = Rx.Observable.interval(1000).take(2); - * - * Rx.Observable.concat(timer, timer) // concating the same Observable! - * .subscribe( - * value => console.log(value), - * err => {}, - * () => console.log('...and it is done!') - * ); - * - * // Logs: - * // 0 after 1s - * // 1 after 2s - * // 0 after 3s - * // 1 after 4s - * // "...and it is done!" also after 4s - * - * @see {@link concatAll} - * @see {@link concatMap} - * @see {@link concatMapTo} - * - * @param {ObservableInput} input1 An input Observable to concatenate with others. - * @param {ObservableInput} input2 An input Observable to concatenate with others. - * More than one input Observables may be given as argument. - * @param {Scheduler} [scheduler=null] An optional IScheduler to schedule each - * Observable subscription on. - * @return {Observable} All values of each passed Observable merged into a - * single Observable, in order, in serial fashion. - * @static true - * @name concat - * @owner Observable - */ -export function concatStatic(...observables: Array | IScheduler>): Observable { - let scheduler: IScheduler = null; - let args = observables; - if (isScheduler(args[observables.length - 1])) { - scheduler = args.pop(); - } - - if (scheduler === null && observables.length === 1 && observables[0] instanceof Observable) { - return >observables[0]; - } - - return new ArrayObservable(observables, scheduler).lift(new MergeAllOperator(1)); + return higherOrder(...observables)(this); } diff --git a/src/operator/concatMap.ts b/src/operator/concatMap.ts index 4999e5ac43..9f41ab4bfb 100644 --- a/src/operator/concatMap.ts +++ b/src/operator/concatMap.ts @@ -1,4 +1,4 @@ -import { MergeMapOperator } from './mergeMap'; +import { concatMap as higherOrderConcatMap } from '../operators'; import { Observable, ObservableInput } from '../Observable'; /* tslint:disable:max-line-length */ @@ -67,5 +67,5 @@ export function concatMap(this: Observable, project: (value: T, inde */ export function concatMap(this: Observable, project: (value: T, index: number) => ObservableInput, resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { - return this.lift(new MergeMapOperator(project, resultSelector, 1)); + return higherOrderConcatMap(project, resultSelector)(this); } diff --git a/src/operator/concatMapTo.ts b/src/operator/concatMapTo.ts index 649aaf188e..7a904740b2 100644 --- a/src/operator/concatMapTo.ts +++ b/src/operator/concatMapTo.ts @@ -1,5 +1,5 @@ import { Observable, ObservableInput } from '../Observable'; -import { MergeMapToOperator } from './mergeMapTo'; +import { concatMapTo as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function concatMapTo(this: Observable, observable: ObservableInput): Observable; @@ -64,5 +64,5 @@ export function concatMapTo(this: Observable, observable: Observable */ export function concatMapTo(this: Observable, innerObservable: Observable, resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): Observable { - return this.lift(new MergeMapToOperator(innerObservable, resultSelector, 1)); + return higherOrder(innerObservable, resultSelector)(this); } diff --git a/src/operator/count.ts b/src/operator/count.ts index 2f5035dd7b..7a71f276ce 100644 --- a/src/operator/count.ts +++ b/src/operator/count.ts @@ -1,7 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Observer } from '../Observer'; -import { Subscriber } from '../Subscriber'; +import { count as higherOrder } from '../operators'; /** * Counts the number of emissions on the source and emits that number when the @@ -52,59 +50,5 @@ import { Subscriber } from '../Subscriber'; * @owner Observable */ export function count(this: Observable, predicate?: (value: T, index: number, source: Observable) => boolean): Observable { - return this.lift(new CountOperator(predicate, this)); -} - -class CountOperator implements Operator { - constructor(private predicate?: (value: T, index: number, source: Observable) => boolean, - private source?: Observable) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new CountSubscriber(subscriber, this.predicate, this.source)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class CountSubscriber extends Subscriber { - private count: number = 0; - private index: number = 0; - - constructor(destination: Observer, - private predicate?: (value: T, index: number, source: Observable) => boolean, - private source?: Observable) { - super(destination); - } - - protected _next(value: T): void { - if (this.predicate) { - this._tryPredicate(value); - } else { - this.count++; - } - } - - private _tryPredicate(value: T) { - let result: any; - - try { - result = this.predicate(value, this.index++, this.source); - } catch (err) { - this.destination.error(err); - return; - } - - if (result) { - this.count++; - } - } - - protected _complete(): void { - this.destination.next(this.count); - this.destination.complete(); - } + return higherOrder(predicate)(this); } diff --git a/src/operator/debounce.ts b/src/operator/debounce.ts index d19d508d26..895b7b6d9a 100644 --- a/src/operator/debounce.ts +++ b/src/operator/debounce.ts @@ -1,11 +1,6 @@ -import { Operator } from '../Operator'; -import { Observable, SubscribableOrPromise } from '../Observable'; -import { Subscriber } from '../Subscriber'; -import { Subscription, TeardownLogic } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { debounce as higherOrder } from '../operators'; /** * Emits a value from the source Observable only after a particular time span @@ -50,87 +45,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function debounce(this: Observable, durationSelector: (value: T) => SubscribableOrPromise): Observable { - return this.lift(new DebounceOperator(durationSelector)); -} - -class DebounceOperator implements Operator { - constructor(private durationSelector: (value: T) => SubscribableOrPromise) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DebounceSubscriber(subscriber, this.durationSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DebounceSubscriber extends OuterSubscriber { - private value: T; - private hasValue: boolean = false; - private durationSubscription: Subscription = null; - - constructor(destination: Subscriber, - private durationSelector: (value: T) => SubscribableOrPromise) { - super(destination); - } - - protected _next(value: T): void { - try { - const result = this.durationSelector.call(this, value); - - if (result) { - this._tryNext(value, result); - } - } catch (err) { - this.destination.error(err); - } - } - - protected _complete(): void { - this.emitValue(); - this.destination.complete(); - } - - private _tryNext(value: T, duration: SubscribableOrPromise): void { - let subscription = this.durationSubscription; - this.value = value; - this.hasValue = true; - if (subscription) { - subscription.unsubscribe(); - this.remove(subscription); - } - - subscription = subscribeToResult(this, duration); - if (!subscription.closed) { - this.add(this.durationSubscription = subscription); - } - } - - notifyNext(outerValue: T, innerValue: R, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.emitValue(); - } - - notifyComplete(): void { - this.emitValue(); - } - - emitValue(): void { - if (this.hasValue) { - const value = this.value; - const subscription = this.durationSubscription; - if (subscription) { - this.durationSubscription = null; - subscription.unsubscribe(); - this.remove(subscription); - } - this.value = null; - this.hasValue = false; - super._next(value); - } - } + return higherOrder(durationSelector)(this); } diff --git a/src/operator/debounceTime.ts b/src/operator/debounceTime.ts index 53fcc732fc..22233cbd94 100644 --- a/src/operator/debounceTime.ts +++ b/src/operator/debounceTime.ts @@ -1,9 +1,8 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; import { IScheduler } from '../Scheduler'; -import { Subscription, TeardownLogic } from '../Subscription'; import { async } from '../scheduler/async'; +import { debounceTime as higherOrder } from '../operators'; /** * Emits a value from the source Observable only after a particular time span @@ -52,67 +51,5 @@ import { async } from '../scheduler/async'; * @owner Observable */ export function debounceTime(this: Observable, dueTime: number, scheduler: IScheduler = async): Observable { - return this.lift(new DebounceTimeOperator(dueTime, scheduler)); -} - -class DebounceTimeOperator implements Operator { - constructor(private dueTime: number, private scheduler: IScheduler) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DebounceTimeSubscriber(subscriber, this.dueTime, this.scheduler)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DebounceTimeSubscriber extends Subscriber { - private debouncedSubscription: Subscription = null; - private lastValue: T = null; - private hasValue: boolean = false; - - constructor(destination: Subscriber, - private dueTime: number, - private scheduler: IScheduler) { - super(destination); - } - - protected _next(value: T) { - this.clearDebounce(); - this.lastValue = value; - this.hasValue = true; - this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext, this.dueTime, this)); - } - - protected _complete() { - this.debouncedNext(); - this.destination.complete(); - } - - debouncedNext(): void { - this.clearDebounce(); - - if (this.hasValue) { - this.destination.next(this.lastValue); - this.lastValue = null; - this.hasValue = false; - } - } - - private clearDebounce(): void { - const debouncedSubscription = this.debouncedSubscription; - - if (debouncedSubscription !== null) { - this.remove(debouncedSubscription); - debouncedSubscription.unsubscribe(); - this.debouncedSubscription = null; - } - } -} - -function dispatchNext(subscriber: DebounceTimeSubscriber) { - subscriber.debouncedNext(); + return higherOrder(dueTime, scheduler)(this); } diff --git a/src/operator/defaultIfEmpty.ts b/src/operator/defaultIfEmpty.ts index a28aad1271..7c0ef12cac 100644 --- a/src/operator/defaultIfEmpty.ts +++ b/src/operator/defaultIfEmpty.ts @@ -1,6 +1,6 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; +import { defaultIfEmpty as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function defaultIfEmpty(this: Observable, defaultValue?: T): Observable; @@ -38,40 +38,5 @@ export function defaultIfEmpty(this: Observable, defaultValue?: R): Obs * @owner Observable */ export function defaultIfEmpty(this: Observable, defaultValue: R = null): Observable { - return this.lift(new DefaultIfEmptyOperator(defaultValue)); -} - -class DefaultIfEmptyOperator implements Operator { - - constructor(private defaultValue: R) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new DefaultIfEmptySubscriber(subscriber, this.defaultValue)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DefaultIfEmptySubscriber extends Subscriber { - private isEmpty: boolean = true; - - constructor(destination: Subscriber, private defaultValue: R) { - super(destination); - } - - protected _next(value: T): void { - this.isEmpty = false; - this.destination.next(value); - } - - protected _complete(): void { - if (this.isEmpty) { - this.destination.next(this.defaultValue); - } - this.destination.complete(); - } + return higherOrder(defaultValue)(this); } diff --git a/src/operator/delay.ts b/src/operator/delay.ts index b52f561e6e..aa0bb3e8fd 100644 --- a/src/operator/delay.ts +++ b/src/operator/delay.ts @@ -1,13 +1,7 @@ import { async } from '../scheduler/async'; -import { isDate } from '../util/isDate'; -import { Operator } from '../Operator'; import { IScheduler } from '../Scheduler'; -import { Subscriber } from '../Subscriber'; -import { Action } from '../scheduler/Action'; -import { Notification } from '../Notification'; import { Observable } from '../Observable'; -import { PartialObserver } from '../Observer'; -import { TeardownLogic } from '../Subscription'; +import { delay as higherOrder } from '../operators'; /** * Delays the emission of items from the source Observable by a given timeout or @@ -50,99 +44,5 @@ import { TeardownLogic } from '../Subscription'; */ export function delay(this: Observable, delay: number|Date, scheduler: IScheduler = async): Observable { - const absoluteDelay = isDate(delay); - const delayFor = absoluteDelay ? (+delay - scheduler.now()) : Math.abs(delay); - return this.lift(new DelayOperator(delayFor, scheduler)); -} - -class DelayOperator implements Operator { - constructor(private delay: number, - private scheduler: IScheduler) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler)); - } -} - -interface DelayState { - source: DelaySubscriber; - destination: PartialObserver; - scheduler: IScheduler; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DelaySubscriber extends Subscriber { - private queue: Array> = []; - private active: boolean = false; - private errored: boolean = false; - - private static dispatch(this: Action>, state: DelayState): void { - const source = state.source; - const queue = source.queue; - const scheduler = state.scheduler; - const destination = state.destination; - - while (queue.length > 0 && (queue[0].time - scheduler.now()) <= 0) { - queue.shift().notification.observe(destination); - } - - if (queue.length > 0) { - const delay = Math.max(0, queue[0].time - scheduler.now()); - this.schedule(state, delay); - } else { - source.active = false; - } - } - - constructor(destination: Subscriber, - private delay: number, - private scheduler: IScheduler) { - super(destination); - } - - private _schedule(scheduler: IScheduler): void { - this.active = true; - this.add(scheduler.schedule>(DelaySubscriber.dispatch, this.delay, { - source: this, destination: this.destination, scheduler: scheduler - })); - } - - private scheduleNotification(notification: Notification): void { - if (this.errored === true) { - return; - } - - const scheduler = this.scheduler; - const message = new DelayMessage(scheduler.now() + this.delay, notification); - this.queue.push(message); - - if (this.active === false) { - this._schedule(scheduler); - } - } - - protected _next(value: T) { - this.scheduleNotification(Notification.createNext(value)); - } - - protected _error(err: any) { - this.errored = true; - this.queue = []; - this.destination.error(err); - } - - protected _complete() { - this.scheduleNotification(Notification.createComplete()); - } -} - -class DelayMessage { - constructor(public readonly time: number, - public readonly notification: Notification) { - } + return higherOrder(delay, scheduler)(this); } diff --git a/src/operator/delayWhen.ts b/src/operator/delayWhen.ts index b272f2793f..25f822e6a6 100644 --- a/src/operator/delayWhen.ts +++ b/src/operator/delayWhen.ts @@ -1,11 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { Subscription, TeardownLogic } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { delayWhen as higherOrder } from '../operators'; /** * Delays the emission of items from the source Observable by a given time span @@ -54,151 +49,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; */ export function delayWhen(this: Observable, delayDurationSelector: (value: T) => Observable, subscriptionDelay?: Observable): Observable { - if (subscriptionDelay) { - return new SubscriptionDelayObservable(this, subscriptionDelay) - .lift(new DelayWhenOperator(delayDurationSelector)); - } - return this.lift(new DelayWhenOperator(delayDurationSelector)); -} - -class DelayWhenOperator implements Operator { - constructor(private delayDurationSelector: (value: T) => Observable) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DelayWhenSubscriber(subscriber, this.delayDurationSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DelayWhenSubscriber extends OuterSubscriber { - private completed: boolean = false; - private delayNotifierSubscriptions: Array = []; - private values: Array = []; - - constructor(destination: Subscriber, - private delayDurationSelector: (value: T) => Observable) { - super(destination); - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.destination.next(outerValue); - this.removeSubscription(innerSub); - this.tryComplete(); - } - - notifyError(error: any, innerSub: InnerSubscriber): void { - this._error(error); - } - - notifyComplete(innerSub: InnerSubscriber): void { - const value = this.removeSubscription(innerSub); - if (value) { - this.destination.next(value); - } - this.tryComplete(); - } - - protected _next(value: T): void { - try { - const delayNotifier = this.delayDurationSelector(value); - if (delayNotifier) { - this.tryDelay(delayNotifier, value); - } - } catch (err) { - this.destination.error(err); - } - } - - protected _complete(): void { - this.completed = true; - this.tryComplete(); - } - - private removeSubscription(subscription: InnerSubscriber): T { - subscription.unsubscribe(); - - const subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription); - let value: T = null; - - if (subscriptionIdx !== -1) { - value = this.values[subscriptionIdx]; - this.delayNotifierSubscriptions.splice(subscriptionIdx, 1); - this.values.splice(subscriptionIdx, 1); - } - - return value; - } - - private tryDelay(delayNotifier: Observable, value: T): void { - const notifierSubscription = subscribeToResult(this, delayNotifier, value); - - if (notifierSubscription && !notifierSubscription.closed) { - this.add(notifierSubscription); - this.delayNotifierSubscriptions.push(notifierSubscription); - } - - this.values.push(value); - } - - private tryComplete(): void { - if (this.completed && this.delayNotifierSubscriptions.length === 0) { - this.destination.complete(); - } - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SubscriptionDelayObservable extends Observable { - constructor(protected source: Observable, private subscriptionDelay: Observable) { - super(); - } - - protected _subscribe(subscriber: Subscriber) { - this.subscriptionDelay.subscribe(new SubscriptionDelaySubscriber(subscriber, this.source)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SubscriptionDelaySubscriber extends Subscriber { - private sourceSubscribed: boolean = false; - - constructor(private parent: Subscriber, private source: Observable) { - super(); - } - - protected _next(unused: any) { - this.subscribeToSource(); - } - - protected _error(err: any) { - this.unsubscribe(); - this.parent.error(err); - } - - protected _complete() { - this.subscribeToSource(); - } - - private subscribeToSource(): void { - if (!this.sourceSubscribed) { - this.sourceSubscribed = true; - this.unsubscribe(); - this.source.subscribe(this.parent); - } - } + return higherOrder(delayDurationSelector, subscriptionDelay)(this); } diff --git a/src/operator/dematerialize.ts b/src/operator/dematerialize.ts index ceccab40c4..f8eeecaaab 100644 --- a/src/operator/dematerialize.ts +++ b/src/operator/dematerialize.ts @@ -1,7 +1,7 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; import { Notification } from '../Notification'; +import { dematerialize as higherOrder } from '../operators'; /** * Converts an Observable of {@link Notification} objects into the emissions @@ -43,27 +43,6 @@ import { Notification } from '../Notification'; * @method dematerialize * @owner Observable */ -export function dematerialize(this: Observable): Observable { - return this.lift(new DeMaterializeOperator()); -} - -class DeMaterializeOperator, R> implements Operator { - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new DeMaterializeSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DeMaterializeSubscriber> extends Subscriber { - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(value: T) { - value.observe(this.destination); - } +export function dematerialize(this: Observable>): Observable { + return higherOrder()(this); } diff --git a/src/operator/distinctUntilChanged.ts b/src/operator/distinctUntilChanged.ts index 525067fea2..a418be492d 100644 --- a/src/operator/distinctUntilChanged.ts +++ b/src/operator/distinctUntilChanged.ts @@ -1,9 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; + import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; +import { distinctUntilChanged as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function distinctUntilChanged(this: Observable, compare?: (x: T, y: T) => boolean): Observable; @@ -50,67 +47,5 @@ export function distinctUntilChanged(this: Observable, compare: (x: K, * @owner Observable */ export function distinctUntilChanged(this: Observable, compare?: (x: K, y: K) => boolean, keySelector?: (x: T) => K): Observable { - return this.lift(new DistinctUntilChangedOperator(compare, keySelector)); -} - -class DistinctUntilChangedOperator implements Operator { - constructor(private compare: (x: K, y: K) => boolean, - private keySelector: (x: T) => K) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DistinctUntilChangedSubscriber(subscriber, this.compare, this.keySelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DistinctUntilChangedSubscriber extends Subscriber { - private key: K; - private hasKey: boolean = false; - - constructor(destination: Subscriber, - compare: (x: K, y: K) => boolean, - private keySelector: (x: T) => K) { - super(destination); - if (typeof compare === 'function') { - this.compare = compare; - } - } - - private compare(x: any, y: any): boolean { - return x === y; - } - - protected _next(value: T): void { - - const keySelector = this.keySelector; - let key: any = value; - - if (keySelector) { - key = tryCatch(this.keySelector)(value); - if (key === errorObject) { - return this.destination.error(errorObject.e); - } - } - - let result: any = false; - - if (this.hasKey) { - result = tryCatch(this.compare)(this.key, key); - if (result === errorObject) { - return this.destination.error(errorObject.e); - } - } else { - this.hasKey = true; - } - - if (Boolean(result) === false) { - this.key = key; - this.destination.next(value); - } - } + return higherOrder(compare, keySelector)(this); } diff --git a/src/operator/distinctUntilKeyChanged.ts b/src/operator/distinctUntilKeyChanged.ts index 147fc8f40f..62d8c3e7a5 100644 --- a/src/operator/distinctUntilKeyChanged.ts +++ b/src/operator/distinctUntilKeyChanged.ts @@ -1,5 +1,6 @@ -import { distinctUntilChanged } from './distinctUntilChanged'; + import { Observable } from '../Observable'; +import { distinctUntilKeyChanged as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function distinctUntilKeyChanged(this: Observable, key: string): Observable; @@ -64,10 +65,5 @@ export function distinctUntilKeyChanged(this: Observable, key: string, * @owner Observable */ export function distinctUntilKeyChanged(this: Observable, key: string, compare?: (x: T, y: T) => boolean): Observable { - return distinctUntilChanged.call(this, function(x: T, y: T) { - if (compare) { - return compare(x[key], y[key]); - } - return x[key] === y[key]; - }); + return higherOrder(key, compare)(this); } diff --git a/src/operator/do.ts b/src/operator/do.ts index e39c22faec..d7b113c299 100644 --- a/src/operator/do.ts +++ b/src/operator/do.ts @@ -1,8 +1,7 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; + import { Observable } from '../Observable'; import { PartialObserver } from '../Observer'; -import { TeardownLogic } from '../Subscription'; +import { tap as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function _do(this: Observable, next: (x: T) => void, error?: (e: any) => void, complete?: () => void): Observable; @@ -55,67 +54,5 @@ export function _do(this: Observable, observer: PartialObserver): Obser export function _do(this: Observable, nextOrObserver?: PartialObserver | ((x: T) => void), error?: (e: any) => void, complete?: () => void): Observable { - return this.lift(new DoOperator(nextOrObserver, error, complete)); -} - -class DoOperator implements Operator { - constructor(private nextOrObserver?: PartialObserver | ((x: T) => void), - private error?: (e: any) => void, - private complete?: () => void) { - } - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DoSubscriber(subscriber, this.nextOrObserver, this.error, this.complete)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DoSubscriber extends Subscriber { - - private safeSubscriber: Subscriber; - - constructor(destination: Subscriber, - nextOrObserver?: PartialObserver | ((x: T) => void), - error?: (e: any) => void, - complete?: () => void) { - super(destination); - - const safeSubscriber = new Subscriber(nextOrObserver, error, complete); - safeSubscriber.syncErrorThrowable = true; - this.add(safeSubscriber); - this.safeSubscriber = safeSubscriber; - } - - protected _next(value: T): void { - const { safeSubscriber } = this; - safeSubscriber.next(value); - if (safeSubscriber.syncErrorThrown) { - this.destination.error(safeSubscriber.syncErrorValue); - } else { - this.destination.next(value); - } - } - - protected _error(err: any): void { - const { safeSubscriber } = this; - safeSubscriber.error(err); - if (safeSubscriber.syncErrorThrown) { - this.destination.error(safeSubscriber.syncErrorValue); - } else { - this.destination.error(err); - } - } - - protected _complete(): void { - const { safeSubscriber } = this; - safeSubscriber.complete(); - if (safeSubscriber.syncErrorThrown) { - this.destination.error(safeSubscriber.syncErrorValue); - } else { - this.destination.complete(); - } - } + return higherOrder(nextOrObserver, error, complete)(this); } diff --git a/src/operator/elementAt.ts b/src/operator/elementAt.ts index 65561e0890..864ed324dd 100644 --- a/src/operator/elementAt.ts +++ b/src/operator/elementAt.ts @@ -1,8 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; + import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; +import { elementAt as higherOrder } from '../operators'; /** * Emits the single value at the specified `index` in a sequence of emissions @@ -47,49 +45,5 @@ import { TeardownLogic } from '../Subscription'; * @owner Observable */ export function elementAt(this: Observable, index: number, defaultValue?: T): Observable { - return this.lift(new ElementAtOperator(index, defaultValue)); -} - -class ElementAtOperator implements Operator { - - constructor(private index: number, private defaultValue?: T) { - if (index < 0) { - throw new ArgumentOutOfRangeError; - } - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ElementAtSubscriber(subscriber, this.index, this.defaultValue)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ElementAtSubscriber extends Subscriber { - - constructor(destination: Subscriber, private index: number, private defaultValue?: T) { - super(destination); - } - - protected _next(x: T) { - if (this.index-- === 0) { - this.destination.next(x); - this.destination.complete(); - } - } - - protected _complete() { - const destination = this.destination; - if (this.index >= 0) { - if (typeof this.defaultValue !== 'undefined') { - destination.next(this.defaultValue); - } else { - destination.error(new ArgumentOutOfRangeError); - } - } - destination.complete(); - } + return higherOrder(index, defaultValue)(this); } diff --git a/src/operator/every.ts b/src/operator/every.ts index 9bdcbf76c3..5b107617f6 100644 --- a/src/operator/every.ts +++ b/src/operator/every.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; -import { Observer } from '../Observer'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; +import { every as higherOrder } from '../operators'; /** * Returns an Observable that emits whether or not every item of the source satisfies the condition specified. @@ -19,56 +18,5 @@ import { Subscriber } from '../Subscriber'; */ export function every(this: Observable, predicate: (value: T, index: number, source: Observable) => boolean, thisArg?: any): Observable { - return this.lift(new EveryOperator(predicate, thisArg, this)); -} - -class EveryOperator implements Operator { - constructor(private predicate: (value: T, index: number, source: Observable) => boolean, - private thisArg?: any, - private source?: Observable) { - } - - call(observer: Subscriber, source: any): any { - return source.subscribe(new EverySubscriber(observer, this.predicate, this.thisArg, this.source)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class EverySubscriber extends Subscriber { - private index: number = 0; - - constructor(destination: Observer, - private predicate: (value: T, index: number, source: Observable) => boolean, - private thisArg: any, - private source?: Observable) { - super(destination); - this.thisArg = thisArg || this; - } - - private notifyComplete(everyValueMatch: boolean): void { - this.destination.next(everyValueMatch); - this.destination.complete(); - } - - protected _next(value: T): void { - let result = false; - try { - result = this.predicate.call(this.thisArg, value, this.index++, this.source); - } catch (err) { - this.destination.error(err); - return; - } - - if (!result) { - this.notifyComplete(false); - } - } - - protected _complete(): void { - this.notifyComplete(true); - } -} + return higherOrder(predicate, thisArg)(this); +} \ No newline at end of file diff --git a/src/operator/exhaust.ts b/src/operator/exhaust.ts index c06675ca82..29c4154435 100644 --- a/src/operator/exhaust.ts +++ b/src/operator/exhaust.ts @@ -1,9 +1,6 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; -import { Subscription, TeardownLogic } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { exhaust as higherOrder } from '../operators'; /** * Converts a higher-order Observable into a first-order Observable by dropping @@ -41,47 +38,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function exhaust(this: Observable): Observable { - return this.lift(new SwitchFirstOperator()); -} - -class SwitchFirstOperator implements Operator { - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SwitchFirstSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SwitchFirstSubscriber extends OuterSubscriber { - private hasCompleted: boolean = false; - private hasSubscription: boolean = false; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(value: T): void { - if (!this.hasSubscription) { - this.hasSubscription = true; - this.add(subscribeToResult(this, value)); - } - } - - protected _complete(): void { - this.hasCompleted = true; - if (!this.hasSubscription) { - this.destination.complete(); - } - } - - notifyComplete(innerSub: Subscription): void { - this.remove(innerSub); - this.hasSubscription = false; - if (this.hasCompleted) { - this.destination.complete(); - } - } + return higherOrder()(this); } diff --git a/src/operator/exhaustMap.ts b/src/operator/exhaustMap.ts index be1f74710b..111a5a151c 100644 --- a/src/operator/exhaustMap.ts +++ b/src/operator/exhaustMap.ts @@ -1,10 +1,6 @@ -import { Operator } from '../Operator'; + import { Observable, ObservableInput } from '../Observable'; -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { exhaustMap as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function exhaustMap(this: Observable, project: (value: T, index: number) => ObservableInput): Observable; @@ -58,92 +54,5 @@ export function exhaustMap(this: Observable, project: (value: T, ind */ export function exhaustMap(this: Observable, project: (value: T, index: number) => ObservableInput, resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): Observable { - return this.lift(new SwitchFirstMapOperator(project, resultSelector)); -} - -class SwitchFirstMapOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new SwitchFirstMapSubscriber(subscriber, this.project, this.resultSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SwitchFirstMapSubscriber extends OuterSubscriber { - private hasSubscription: boolean = false; - private hasCompleted: boolean = false; - private index: number = 0; - - constructor(destination: Subscriber, - private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { - super(destination); - } - - protected _next(value: T): void { - if (!this.hasSubscription) { - this.tryNext(value); - } - } - - private tryNext(value: T): void { - const index = this.index++; - const destination = this.destination; - try { - const result = this.project(value, index); - this.hasSubscription = true; - this.add(subscribeToResult(this, result, value, index)); - } catch (err) { - destination.error(err); - } - } - - protected _complete(): void { - this.hasCompleted = true; - if (!this.hasSubscription) { - this.destination.complete(); - } - } - - notifyNext(outerValue: T, innerValue: I, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - const { resultSelector, destination } = this; - if (resultSelector) { - this.trySelectResult(outerValue, innerValue, outerIndex, innerIndex); - } else { - destination.next(innerValue); - } - } - - private trySelectResult(outerValue: T, innerValue: I, - outerIndex: number, innerIndex: number): void { - const { resultSelector, destination } = this; - try { - const result = resultSelector(outerValue, innerValue, outerIndex, innerIndex); - destination.next(result); - } catch (err) { - destination.error(err); - } - } - - notifyError(err: any): void { - this.destination.error(err); - } - - notifyComplete(innerSub: Subscription): void { - this.remove(innerSub); - - this.hasSubscription = false; - if (this.hasCompleted) { - this.destination.complete(); - } - } + return higherOrder(project, resultSelector)(this); } diff --git a/src/operator/expand.ts b/src/operator/expand.ts index e4975e4cfe..b6f9183fe9 100644 --- a/src/operator/expand.ts +++ b/src/operator/expand.ts @@ -1,13 +1,6 @@ import { Observable } from '../Observable'; import { IScheduler } from '../Scheduler'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; -import { Subscription } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { expand as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function expand(this: Observable, project: (value: T, index: number) => Observable, concurrent?: number, scheduler?: IScheduler): Observable; @@ -64,105 +57,5 @@ export function expand(this: Observable, project: (value: T, index: num scheduler: IScheduler = undefined): Observable { concurrent = (concurrent || 0) < 1 ? Number.POSITIVE_INFINITY : concurrent; - return this.lift(new ExpandOperator(project, concurrent, scheduler)); -} - -export class ExpandOperator implements Operator { - constructor(private project: (value: T, index: number) => Observable, - private concurrent: number, - private scheduler: IScheduler) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ExpandSubscriber(subscriber, this.project, this.concurrent, this.scheduler)); - } -} - -interface DispatchArg { - subscriber: ExpandSubscriber; - result: Observable; - value: any; - index: number; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class ExpandSubscriber extends OuterSubscriber { - private index: number = 0; - private active: number = 0; - private hasCompleted: boolean = false; - private buffer: any[]; - - constructor(destination: Subscriber, - private project: (value: T, index: number) => Observable, - private concurrent: number, - private scheduler: IScheduler) { - super(destination); - if (concurrent < Number.POSITIVE_INFINITY) { - this.buffer = []; - } - } - - private static dispatch(arg: DispatchArg): void { - const {subscriber, result, value, index} = arg; - subscriber.subscribeToProjection(result, value, index); - } - - protected _next(value: any): void { - const destination = this.destination; - - if (destination.closed) { - this._complete(); - return; - } - - const index = this.index++; - if (this.active < this.concurrent) { - destination.next(value); - let result = tryCatch(this.project)(value, index); - if (result === errorObject) { - destination.error(errorObject.e); - } else if (!this.scheduler) { - this.subscribeToProjection(result, value, index); - } else { - const state: DispatchArg = { subscriber: this, result, value, index }; - this.add(this.scheduler.schedule(ExpandSubscriber.dispatch, 0, state)); - } - } else { - this.buffer.push(value); - } - } - - private subscribeToProjection(result: any, value: T, index: number): void { - this.active++; - this.add(subscribeToResult(this, result, value, index)); - } - - protected _complete(): void { - this.hasCompleted = true; - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - } - - notifyNext(outerValue: T, innerValue: R, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this._next(innerValue); - } - - notifyComplete(innerSub: Subscription): void { - const buffer = this.buffer; - this.remove(innerSub); - this.active--; - if (buffer && buffer.length > 0) { - this._next(buffer.shift()); - } - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - } + return higherOrder(project, concurrent, scheduler)(this); } diff --git a/src/operator/filter.ts b/src/operator/filter.ts index d9961fbdbc..67d81acdf5 100644 --- a/src/operator/filter.ts +++ b/src/operator/filter.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; + import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; +import { filter as higherOrderFilter } from '../operators'; /* tslint:disable:max-line-length */ export function filter(this: Observable, @@ -53,46 +52,5 @@ export function filter(this: Observable, */ export function filter(this: Observable, predicate: (value: T, index: number) => boolean, thisArg?: any): Observable { - return this.lift(new FilterOperator(predicate, thisArg)); -} - -class FilterOperator implements Operator { - constructor(private predicate: (value: T, index: number) => boolean, - private thisArg?: any) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class FilterSubscriber extends Subscriber { - - count: number = 0; - - constructor(destination: Subscriber, - private predicate: (value: T, index: number) => boolean, - private thisArg: any) { - super(destination); - } - - // the try catch block below is left specifically for - // optimization and perf reasons. a tryCatcher is not necessary here. - protected _next(value: T) { - let result: any; - try { - result = this.predicate.call(this.thisArg, value, this.count++); - } catch (err) { - this.destination.error(err); - return; - } - if (result) { - this.destination.next(value); - } - } + return higherOrderFilter(predicate, thisArg)(this); } diff --git a/src/operator/finally.ts b/src/operator/finally.ts index 269858a580..bd944f81f1 100644 --- a/src/operator/finally.ts +++ b/src/operator/finally.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Subscription, TeardownLogic } from '../Subscription'; + import { Observable } from '../Observable'; +import { finalize } from '../operators'; /** * Returns an Observable that mirrors the source Observable, but will call a specified function when @@ -12,26 +11,5 @@ import { Observable } from '../Observable'; * @owner Observable */ export function _finally(this: Observable, callback: () => void): Observable { - return this.lift(new FinallyOperator(callback)); -} - -class FinallyOperator implements Operator { - constructor(private callback: () => void) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new FinallySubscriber(subscriber, this.callback)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class FinallySubscriber extends Subscriber { - constructor(destination: Subscriber, callback: () => void) { - super(destination); - this.add(new Subscription(callback)); - } + return finalize(callback)(this); } diff --git a/src/operator/find.ts b/src/operator/find.ts index 110c813958..7f39d8de8b 100644 --- a/src/operator/find.ts +++ b/src/operator/find.ts @@ -1,6 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; +import { find as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function find(this: Observable, @@ -46,61 +45,5 @@ export function find(this: Observable, */ export function find(this: Observable, predicate: (value: T, index: number, source: Observable) => boolean, thisArg?: any): Observable { - if (typeof predicate !== 'function') { - throw new TypeError('predicate is not a function'); - } - return this.lift(new FindValueOperator(predicate, this, false, thisArg)); -} - -export class FindValueOperator implements Operator { - constructor(private predicate: (value: T, index: number, source: Observable) => boolean, - private source: Observable, - private yieldIndex: boolean, - private thisArg?: any) { - } - - call(observer: Subscriber, source: any): any { - return source.subscribe(new FindValueSubscriber(observer, this.predicate, this.source, this.yieldIndex, this.thisArg)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class FindValueSubscriber extends Subscriber { - private index: number = 0; - - constructor(destination: Subscriber, - private predicate: (value: T, index: number, source: Observable) => boolean, - private source: Observable, - private yieldIndex: boolean, - private thisArg?: any) { - super(destination); - } - - private notifyComplete(value: any): void { - const destination = this.destination; - - destination.next(value); - destination.complete(); - } - - protected _next(value: T): void { - const { predicate, thisArg } = this; - const index = this.index++; - try { - const result = predicate.call(thisArg || this, value, index, this.source); - if (result) { - this.notifyComplete(this.yieldIndex ? index : value); - } - } catch (err) { - this.destination.error(err); - } - } - - protected _complete(): void { - this.notifyComplete(this.yieldIndex ? -1 : undefined); - } + return higherOrder(predicate, thisArg)(this); } diff --git a/src/operator/findIndex.ts b/src/operator/findIndex.ts index f12789e333..0195ee3785 100644 --- a/src/operator/findIndex.ts +++ b/src/operator/findIndex.ts @@ -1,6 +1,5 @@ import { Observable } from '../Observable'; -import { FindValueOperator } from './find'; - +import { findIndex as higherOrder } from '../operators'; /** * Emits only the index of the first value emitted by the source Observable that * meets some condition. @@ -37,5 +36,5 @@ import { FindValueOperator } from './find'; */ export function findIndex(this: Observable, predicate: (value: T, index: number, source: Observable) => boolean, thisArg?: any): Observable { - return this.lift(new FindValueOperator(predicate, this, true, thisArg)); + return higherOrder(predicate, thisArg)(this); } diff --git a/src/operator/first.ts b/src/operator/first.ts index cb6d09baf4..8f7529b841 100644 --- a/src/operator/first.ts +++ b/src/operator/first.ts @@ -1,7 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { EmptyError } from '../util/EmptyError'; +import { first as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function first(this: Observable, @@ -76,97 +74,5 @@ export function first(this: Observable, export function first(this: Observable, predicate?: (value: T, index: number, source: Observable) => boolean, resultSelector?: ((value: T, index: number) => R) | void, defaultValue?: R): Observable { - return this.lift(new FirstOperator(predicate, resultSelector, defaultValue, this)); -} - -class FirstOperator implements Operator { - constructor(private predicate?: (value: T, index: number, source: Observable) => boolean, - private resultSelector?: ((value: T, index: number) => R) | void, - private defaultValue?: any, - private source?: Observable) { - } - - call(observer: Subscriber, source: any): any { - return source.subscribe(new FirstSubscriber(observer, this.predicate, this.resultSelector, this.defaultValue, this.source)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class FirstSubscriber extends Subscriber { - private index: number = 0; - private hasCompleted: boolean = false; - private _emitted: boolean = false; - - constructor(destination: Subscriber, - private predicate?: (value: T, index: number, source: Observable) => boolean, - private resultSelector?: ((value: T, index: number) => R) | void, - private defaultValue?: any, - private source?: Observable) { - super(destination); - } - - protected _next(value: T): void { - const index = this.index++; - if (this.predicate) { - this._tryPredicate(value, index); - } else { - this._emit(value, index); - } - } - - private _tryPredicate(value: T, index: number) { - let result: any; - try { - result = this.predicate(value, index, this.source); - } catch (err) { - this.destination.error(err); - 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 = (this).resultSelector(value, index); - } catch (err) { - this.destination.error(err); - return; - } - this._emitFinal(result); - } - - private _emitFinal(value: any) { - const destination = this.destination; - if (!this._emitted) { - this._emitted = true; - destination.next(value); - destination.complete(); - this.hasCompleted = true; - } - } - - protected _complete(): void { - const destination = this.destination; - if (!this.hasCompleted && typeof this.defaultValue !== 'undefined') { - destination.next(this.defaultValue); - destination.complete(); - } else if (!this.hasCompleted) { - destination.error(new EmptyError); - } - } + return higherOrder(predicate, resultSelector as any, defaultValue)(this); } diff --git a/src/operator/groupBy.ts b/src/operator/groupBy.ts index de801caf17..9ceb6fe34f 100644 --- a/src/operator/groupBy.ts +++ b/src/operator/groupBy.ts @@ -1,10 +1,7 @@ -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; + import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subject } from '../Subject'; -import { Map } from '../util/Map'; -import { FastMap } from '../util/FastMap'; +import { groupBy as higherOrder, GroupedObservable } from '../operators/groupBy'; /* tslint:disable:max-line-length */ export function groupBy(this: Observable, keySelector: (value: T) => K): Observable>; @@ -84,210 +81,5 @@ export function groupBy(this: Observable, keySelector: (value: T) => elementSelector?: ((value: T) => R) | void, durationSelector?: (grouped: GroupedObservable) => Observable, subjectSelector?: () => Subject): Observable> { - return this.lift(new GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector)); -} - -export interface RefCountSubscription { - count: number; - unsubscribe: () => void; - closed: boolean; - attemptedToUnsubscribe: boolean; -} - -class GroupByOperator implements Operator> { - constructor(private keySelector: (value: T) => K, - private elementSelector?: ((value: T) => R) | void, - private durationSelector?: (grouped: GroupedObservable) => Observable, - private subjectSelector?: () => Subject) { - } - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new GroupBySubscriber( - subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector - )); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class GroupBySubscriber extends Subscriber implements RefCountSubscription { - private groups: Map> = null; - public attemptedToUnsubscribe: boolean = false; - public count: number = 0; - - constructor(destination: Subscriber>, - private keySelector: (value: T) => K, - private elementSelector?: ((value: T) => R) | void, - private durationSelector?: (grouped: GroupedObservable) => Observable, - private subjectSelector?: () => Subject) { - super(destination); - } - - protected _next(value: T): void { - let key: K; - try { - key = this.keySelector(value); - } catch (err) { - this.error(err); - return; - } - - this._group(value, key); - } - - private _group(value: T, key: K) { - let groups = this.groups; - - if (!groups) { - groups = this.groups = typeof key === 'string' ? new FastMap() : new Map(); - } - - let group = groups.get(key); - - let element: R; - if (this.elementSelector) { - try { - element = this.elementSelector(value); - } catch (err) { - this.error(err); - } - } else { - element = value; - } - - if (!group) { - group = this.subjectSelector ? this.subjectSelector() : new Subject(); - groups.set(key, group); - const groupedObservable = new GroupedObservable(key, group, this); - this.destination.next(groupedObservable); - if (this.durationSelector) { - let duration: any; - try { - duration = this.durationSelector(new GroupedObservable(key, >group)); - } catch (err) { - this.error(err); - return; - } - this.add(duration.subscribe(new GroupDurationSubscriber(key, group, this))); - } - } - - if (!group.closed) { - group.next(element); - } - } - - protected _error(err: any): void { - const groups = this.groups; - if (groups) { - groups.forEach((group, key) => { - group.error(err); - }); - - groups.clear(); - } - this.destination.error(err); - } - - protected _complete(): void { - const groups = this.groups; - if (groups) { - groups.forEach((group, key) => { - group.complete(); - }); - - groups.clear(); - } - this.destination.complete(); - } - - removeGroup(key: K): void { - this.groups.delete(key); - } - - unsubscribe() { - if (!this.closed) { - this.attemptedToUnsubscribe = true; - if (this.count === 0) { - super.unsubscribe(); - } - } - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class GroupDurationSubscriber extends Subscriber { - constructor(private key: K, - private group: Subject, - private parent: GroupBySubscriber) { - super(group); - } - - protected _next(value: T): void { - this.complete(); - } - - protected _unsubscribe() { - const { parent, key } = this; - this.key = this.parent = null; - if (parent) { - parent.removeGroup(key); - } - } -} - -/** - * An Observable representing values belonging to the same group represented by - * a common key. The values emitted by a GroupedObservable come from the source - * Observable. The common key is available as the field `key` on a - * GroupedObservable instance. - * - * @class GroupedObservable - */ -export class GroupedObservable extends Observable { - constructor(public key: K, - private groupSubject: Subject, - private refCountSubscription?: RefCountSubscription) { - super(); - } - - protected _subscribe(subscriber: Subscriber) { - const subscription = new Subscription(); - const {refCountSubscription, groupSubject} = this; - if (refCountSubscription && !refCountSubscription.closed) { - subscription.add(new InnerRefCountSubscription(refCountSubscription)); - } - subscription.add(groupSubject.subscribe(subscriber)); - return subscription; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class InnerRefCountSubscription extends Subscription { - constructor(private parent: RefCountSubscription) { - super(); - parent.count++; - } - - unsubscribe() { - const parent = this.parent; - if (!parent.closed && !this.closed) { - super.unsubscribe(); - parent.count -= 1; - if (parent.count === 0 && parent.attemptedToUnsubscribe) { - parent.unsubscribe(); - } - } - } + return higherOrder(keySelector, elementSelector as any, durationSelector, subjectSelector)(this); } diff --git a/src/operator/ignoreElements.ts b/src/operator/ignoreElements.ts index 23727ead2e..2cdf11bece 100644 --- a/src/operator/ignoreElements.ts +++ b/src/operator/ignoreElements.ts @@ -1,7 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { noop } from '../util/noop'; +import { ignoreElements as higherOrder } from '../operators'; /** * Ignores all items emitted by the source Observable and only passes calls of `complete` or `error`. @@ -14,22 +12,5 @@ import { noop } from '../util/noop'; * @owner Observable */ export function ignoreElements(this: Observable): Observable { - return this.lift(new IgnoreElementsOperator()); + return higherOrder()(this); }; - -class IgnoreElementsOperator implements Operator { - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new IgnoreElementsSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class IgnoreElementsSubscriber extends Subscriber { - protected _next(unused: T): void { - noop(); - } -} diff --git a/src/operator/map.ts b/src/operator/map.ts index cf49b6b776..03879dde50 100644 --- a/src/operator/map.ts +++ b/src/operator/map.ts @@ -1,5 +1,4 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; +import { map as higherOrderMap } from '../operators'; import { Observable } from '../Observable'; /** @@ -36,47 +35,5 @@ import { Observable } from '../Observable'; * @owner Observable */ export function map(this: Observable, project: (value: T, index: number) => R, thisArg?: any): Observable { - if (typeof project !== 'function') { - throw new TypeError('argument is not a function. Are you looking for `mapTo()`?'); - } - return this.lift(new MapOperator(project, thisArg)); -} - -export class MapOperator implements Operator { - constructor(private project: (value: T, index: number) => R, private thisArg: any) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new MapSubscriber(subscriber, this.project, this.thisArg)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class MapSubscriber extends Subscriber { - count: number = 0; - private thisArg: any; - - constructor(destination: Subscriber, - private project: (value: T, index: number) => R, - thisArg: any) { - super(destination); - this.thisArg = thisArg || this; - } - - // NOTE: This looks unoptimized, but it's actually purposefully NOT - // using try/catch optimizations. - protected _next(value: T) { - let result: any; - try { - result = this.project.call(this.thisArg, value, this.count++); - } catch (err) { - this.destination.error(err); - return; - } - this.destination.next(result); - } + return higherOrderMap(project, thisArg)(this); } diff --git a/src/operator/materialize.ts b/src/operator/materialize.ts index 2ea2c4d393..3123d0e910 100644 --- a/src/operator/materialize.ts +++ b/src/operator/materialize.ts @@ -1,7 +1,7 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; import { Notification } from '../Notification'; +import { materialize as higherOrder } from '../operators'; /** * Represents all of the notifications from the source Observable as `next` @@ -48,38 +48,5 @@ import { Notification } from '../Notification'; * @owner Observable */ export function materialize(this: Observable): Observable> { - return this.lift(new MaterializeOperator()); -} - -class MaterializeOperator implements Operator> { - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new MaterializeSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class MaterializeSubscriber extends Subscriber { - constructor(destination: Subscriber>) { - super(destination); - } - - protected _next(value: T) { - this.destination.next(Notification.createNext(value)); - } - - protected _error(err: any) { - const destination = this.destination; - destination.next(Notification.createError(err)); - destination.complete(); - } - - protected _complete() { - const destination = this.destination; - destination.next(Notification.createComplete()); - destination.complete(); - } + return higherOrder()(this); } diff --git a/src/operator/max.ts b/src/operator/max.ts index eedcd7b97a..240d3d3b45 100644 --- a/src/operator/max.ts +++ b/src/operator/max.ts @@ -1,5 +1,5 @@ import { Observable } from '../Observable'; -import { ReduceOperator } from './reduce'; +import { max as higherOrderMax } from '../operators'; /** * The Max operator operates on an Observable that emits numbers (or items that can be compared with a provided function), @@ -33,8 +33,5 @@ import { ReduceOperator } from './reduce'; * @owner Observable */ export function max(this: Observable, comparer?: (x: T, y: T) => number): Observable { - const max: (x: T, y: T) => T = (typeof comparer === 'function') - ? (x, y) => comparer(x, y) > 0 ? x : y - : (x, y) => x > y ? x : y; - return this.lift(new ReduceOperator(max)); + return higherOrderMax(comparer)(this); } diff --git a/src/operator/mergeMap.ts b/src/operator/mergeMap.ts index 44d53906c2..b4fdaec05f 100644 --- a/src/operator/mergeMap.ts +++ b/src/operator/mergeMap.ts @@ -1,10 +1,5 @@ import { Observable, ObservableInput } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; -import { subscribeToResult } from '../util/subscribeToResult'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; +import { mergeMap as higherOrderMergeMap } from '../operators'; /* tslint:disable:max-line-length */ export function mergeMap(this: Observable, project: (value: T, index: number) => ObservableInput, concurrent?: number): Observable; @@ -72,105 +67,5 @@ export function mergeMap(this: Observable, project: (value: T, index export function mergeMap(this: Observable, project: (value: T, index: number) => ObservableInput, resultSelector?: ((outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) | number, concurrent: number = Number.POSITIVE_INFINITY): Observable { - if (typeof resultSelector === 'number') { - concurrent = resultSelector; - resultSelector = null; - } - return this.lift(new MergeMapOperator(project, resultSelector, concurrent)); -} - -export class MergeMapOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R, - private concurrent: number = Number.POSITIVE_INFINITY) { - } - - call(observer: Subscriber, source: any): any { - return source.subscribe(new MergeMapSubscriber( - observer, this.project, this.resultSelector, this.concurrent - )); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class MergeMapSubscriber extends OuterSubscriber { - private hasCompleted: boolean = false; - private buffer: T[] = []; - private active: number = 0; - protected index: number = 0; - - constructor(destination: Subscriber, - private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R, - private concurrent: number = Number.POSITIVE_INFINITY) { - super(destination); - } - - protected _next(value: T): void { - if (this.active < this.concurrent) { - this._tryNext(value); - } else { - this.buffer.push(value); - } - } - - protected _tryNext(value: T) { - let result: ObservableInput; - const index = this.index++; - try { - result = this.project(value, index); - } catch (err) { - this.destination.error(err); - return; - } - this.active++; - this._innerSub(result, value, index); - } - - private _innerSub(ish: ObservableInput, value: T, index: number): void { - this.add(subscribeToResult(this, ish, value, index)); - } - - protected _complete(): void { - this.hasCompleted = true; - if (this.active === 0 && this.buffer.length === 0) { - this.destination.complete(); - } - } - - notifyNext(outerValue: T, innerValue: I, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - if (this.resultSelector) { - this._notifyResultSelector(outerValue, innerValue, outerIndex, innerIndex); - } else { - this.destination.next(innerValue); - } - } - - private _notifyResultSelector(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) { - let result: R; - try { - result = this.resultSelector(outerValue, innerValue, outerIndex, innerIndex); - } catch (err) { - this.destination.error(err); - return; - } - this.destination.next(result); - } - - notifyComplete(innerSub: Subscription): void { - const buffer = this.buffer; - this.remove(innerSub); - this.active--; - if (buffer.length > 0) { - this._next(buffer.shift()); - } else if (this.active === 0 && this.hasCompleted) { - this.destination.complete(); - } - } + return higherOrderMergeMap(project, resultSelector, concurrent)(this); } diff --git a/src/operator/min.ts b/src/operator/min.ts index 997fb5dd3f..27eb2e331f 100644 --- a/src/operator/min.ts +++ b/src/operator/min.ts @@ -1,5 +1,5 @@ import { Observable } from '../Observable'; -import { ReduceOperator } from './reduce'; +import { min as higherOrderMin } from '../operators'; /** * The Min operator operates on an Observable that emits numbers (or items that can be compared with a provided function), @@ -33,8 +33,5 @@ import { ReduceOperator } from './reduce'; * @owner Observable */ export function min(this: Observable, comparer?: (x: T, y: T) => number): Observable { - const min: (x: T, y: T) => T = (typeof comparer === 'function') - ? (x, y) => comparer(x, y) < 0 ? x : y - : (x, y) => x < y ? x : y; - return this.lift(new ReduceOperator(min)); + return higherOrderMin(comparer)(this); } diff --git a/src/operator/multicast.ts b/src/operator/multicast.ts index 75a71cf0c5..5438f4bf9f 100644 --- a/src/operator/multicast.ts +++ b/src/operator/multicast.ts @@ -1,12 +1,12 @@ import { Subject } from '../Subject'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { ConnectableObservable, connectableObservableDescriptor } from '../observable/ConnectableObservable'; +import { ConnectableObservable } from '../observable/ConnectableObservable'; +import { multicast as higherOrder } from '../operators'; +import { FactoryOrValue, MonoTypeOperatorFunction } from '../interfaces'; /* tslint:disable:max-line-length */ -export function multicast(this: Observable, subjectOrSubjectFactory: factoryOrValue>): ConnectableObservable; -export function multicast(SubjectFactory: (this: Observable) => Subject, selector?: selector): Observable; +export function multicast(this: Observable, subjectOrSubjectFactory: FactoryOrValue>): ConnectableObservable; +export function multicast(SubjectFactory: (this: Observable) => Subject, selector?: MonoTypeOperatorFunction): Observable; /* tslint:enable:max-line-length */ /** @@ -30,38 +30,5 @@ export function multicast(SubjectFactory: (this: Observable) => Subject */ export function multicast(this: Observable, subjectOrSubjectFactory: Subject | (() => Subject), selector?: (source: Observable) => Observable): Observable | ConnectableObservable { - let subjectFactory: () => Subject; - if (typeof subjectOrSubjectFactory === 'function') { - subjectFactory = <() => Subject>subjectOrSubjectFactory; - } else { - subjectFactory = function subjectFactory() { - return >subjectOrSubjectFactory; - }; - } - - if (typeof selector === 'function') { - return this.lift(new MulticastOperator(subjectFactory, selector)); - } - - const connectable: any = Object.create(this, connectableObservableDescriptor); - connectable.source = this; - connectable.subjectFactory = subjectFactory; - - return > connectable; -} - -export type factoryOrValue = T | (() => T); -export type selector = (source: Observable) => Observable; - -export class MulticastOperator implements Operator { - constructor(private subjectFactory: () => Subject, - private selector: (source: Observable) => Observable) { - } - call(subscriber: Subscriber, source: any): any { - const { selector } = this; - const subject = this.subjectFactory(); - const subscription = selector(subject).subscribe(subscriber); - subscription.add(source.subscribe(subject)); - return subscription; - } + return higherOrder(subjectOrSubjectFactory, selector)(this); } diff --git a/src/operator/observeOn.ts b/src/operator/observeOn.ts index ecbacd9899..cdbef0d03f 100644 --- a/src/operator/observeOn.ts +++ b/src/operator/observeOn.ts @@ -1,11 +1,6 @@ import { Observable } from '../Observable'; import { IScheduler } from '../Scheduler'; -import { Operator } from '../Operator'; -import { PartialObserver } from '../Observer'; -import { Subscriber } from '../Subscriber'; -import { Notification } from '../Notification'; -import { TeardownLogic } from '../Subscription'; -import { Action } from '../scheduler/Action'; +import { observeOn as higherOrder } from '../operators'; /** * @@ -54,59 +49,5 @@ import { Action } from '../scheduler/Action'; * @owner Observable */ export function observeOn(this: Observable, scheduler: IScheduler, delay: number = 0): Observable { - return this.lift(new ObserveOnOperator(scheduler, delay)); -} - -export class ObserveOnOperator implements Operator { - constructor(private scheduler: IScheduler, private delay: number = 0) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class ObserveOnSubscriber extends Subscriber { - static dispatch(this: Action, arg: ObserveOnMessage) { - const { notification, destination } = arg; - notification.observe(destination); - this.unsubscribe(); - } - - constructor(destination: Subscriber, - private scheduler: IScheduler, - private delay: number = 0) { - super(destination); - } - - private scheduleMessage(notification: Notification): void { - this.add(this.scheduler.schedule( - ObserveOnSubscriber.dispatch, - this.delay, - new ObserveOnMessage(notification, this.destination) - )); - } - - protected _next(value: T): void { - this.scheduleMessage(Notification.createNext(value)); - } - - protected _error(err: any): void { - this.scheduleMessage(Notification.createError(err)); - } - - protected _complete(): void { - this.scheduleMessage(Notification.createComplete()); - } -} - -export class ObserveOnMessage { - constructor(public notification: Notification, - public destination: PartialObserver) { - } + return higherOrder(scheduler, delay)(this); } diff --git a/src/operator/publish.ts b/src/operator/publish.ts index a2e8dc1c95..960bc7cac4 100644 --- a/src/operator/publish.ts +++ b/src/operator/publish.ts @@ -1,7 +1,7 @@ -import { Subject } from '../Subject'; + import { Observable } from '../Observable'; -import { multicast } from './multicast'; import { ConnectableObservable } from '../observable/ConnectableObservable'; +import { publish as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function publish(this: Observable): ConnectableObservable; @@ -22,8 +22,7 @@ export function publish(this: Observable, selector: selector): Observab * @owner Observable */ export function publish(this: Observable, selector?: (source: Observable) => Observable): Observable | ConnectableObservable { - return selector ? multicast.call(this, () => new Subject(), selector) : - multicast.call(this, new Subject()); + return higherOrder(selector)(this); } export type selector = (source: Observable) => Observable; diff --git a/src/operator/race.ts b/src/operator/race.ts index a5675f7c7d..f166fbf022 100644 --- a/src/operator/race.ts +++ b/src/operator/race.ts @@ -1,12 +1,8 @@ import { Observable } from '../Observable'; -import { isArray } from '../util/isArray'; -import { ArrayObservable } from '../observable/ArrayObservable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Subscription, TeardownLogic } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { race as higherOrder } from '../operators'; + +// NOTE: to support backwards compatability with 5.4.* and lower +export { race as raceStatic } from '../observable/race'; /* tslint:disable:max-line-length */ export function race(this: Observable, observables: Array>): Observable; @@ -24,102 +20,5 @@ export function race(this: Observable, ...observables: Array(this: Observable, ...observables: Array | Array>>): Observable { - // if the only argument is an array, it was most likely called with - // `pair([obs1, obs2, ...])` - if (observables.length === 1 && isArray(observables[0])) { - observables = >>observables[0]; - } - - return this.lift.call(raceStatic(this, ...observables)); -} - -/** - * Returns an Observable that mirrors the first source Observable to emit an item. - * @param {...Observables} ...observables sources used to race for which Observable emits first. - * @return {Observable} an Observable that mirrors the output of the first Observable to emit an item. - * @static true - * @name race - * @owner Observable - */ -export function raceStatic(observables: Array>): Observable; -export function raceStatic(observables: Array>): Observable; -export function raceStatic(...observables: Array | Array>>): Observable; -export function raceStatic(...observables: Array | Array>>): Observable { - // if the only argument is an array, it was most likely called with - // `race([obs1, obs2, ...])` - if (observables.length === 1) { - if (isArray(observables[0])) { - observables = >>observables[0]; - } else { - return >observables[0]; - } - } - - return new ArrayObservable(observables).lift(new RaceOperator()); -} - -export class RaceOperator implements Operator { - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new RaceSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class RaceSubscriber extends OuterSubscriber { - private hasFirst: boolean = false; - private observables: Observable[] = []; - private subscriptions: Subscription[] = []; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(observable: any): void { - this.observables.push(observable); - } - - protected _complete() { - const observables = this.observables; - const len = observables.length; - - if (len === 0) { - this.destination.complete(); - } else { - for (let i = 0; i < len && !this.hasFirst; i++) { - let observable = observables[i]; - let subscription = subscribeToResult(this, observable, observable, i); - - if (this.subscriptions) { - this.subscriptions.push(subscription); - } - this.add(subscription); - } - this.observables = null; - } - } - - notifyNext(outerValue: T, innerValue: T, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - if (!this.hasFirst) { - this.hasFirst = true; - - for (let i = 0; i < this.subscriptions.length; i++) { - if (i !== outerIndex) { - let subscription = this.subscriptions[i]; - - subscription.unsubscribe(); - this.remove(subscription); - } - } - - this.subscriptions = null; - } - - this.destination.next(innerValue); - } + return higherOrder(...observables)(this); } diff --git a/src/operator/reduce.ts b/src/operator/reduce.ts index 9cbb638781..53d202c5b0 100644 --- a/src/operator/reduce.ts +++ b/src/operator/reduce.ts @@ -1,6 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; +import { reduce as higherOrderReduce } from '../operators'; /* tslint:disable:max-line-length */ export function reduce(this: Observable, accumulator: (acc: T, value: T, index: number) => T, seed?: T): Observable; @@ -53,73 +52,14 @@ export function reduce(this: Observable, accumulator: (acc: R, value: T * @owner Observable */ export function reduce(this: Observable, accumulator: (acc: R, value: T, index?: number) => R, seed?: R): Observable { - let hasSeed = false; // providing a seed of `undefined` *should* be valid and trigger // hasSeed! so don't use `seed !== undefined` checks! // For this reason, we have to check it here at the original call site // otherwise inside Operator/Subscriber we won't know if `undefined` // means they didn't provide anything or if they literally provided `undefined` if (arguments.length >= 2) { - hasSeed = true; + return higherOrderReduce(accumulator, seed)(this); } - return this.lift(new ReduceOperator(accumulator, seed, hasSeed)); -} - -export class ReduceOperator implements Operator { - constructor(private accumulator: (acc: R, value: T, index?: number) => R, private seed?: R, private hasSeed: boolean = false) {} - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ReduceSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class ReduceSubscriber extends Subscriber { - private index: number = 0; - private acc: T | R; - private hasValue: boolean = false; - - constructor(destination: Subscriber, - private accumulator: (acc: R, value: T, index?: number) => R, - seed: R, - private hasSeed: boolean) { - super(destination); - this.acc = seed; - - if (!this.hasSeed) { - this.index++; - } - } - - protected _next(value: T) { - if (this.hasValue || (this.hasValue = this.hasSeed)) { - this._tryReduce(value); - } else { - this.acc = value; - this.hasValue = true; - } - } - - private _tryReduce(value: T) { - let result: any; - try { - result = this.accumulator(this.acc, value, this.index++); - } catch (err) { - this.destination.error(err); - return; - } - this.acc = result; - } - - protected _complete() { - if (this.hasValue || this.hasSeed) { - this.destination.next(this.acc); - } - this.destination.complete(); - } + return higherOrderReduce(accumulator)(this); } diff --git a/src/operator/scan.ts b/src/operator/scan.ts index 558f87667d..c3597f5631 100644 --- a/src/operator/scan.ts +++ b/src/operator/scan.ts @@ -1,6 +1,6 @@ -import { Operator } from '../Operator'; + import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; +import { scan as higherOrderScan } from '../operators'; /* tslint:disable:max-line-length */ export function scan(this: Observable, accumulator: (acc: T, value: T, index: number) => T, seed?: T): Observable; @@ -46,67 +46,8 @@ export function scan(this: Observable, accumulator: (acc: R, value: T, * @owner Observable */ export function scan(this: Observable, accumulator: (acc: R, value: T, index: number) => R, seed?: T | R): Observable { - let hasSeed = false; - // providing a seed of `undefined` *should* be valid and trigger - // hasSeed! so don't use `seed !== undefined` checks! - // For this reason, we have to check it here at the original call site - // otherwise inside Operator/Subscriber we won't know if `undefined` - // means they didn't provide anything or if they literally provided `undefined` if (arguments.length >= 2) { - hasSeed = true; - } - - return this.lift(new ScanOperator(accumulator, seed, hasSeed)); -} - -class ScanOperator implements Operator { - constructor(private accumulator: (acc: R, value: T, index: number) => R, private seed?: T | R, private hasSeed: boolean = false) {} - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ScanSubscriber extends Subscriber { - private index: number = 0; - - get seed(): T | R { - return this._seed; - } - - set seed(value: T | R) { - this.hasSeed = true; - this._seed = value; - } - - constructor(destination: Subscriber, private accumulator: (acc: R, value: T, index: number) => R, private _seed: T | R, - private hasSeed: boolean) { - super(destination); - } - - protected _next(value: T): void { - if (!this.hasSeed) { - this.seed = value; - this.destination.next(value); - } else { - return this._tryNext(value); - } - } - - private _tryNext(value: T): void { - const index = this.index++; - let result: any; - try { - result = this.accumulator(this.seed, value, index); - } catch (err) { - this.destination.error(err); - } - this.seed = result; - this.destination.next(result); + return higherOrderScan(accumulator, seed)(this); } + return higherOrderScan(accumulator)(this); } diff --git a/src/operator/startWith.ts b/src/operator/startWith.ts index e4a33b06d5..88db23e1ef 100644 --- a/src/operator/startWith.ts +++ b/src/operator/startWith.ts @@ -3,7 +3,7 @@ import { Observable } from '../Observable'; import { ArrayObservable } from '../observable/ArrayObservable'; import { ScalarObservable } from '../observable/ScalarObservable'; import { EmptyObservable } from '../observable/EmptyObservable'; -import { concatStatic } from './concat'; +import { concat as concatStatic } from '../observable/concat'; import { isScheduler } from '../util/isScheduler'; /* tslint:disable:max-line-length */ diff --git a/src/operator/subscribeOn.ts b/src/operator/subscribeOn.ts index f68b7a644a..ac0ec35fb4 100644 --- a/src/operator/subscribeOn.ts +++ b/src/operator/subscribeOn.ts @@ -1,9 +1,7 @@ -import { Operator } from '../Operator'; + import { IScheduler } from '../Scheduler'; -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; -import { SubscribeOnObservable } from '../observable/SubscribeOnObservable'; +import { subscribeOn as higherOrder } from '../operators'; /** * Asynchronously subscribes Observers to this Observable on the specified IScheduler. @@ -17,16 +15,5 @@ import { SubscribeOnObservable } from '../observable/SubscribeOnObservable'; * @owner Observable */ export function subscribeOn(this: Observable, scheduler: IScheduler, delay: number = 0): Observable { - return this.lift(new SubscribeOnOperator(scheduler, delay)); -} - -class SubscribeOnOperator implements Operator { - constructor(private scheduler: IScheduler, - private delay: number) { - } - call(subscriber: Subscriber, source: any): TeardownLogic { - return new SubscribeOnObservable( - source, this.delay, this.scheduler - ).subscribe(subscriber); - } + return higherOrder(scheduler, delay)(this); } diff --git a/src/operator/switch.ts b/src/operator/switch.ts index 8e5629fd86..bd8e4ee0bc 100644 --- a/src/operator/switch.ts +++ b/src/operator/switch.ts @@ -1,10 +1,5 @@ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { switchAll as higherOrder } from '../operators'; /** * Converts a higher-order Observable into a first-order Observable by @@ -48,66 +43,6 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @name switch * @owner Observable */ -export function _switch(this: Observable): T { - return this.lift(new SwitchOperator()); -} - -class SwitchOperator implements Operator { - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new SwitchSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SwitchSubscriber extends OuterSubscriber { - private active: number = 0; - private hasCompleted: boolean = false; - innerSubscription: Subscription; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(value: T): void { - this.unsubscribeInner(); - this.active++; - this.add(this.innerSubscription = subscribeToResult(this, value)); - } - - protected _complete(): void { - this.hasCompleted = true; - if (this.active === 0) { - this.destination.complete(); - } - } - - private unsubscribeInner(): void { - this.active = this.active > 0 ? this.active - 1 : 0; - const innerSubscription = this.innerSubscription; - if (innerSubscription) { - innerSubscription.unsubscribe(); - this.remove(innerSubscription); - } - } - - notifyNext(outerValue: T, innerValue: R, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.destination.next(innerValue); - } - - notifyError(err: any): void { - this.destination.error(err); - } - - notifyComplete(): void { - this.unsubscribeInner(); - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - } +export function _switch(this: Observable>): Observable { + return higherOrder()(this); } diff --git a/src/operator/switchMap.ts b/src/operator/switchMap.ts index fd59954437..fa5004503a 100644 --- a/src/operator/switchMap.ts +++ b/src/operator/switchMap.ts @@ -1,10 +1,6 @@ -import { Operator } from '../Operator'; + import { Observable, ObservableInput } from '../Observable'; -import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { switchMap as higherOrderSwitchMap } from '../operators'; /* tslint:disable:max-line-length */ export function switchMap(this: Observable, project: (value: T, index: number) => ObservableInput): Observable; @@ -60,91 +56,5 @@ export function switchMap(this: Observable, project: (value: T, inde */ export function switchMap(this: Observable, project: (value: T, index: number) => ObservableInput, resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): Observable { - return this.lift(new SwitchMapOperator(project, resultSelector)); -} - -class SwitchMapOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new SwitchMapSubscriber(subscriber, this.project, this.resultSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SwitchMapSubscriber extends OuterSubscriber { - private index: number = 0; - private innerSubscription: Subscription; - - constructor(destination: Subscriber, - private project: (value: T, index: number) => ObservableInput, - private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { - super(destination); - } - - protected _next(value: T) { - let result: ObservableInput; - const index = this.index++; - try { - result = this.project(value, index); - } catch (error) { - this.destination.error(error); - return; - } - this._innerSub(result, value, index); - } - - private _innerSub(result: ObservableInput, value: T, index: number) { - const innerSubscription = this.innerSubscription; - if (innerSubscription) { - innerSubscription.unsubscribe(); - } - this.add(this.innerSubscription = subscribeToResult(this, result, value, index)); - } - - protected _complete(): void { - const {innerSubscription} = this; - if (!innerSubscription || innerSubscription.closed) { - super._complete(); - } - } - - protected _unsubscribe() { - this.innerSubscription = null; - } - - notifyComplete(innerSub: Subscription): void { - this.remove(innerSub); - this.innerSubscription = null; - if (this.isStopped) { - super._complete(); - } - } - - notifyNext(outerValue: T, innerValue: I, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - if (this.resultSelector) { - this._tryNotifyNext(outerValue, innerValue, outerIndex, innerIndex); - } else { - this.destination.next(innerValue); - } - } - - private _tryNotifyNext(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): void { - let result: R; - try { - result = this.resultSelector(outerValue, innerValue, outerIndex, innerIndex); - } catch (err) { - this.destination.error(err); - return; - } - this.destination.next(result); - } + return higherOrderSwitchMap(project, resultSelector)(this); } diff --git a/src/operator/takeLast.ts b/src/operator/takeLast.ts index 89490ef808..0af02fb2ec 100644 --- a/src/operator/takeLast.ts +++ b/src/operator/takeLast.ts @@ -1,9 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; -import { EmptyObservable } from '../observable/EmptyObservable'; + import { Observable } from '../Observable'; -import { TeardownLogic } from '../Subscription'; +import { takeLast as higherOrderTakeLast } from '../operators'; /** * Emits only the last `count` values emitted by the source Observable. @@ -42,65 +39,5 @@ import { TeardownLogic } from '../Subscription'; * @owner Observable */ export function takeLast(this: Observable, count: number): Observable { - if (count === 0) { - return new EmptyObservable(); - } else { - return this.lift(new TakeLastOperator(count)); - } -} - -class TakeLastOperator implements Operator { - constructor(private total: number) { - if (this.total < 0) { - throw new ArgumentOutOfRangeError; - } - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new TakeLastSubscriber(subscriber, this.total)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class TakeLastSubscriber extends Subscriber { - private ring: Array = new Array(); - private count: number = 0; - - constructor(destination: Subscriber, private total: number) { - super(destination); - } - - protected _next(value: T): void { - const ring = this.ring; - const total = this.total; - const count = this.count++; - - if (ring.length < total) { - ring.push(value); - } else { - const index = count % total; - ring[index] = value; - } - } - - protected _complete(): void { - const destination = this.destination; - let count = this.count; - - if (count > 0) { - const total = this.count >= this.total ? this.total : this.count; - const ring = this.ring; - - for (let i = 0; i < total; i++) { - const idx = (count++) % total; - destination.next(ring[idx]); - } - } - - destination.complete(); - } + return higherOrderTakeLast(count)(this); } diff --git a/src/operator/timestamp.ts b/src/operator/timestamp.ts index 29676af316..e40fbbbb1e 100644 --- a/src/operator/timestamp.ts +++ b/src/operator/timestamp.ts @@ -1,9 +1,8 @@ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; import { IScheduler } from '../Scheduler'; import { async } from '../scheduler/async'; - +import { timestamp as higherOrder } from '../operators'; +import { Timestamp } from '../operators/timestamp'; /** * @param scheduler * @return {Observable>|WebSocketSubject|Observable} @@ -11,31 +10,5 @@ import { async } from '../scheduler/async'; * @owner Observable */ export function timestamp(this: Observable, scheduler: IScheduler = async): Observable> { - return this.lift(new TimestampOperator(scheduler)); -} - -export class Timestamp { - constructor(public value: T, public timestamp: number) { - } -}; - -class TimestampOperator implements Operator> { - constructor(private scheduler: IScheduler) { - } - - call(observer: Subscriber>, source: any): any { - return source.subscribe(new TimestampSubscriber(observer, this.scheduler)); - } -} - -class TimestampSubscriber extends Subscriber { - constructor(destination: Subscriber>, private scheduler: IScheduler) { - super(destination); - } - - protected _next(value: T): void { - const now = this.scheduler.now(); - - this.destination.next(new Timestamp(value, now)); - } + return higherOrder(scheduler)(this); } diff --git a/src/operator/toArray.ts b/src/operator/toArray.ts index 05f815ad75..78111f9f81 100644 --- a/src/operator/toArray.ts +++ b/src/operator/toArray.ts @@ -1,6 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; + import { Observable } from '../Observable'; +import { toArray as higherOrder } from '../operators'; /** * @return {Observable|WebSocketSubject|Observable} @@ -8,34 +8,5 @@ import { Observable } from '../Observable'; * @owner Observable */ export function toArray(this: Observable): Observable { - return this.lift(new ToArrayOperator()); -} - -class ToArrayOperator implements Operator { - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ToArraySubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ToArraySubscriber extends Subscriber { - - private array: T[] = []; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(x: T) { - this.array.push(x); - } - - protected _complete() { - this.destination.next(this.array); - this.destination.complete(); - } + return higherOrder()(this); } diff --git a/src/operator/window.ts b/src/operator/window.ts index 527fc3dc3f..5f1270818d 100644 --- a/src/operator/window.ts +++ b/src/operator/window.ts @@ -1,11 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { Subject } from '../Subject'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { window as higherOrder } from '../operators'; /** * Branch out the source Observable values as a nested Observable whenever @@ -44,77 +39,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function window(this: Observable, windowBoundaries: Observable): Observable> { - return this.lift(new WindowOperator(windowBoundaries)); -} - -class WindowOperator implements Operator> { - - constructor(private windowBoundaries: Observable) { - } - - call(subscriber: Subscriber>, source: any): any { - const windowSubscriber = new WindowSubscriber(subscriber); - const sourceSubscription = source.subscribe(windowSubscriber); - if (!sourceSubscription.closed) { - windowSubscriber.add(subscribeToResult(windowSubscriber, this.windowBoundaries)); - } - return sourceSubscription; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowSubscriber extends OuterSubscriber { - - private window: Subject = new Subject(); - - constructor(destination: Subscriber>) { - super(destination); - destination.next(this.window); - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.openWindow(); - } - - notifyError(error: any, innerSub: InnerSubscriber): void { - this._error(error); - } - - notifyComplete(innerSub: InnerSubscriber): void { - this._complete(); - } - - protected _next(value: T): void { - this.window.next(value); - } - - protected _error(err: any): void { - this.window.error(err); - this.destination.error(err); - } - - protected _complete(): void { - this.window.complete(); - this.destination.complete(); - } - - protected _unsubscribe() { - this.window = null; - } - - private openWindow(): void { - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - const destination = this.destination; - const newWindow = this.window = new Subject(); - destination.next(newWindow); - } + return higherOrder(windowBoundaries)(this); } diff --git a/src/operator/windowCount.ts b/src/operator/windowCount.ts index a570fb542d..54b0af069f 100644 --- a/src/operator/windowCount.ts +++ b/src/operator/windowCount.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; + import { Observable } from '../Observable'; -import { Subject } from '../Subject'; +import { windowCount as higherOrder } from '../operators'; /** * Branch out the source Observable values as a nested Observable with each @@ -53,79 +52,5 @@ import { Subject } from '../Subject'; */ export function windowCount(this: Observable, windowSize: number, startWindowEvery: number = 0): Observable> { - return this.lift(new WindowCountOperator(windowSize, startWindowEvery)); -} - -class WindowCountOperator implements Operator> { - - constructor(private windowSize: number, - private startWindowEvery: number) { - } - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new WindowCountSubscriber(subscriber, this.windowSize, this.startWindowEvery)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowCountSubscriber extends Subscriber { - private windows: Subject[] = [ new Subject() ]; - private count: number = 0; - - constructor(protected destination: Subscriber>, - private windowSize: number, - private startWindowEvery: number) { - super(destination); - destination.next(this.windows[0]); - } - - protected _next(value: T) { - const startWindowEvery = (this.startWindowEvery > 0) ? this.startWindowEvery : this.windowSize; - const destination = this.destination; - const windowSize = this.windowSize; - const windows = this.windows; - const len = windows.length; - - for (let i = 0; i < len && !this.closed; i++) { - windows[i].next(value); - } - const c = this.count - windowSize + 1; - if (c >= 0 && c % startWindowEvery === 0 && !this.closed) { - windows.shift().complete(); - } - if (++this.count % startWindowEvery === 0 && !this.closed) { - const window = new Subject(); - windows.push(window); - destination.next(window); - } - } - - protected _error(err: any) { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift().error(err); - } - } - this.destination.error(err); - } - - protected _complete() { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift().complete(); - } - } - this.destination.complete(); - } - - protected _unsubscribe() { - this.count = 0; - this.windows = null; - } + return higherOrder(windowSize, startWindowEvery)(this); } diff --git a/src/operator/windowTime.ts b/src/operator/windowTime.ts index a1f9964a5b..88687db490 100644 --- a/src/operator/windowTime.ts +++ b/src/operator/windowTime.ts @@ -1,13 +1,9 @@ import { IScheduler } from '../Scheduler'; -import { Action } from '../scheduler/Action'; -import { Subject } from '../Subject'; -import { Operator } from '../Operator'; import { async } from '../scheduler/async'; -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; import { isNumeric } from '../util/isNumeric'; import { isScheduler } from '../util/isScheduler'; +import { windowTime as higherOrder } from '../operators'; /** * Branch out the source Observable values as a nested Observable periodically @@ -102,160 +98,5 @@ export function windowTime(this: Observable, windowCreationInterval = arguments[1]; } - return this.lift(new WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)); -} - -class WindowTimeOperator implements Operator> { - - constructor(private windowTimeSpan: number, - private windowCreationInterval: number | null, - private maxWindowSize: number, - private scheduler: IScheduler) { - } - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new WindowTimeSubscriber( - subscriber, this.windowTimeSpan, this.windowCreationInterval, this.maxWindowSize, this.scheduler - )); - } -} - -interface CreationState { - windowTimeSpan: number; - windowCreationInterval: number; - subscriber: WindowTimeSubscriber; - scheduler: IScheduler; -} - -interface TimeSpanOnlyState { - window: CountedSubject; - windowTimeSpan: number; - subscriber: WindowTimeSubscriber; - } - -interface CloseWindowContext { - action: Action>; - subscription: Subscription; -} - -interface CloseState { - subscriber: WindowTimeSubscriber; - window: CountedSubject; - context: CloseWindowContext; -} - -class CountedSubject extends Subject { - private _numberOfNextedValues: number = 0; - - next(value?: T): void { - this._numberOfNextedValues++; - super.next(value); - } - - get numberOfNextedValues(): number { - return this._numberOfNextedValues; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowTimeSubscriber extends Subscriber { - private windows: CountedSubject[] = []; - - constructor(protected destination: Subscriber>, - private windowTimeSpan: number, - private windowCreationInterval: number | null, - private maxWindowSize: number, - private scheduler: IScheduler) { - super(destination); - - const window = this.openWindow(); - if (windowCreationInterval !== null && windowCreationInterval >= 0) { - const closeState: CloseState = { subscriber: this, window, context: null }; - const creationState: CreationState = { windowTimeSpan, windowCreationInterval, subscriber: this, scheduler }; - this.add(scheduler.schedule(dispatchWindowClose, windowTimeSpan, closeState)); - this.add(scheduler.schedule(dispatchWindowCreation, windowCreationInterval, creationState)); - } else { - const timeSpanOnlyState: TimeSpanOnlyState = { subscriber: this, window, windowTimeSpan }; - this.add(scheduler.schedule(dispatchWindowTimeSpanOnly, windowTimeSpan, timeSpanOnlyState)); - } - } - - protected _next(value: T): void { - const windows = this.windows; - const len = windows.length; - for (let i = 0; i < len; i++) { - const window = windows[i]; - if (!window.closed) { - window.next(value); - if (window.numberOfNextedValues >= this.maxWindowSize) { - this.closeWindow(window); - } - } - } - } - - protected _error(err: any): void { - const windows = this.windows; - while (windows.length > 0) { - windows.shift().error(err); - } - this.destination.error(err); - } - - protected _complete(): void { - const windows = this.windows; - while (windows.length > 0) { - const window = windows.shift(); - if (!window.closed) { - window.complete(); - } - } - this.destination.complete(); - } - - public openWindow(): CountedSubject { - const window = new CountedSubject(); - this.windows.push(window); - const destination = this.destination; - destination.next(window); - return window; - } - - public closeWindow(window: CountedSubject): void { - window.complete(); - const windows = this.windows; - windows.splice(windows.indexOf(window), 1); - } -} - -function dispatchWindowTimeSpanOnly(this: Action>, state: TimeSpanOnlyState): void { - const { subscriber, windowTimeSpan, window } = state; - if (window) { - subscriber.closeWindow(window); - } - state.window = subscriber.openWindow(); - this.schedule(state, windowTimeSpan); -} - -function dispatchWindowCreation(this: Action>, state: CreationState): void { - const { windowTimeSpan, subscriber, scheduler, windowCreationInterval } = state; - const window = subscriber.openWindow(); - const action = this; - let context: CloseWindowContext = { action, subscription: null }; - const timeSpanState: CloseState = { subscriber, window, context }; - context.subscription = scheduler.schedule(dispatchWindowClose, windowTimeSpan, timeSpanState); - action.add(context.subscription); - action.schedule(state, windowCreationInterval); -} - -function dispatchWindowClose(state: CloseState): void { - const { subscriber, window, context } = state; - if (context && context.action && context.subscription) { - context.action.remove(context.subscription); - } - subscriber.closeWindow(window); + return higherOrder(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)(this); } diff --git a/src/operator/windowToggle.ts b/src/operator/windowToggle.ts index 141634edd8..888d76be64 100644 --- a/src/operator/windowToggle.ts +++ b/src/operator/windowToggle.ts @@ -1,15 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { Subject } from '../Subject'; -import { Subscription } from '../Subscription'; - -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { windowToggle as higherOrder } from '../operators'; /** * Branch out the source Observable values as a nested Observable starting from @@ -54,154 +45,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; */ export function windowToggle(this: Observable, openings: Observable, closingSelector: (openValue: O) => Observable): Observable> { - return this.lift(new WindowToggleOperator(openings, closingSelector)); -} - -class WindowToggleOperator implements Operator> { - - constructor(private openings: Observable, - private closingSelector: (openValue: O) => Observable) { - } - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new WindowToggleSubscriber( - subscriber, this.openings, this.closingSelector - )); - } -} - -interface WindowContext { - window: Subject; - subscription: Subscription; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowToggleSubscriber extends OuterSubscriber { - private contexts: WindowContext[] = []; - private openSubscription: Subscription; - - constructor(destination: Subscriber>, - private openings: Observable, - private closingSelector: (openValue: O) => Observable) { - super(destination); - this.add(this.openSubscription = subscribeToResult(this, openings, openings)); - } - - protected _next(value: T) { - const { contexts } = this; - if (contexts) { - const len = contexts.length; - for (let i = 0; i < len; i++) { - contexts[i].window.next(value); - } - } - } - - protected _error(err: any) { - - const { contexts } = this; - this.contexts = null; - - if (contexts) { - const len = contexts.length; - let index = -1; - - while (++index < len) { - const context = contexts[index]; - context.window.error(err); - context.subscription.unsubscribe(); - } - } - - super._error(err); - } - - protected _complete() { - const { contexts } = this; - this.contexts = null; - if (contexts) { - const len = contexts.length; - let index = -1; - while (++index < len) { - const context = contexts[index]; - context.window.complete(); - context.subscription.unsubscribe(); - } - } - super._complete(); - } - - protected _unsubscribe() { - const { contexts } = this; - this.contexts = null; - if (contexts) { - const len = contexts.length; - let index = -1; - while (++index < len) { - const context = contexts[index]; - context.window.unsubscribe(); - context.subscription.unsubscribe(); - } - } - } - - notifyNext(outerValue: any, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - - if (outerValue === this.openings) { - - const { closingSelector } = this; - const closingNotifier = tryCatch(closingSelector)(innerValue); - - if (closingNotifier === errorObject) { - return this.error(errorObject.e); - } else { - const window = new Subject(); - const subscription = new Subscription(); - const context = { window, subscription }; - this.contexts.push(context); - const innerSubscription = subscribeToResult(this, closingNotifier, context); - - if (innerSubscription.closed) { - this.closeWindow(this.contexts.length - 1); - } else { - ( innerSubscription).context = context; - subscription.add(innerSubscription); - } - - this.destination.next(window); - - } - } else { - this.closeWindow(this.contexts.indexOf(outerValue)); - } - } - - notifyError(err: any): void { - this.error(err); - } - - notifyComplete(inner: Subscription): void { - if (inner !== this.openSubscription) { - this.closeWindow(this.contexts.indexOf(( inner).context)); - } - } - - private closeWindow(index: number): void { - if (index === -1) { - return; - } - - const { contexts } = this; - const context = contexts[index]; - const { window, subscription } = context; - contexts.splice(index, 1); - window.complete(); - subscription.unsubscribe(); - } + return higherOrder(openings, closingSelector)(this); } diff --git a/src/operator/windowWhen.ts b/src/operator/windowWhen.ts index 8c45194da7..9839cd9087 100644 --- a/src/operator/windowWhen.ts +++ b/src/operator/windowWhen.ts @@ -1,15 +1,6 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; -import { Observable } from '../Observable'; -import { Subject } from '../Subject'; -import { Subscription } from '../Subscription'; - -import { tryCatch } from '../util/tryCatch'; -import { errorObject } from '../util/errorObject'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; +import { Observable } from '../Observable'; +import { windowWhen as higherOrder } from '../operators'; /** * Branch out the source Observable values as a nested Observable using a @@ -50,90 +41,5 @@ import { subscribeToResult } from '../util/subscribeToResult'; * @owner Observable */ export function windowWhen(this: Observable, closingSelector: () => Observable): Observable> { - return this.lift(new WindowOperator(closingSelector)); -} - -class WindowOperator implements Operator> { - constructor(private closingSelector: () => Observable) { - } - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe(new WindowSubscriber(subscriber, this.closingSelector)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowSubscriber extends OuterSubscriber { - private window: Subject; - private closingNotification: Subscription; - - constructor(protected destination: Subscriber>, - private closingSelector: () => Observable) { - super(destination); - this.openWindow(); - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.openWindow(innerSub); - } - - notifyError(error: any, innerSub: InnerSubscriber): void { - this._error(error); - } - - notifyComplete(innerSub: InnerSubscriber): void { - this.openWindow(innerSub); - } - - protected _next(value: T): void { - this.window.next(value); - } - - protected _error(err: any): void { - this.window.error(err); - this.destination.error(err); - this.unsubscribeClosingNotification(); - } - - protected _complete(): void { - this.window.complete(); - this.destination.complete(); - this.unsubscribeClosingNotification(); - } - - private unsubscribeClosingNotification(): void { - if (this.closingNotification) { - this.closingNotification.unsubscribe(); - } - } - - private openWindow(innerSub: InnerSubscriber = null): void { - if (innerSub) { - this.remove(innerSub); - innerSub.unsubscribe(); - } - - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - - const window = this.window = new Subject(); - this.destination.next(window); - - const closingNotifier = tryCatch(this.closingSelector)(); - if (closingNotifier === errorObject) { - const err = errorObject.e; - this.destination.error(err); - this.window.error(err); - } else { - this.add(this.closingNotification = subscribeToResult(this, closingNotifier)); - } - } + return higherOrder(closingSelector)(this); } diff --git a/src/operator/zip.ts b/src/operator/zip.ts index afa7089f11..f8297386c2 100644 --- a/src/operator/zip.ts +++ b/src/operator/zip.ts @@ -1,13 +1,5 @@ import { Observable, ObservableInput } from '../Observable'; -import { ArrayObservable } from '../observable/ArrayObservable'; -import { isArray } from '../util/isArray'; -import { Operator } from '../Operator'; -import { PartialObserver } from '../Observer'; -import { Subscriber } from '../Subscriber'; -import { OuterSubscriber } from '../OuterSubscriber'; -import { InnerSubscriber } from '../InnerSubscriber'; -import { subscribeToResult } from '../util/subscribeToResult'; -import { iterator as Symbol_iterator } from '../symbol/iterator'; +import { zip as higherOrder } from '../operators'; /* tslint:disable:max-line-length */ export function zipProto(this: Observable, project: (v1: T) => R): Observable; @@ -33,309 +25,5 @@ export function zipProto(this: Observable, array: Array(this: Observable, ...observables: Array | ((...values: Array) => R)>): Observable { - return this.lift.call(zipStatic(this, ...observables)); -} - -/* tslint:disable:max-line-length */ -export function zipStatic(v1: ObservableInput, v2: ObservableInput): Observable<[T, T2]>; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput): Observable<[T, T2, T3]>; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): Observable<[T, T2, T3, T4]>; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): Observable<[T, T2, T3, T4, T5]>; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): Observable<[T, T2, T3, T4, T5, T6]>; - -export function zipStatic(v1: ObservableInput, project: (v1: T) => R): Observable; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, project: (v1: T, v2: T2) => R): Observable; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, project: (v1: T, v2: T2, v3: T3) => R): Observable; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4) => R): Observable; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5) => R): Observable; -export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6) => R): Observable; - -export function zipStatic(array: ObservableInput[]): Observable; -export function zipStatic(array: ObservableInput[]): Observable; -export function zipStatic(array: ObservableInput[], project: (...values: Array) => R): Observable; -export function zipStatic(array: ObservableInput[], project: (...values: Array) => R): Observable; - -export function zipStatic(...observables: Array>): Observable; -export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable; -export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable; -/* tslint:enable:max-line-length */ - -/** - * Combines multiple Observables to create an Observable whose values are calculated from the values, in order, of each - * of its input Observables. - * - * If the latest parameter is a function, this function is used to compute the created value from the input values. - * Otherwise, an array of the input values is returned. - * - * @example Combine age and name from different sources - * - * let age$ = Observable.of(27, 25, 29); - * let name$ = Observable.of('Foo', 'Bar', 'Beer'); - * let isDev$ = Observable.of(true, true, false); - * - * Observable - * .zip(age$, - * name$, - * isDev$, - * (age: number, name: string, isDev: boolean) => ({ age, name, isDev })) - * .subscribe(x => console.log(x)); - * - * // outputs - * // { age: 27, name: 'Foo', isDev: true } - * // { age: 25, name: 'Bar', isDev: true } - * // { age: 29, name: 'Beer', isDev: false } - * - * @param observables - * @return {Observable} - * @static true - * @name zip - * @owner Observable - */ -export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable { - const project = <((...ys: Array) => R)> observables[observables.length - 1]; - if (typeof project === 'function') { - observables.pop(); - } - return new ArrayObservable(observables).lift(new ZipOperator(project)); -} - -export class ZipOperator implements Operator { - - project: (...values: Array) => R; - - constructor(project?: (...values: Array) => R) { - this.project = project; - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ZipSubscriber(subscriber, this.project)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class ZipSubscriber extends Subscriber { - private values: any; - private project: (...values: Array) => R; - private iterators: LookAheadIterator[] = []; - private active = 0; - - constructor(destination: Subscriber, - project?: (...values: Array) => R, - values: any = Object.create(null)) { - super(destination); - this.project = (typeof project === 'function') ? project : null; - this.values = values; - } - - protected _next(value: any) { - const iterators = this.iterators; - if (isArray(value)) { - iterators.push(new StaticArrayIterator(value)); - } else if (typeof value[Symbol_iterator] === 'function') { - iterators.push(new StaticIterator(value[Symbol_iterator]())); - } else { - iterators.push(new ZipBufferIterator(this.destination, this, value)); - } - } - - protected _complete() { - const iterators = this.iterators; - const len = iterators.length; - - if (len === 0) { - this.destination.complete(); - return; - } - - this.active = len; - for (let i = 0; i < len; i++) { - let iterator: ZipBufferIterator = iterators[i]; - if (iterator.stillUnsubscribed) { - this.add(iterator.subscribe(iterator, i)); - } else { - this.active--; // not an observable - } - } - } - - notifyInactive() { - this.active--; - if (this.active === 0) { - this.destination.complete(); - } - } - - checkIterators() { - const iterators = this.iterators; - const len = iterators.length; - const destination = this.destination; - - // abort if not all of them have values - for (let i = 0; i < len; i++) { - let iterator = iterators[i]; - if (typeof iterator.hasValue === 'function' && !iterator.hasValue()) { - return; - } - } - - let shouldComplete = false; - const args: any[] = []; - for (let i = 0; i < len; i++) { - let iterator = iterators[i]; - let result = iterator.next(); - - // check to see if it's completed now that you've gotten - // the next value. - if (iterator.hasCompleted()) { - shouldComplete = true; - } - - if (result.done) { - destination.complete(); - return; - } - - args.push(result.value); - } - - if (this.project) { - this._tryProject(args); - } else { - destination.next(args); - } - - if (shouldComplete) { - destination.complete(); - } - } - - protected _tryProject(args: any[]) { - let result: any; - try { - result = this.project.apply(this, args); - } catch (err) { - this.destination.error(err); - return; - } - this.destination.next(result); - } -} - -interface LookAheadIterator extends Iterator { - hasValue(): boolean; - hasCompleted(): boolean; -} - -class StaticIterator implements LookAheadIterator { - private nextResult: IteratorResult; - - constructor(private iterator: Iterator) { - this.nextResult = iterator.next(); - } - - hasValue() { - return true; - } - - next(): IteratorResult { - const result = this.nextResult; - this.nextResult = this.iterator.next(); - return result; - } - - hasCompleted() { - const nextResult = this.nextResult; - return nextResult && nextResult.done; - } -} - -class StaticArrayIterator implements LookAheadIterator { - private index = 0; - private length = 0; - - constructor(private array: T[]) { - this.length = array.length; - } - - [Symbol_iterator]() { - return this; - } - - next(value?: any): IteratorResult { - const i = this.index++; - const array = this.array; - return i < this.length ? { value: array[i], done: false } : { value: null, done: true }; - } - - hasValue() { - return this.array.length > this.index; - } - - hasCompleted() { - return this.array.length === this.index; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ZipBufferIterator extends OuterSubscriber implements LookAheadIterator { - stillUnsubscribed = true; - buffer: T[] = []; - isComplete = false; - - constructor(destination: PartialObserver, - private parent: ZipSubscriber, - private observable: Observable) { - super(destination); - } - - [Symbol_iterator]() { - return this; - } - - // NOTE: there is actually a name collision here with Subscriber.next and Iterator.next - // this is legit because `next()` will never be called by a subscription in this case. - next(): IteratorResult { - const buffer = this.buffer; - if (buffer.length === 0 && this.isComplete) { - return { value: null, done: true }; - } else { - return { value: buffer.shift(), done: false }; - } - } - - hasValue() { - return this.buffer.length > 0; - } - - hasCompleted() { - return this.buffer.length === 0 && this.isComplete; - } - - notifyComplete() { - if (this.buffer.length > 0) { - this.isComplete = true; - this.parent.notifyInactive(); - } else { - this.destination.complete(); - } - } - - notifyNext(outerValue: T, innerValue: any, - outerIndex: number, innerIndex: number, - innerSub: InnerSubscriber): void { - this.buffer.push(innerValue); - this.parent.checkIterators(); - } - - subscribe(value: any, index: number) { - return subscribeToResult(this, this.observable, this, index); - } + return higherOrder(...observables)(this); } diff --git a/src/operator/zipAll.ts b/src/operator/zipAll.ts index 5d8849b584..2411dbb574 100644 --- a/src/operator/zipAll.ts +++ b/src/operator/zipAll.ts @@ -1,4 +1,4 @@ -import { ZipOperator } from './zip'; +import { ZipOperator } from '../operators/zip'; import { Observable } from '../Observable'; /** diff --git a/src/operators/audit.ts b/src/operators/audit.ts new file mode 100644 index 0000000000..f2fd270674 --- /dev/null +++ b/src/operators/audit.ts @@ -0,0 +1,122 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { Subscription, TeardownLogic } from '../Subscription'; + +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Ignores source values for a duration determined by another Observable, then + * emits the most recent value from the source Observable, then repeats this + * process. + * + * It's like {@link auditTime}, but the silencing + * duration is determined by a second Observable. + * + * + * + * `audit` is similar to `throttle`, but emits the last value from the silenced + * time window, instead of the first value. `audit` emits the most recent value + * from the source Observable on the output Observable as soon as its internal + * timer becomes disabled, and ignores source values while the timer is enabled. + * Initially, the timer is disabled. As soon as the first source value arrives, + * the timer is enabled by calling the `durationSelector` function with the + * source value, which returns the "duration" Observable. When the duration + * Observable emits a value or completes, the timer is disabled, then the most + * recent source value is emitted on the output Observable, and this process + * repeats for the next source value. + * + * @example Emit clicks at a rate of at most one click per second + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.audit(ev => Rx.Observable.interval(1000)); + * result.subscribe(x => console.log(x)); + * + * @see {@link auditTime} + * @see {@link debounce} + * @see {@link delayWhen} + * @see {@link sample} + * @see {@link throttle} + * + * @param {function(value: T): SubscribableOrPromise} durationSelector A function + * that receives a value from the source Observable, for computing the silencing + * duration, returned as an Observable or a Promise. + * @return {Observable} An Observable that performs rate-limiting of + * emissions from the source Observable. + * @method audit + * @owner Observable + */ +export function audit(durationSelector: (value: T) => SubscribableOrPromise): MonoTypeOperatorFunction { + return function auditOperatorFunction(source: Observable) { + return source.lift(new AuditOperator(durationSelector)); + }; +} + +class AuditOperator implements Operator { + constructor(private durationSelector: (value: T) => SubscribableOrPromise) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new AuditSubscriber(subscriber, this.durationSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class AuditSubscriber extends OuterSubscriber { + + private value: T; + private hasValue: boolean = false; + private throttled: Subscription; + + constructor(destination: Subscriber, + private durationSelector: (value: T) => SubscribableOrPromise) { + super(destination); + } + + protected _next(value: T): void { + this.value = value; + this.hasValue = true; + if (!this.throttled) { + const duration = tryCatch(this.durationSelector)(value); + if (duration === errorObject) { + this.destination.error(errorObject.e); + } else { + const innerSubscription = subscribeToResult(this, duration); + if (innerSubscription.closed) { + this.clearThrottle(); + } else { + this.add(this.throttled = innerSubscription); + } + } + } + } + + clearThrottle() { + const { value, hasValue, throttled } = this; + if (throttled) { + this.remove(throttled); + this.throttled = null; + throttled.unsubscribe(); + } + if (hasValue) { + this.value = null; + this.hasValue = false; + this.destination.next(value); + } + } + + notifyNext(outerValue: T, innerValue: R, outerIndex: number, innerIndex: number): void { + this.clearThrottle(); + } + + notifyComplete(): void { + this.clearThrottle(); + } +} diff --git a/src/operators/auditTime.ts b/src/operators/auditTime.ts new file mode 100644 index 0000000000..c297f16280 --- /dev/null +++ b/src/operators/auditTime.ts @@ -0,0 +1,51 @@ +import { async } from '../scheduler/async'; +import { IScheduler } from '../Scheduler'; +import { audit } from './audit'; +import { timer } from '../observable/timer'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Ignores source values for `duration` milliseconds, then emits the most recent + * value from the source Observable, then repeats this process. + * + * When it sees a source values, it ignores that plus + * the next ones for `duration` milliseconds, and then it emits the most recent + * value from the source. + * + * + * + * `auditTime` is similar to `throttleTime`, but emits the last value from the + * silenced time window, instead of the first value. `auditTime` emits the most + * recent value from the source Observable on the output Observable as soon as + * its internal timer becomes disabled, and ignores source values while the + * timer is enabled. Initially, the timer is disabled. As soon as the first + * source value arrives, the timer is enabled. After `duration` milliseconds (or + * the time unit determined internally by the optional `scheduler`) has passed, + * the timer is disabled, then the most recent source value is emitted on the + * output Observable, and this process repeats for the next source value. + * Optionally takes a {@link IScheduler} for managing timers. + * + * @example Emit clicks at a rate of at most one click per second + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.auditTime(1000); + * result.subscribe(x => console.log(x)); + * + * @see {@link audit} + * @see {@link debounceTime} + * @see {@link delay} + * @see {@link sampleTime} + * @see {@link throttleTime} + * + * @param {number} duration Time to wait before emitting the most recent source + * value, measured in milliseconds or the time unit determined internally + * by the optional `scheduler`. + * @param {Scheduler} [scheduler=async] The {@link IScheduler} to use for + * managing the timers that handle the rate-limiting behavior. + * @return {Observable} An Observable that performs rate-limiting of + * emissions from the source Observable. + * @method auditTime + * @owner Observable + */ +export function auditTime(duration: number, scheduler: IScheduler = async): MonoTypeOperatorFunction { + return audit(() => timer(duration, scheduler)); +} diff --git a/src/operators/buffer.ts b/src/operators/buffer.ts new file mode 100644 index 0000000000..255894d390 --- /dev/null +++ b/src/operators/buffer.ts @@ -0,0 +1,81 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/** + * Buffers the source Observable values until `closingNotifier` emits. + * + * Collects values from the past as an array, and emits + * that array only when another Observable emits. + * + * + * + * Buffers the incoming Observable values until the given `closingNotifier` + * Observable emits a value, at which point it emits the buffer on the output + * Observable and starts a new buffer internally, awaiting the next time + * `closingNotifier` emits. + * + * @example On every click, emit array of most recent interval events + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var interval = Rx.Observable.interval(1000); + * var buffered = interval.buffer(clicks); + * buffered.subscribe(x => console.log(x)); + * + * @see {@link bufferCount} + * @see {@link bufferTime} + * @see {@link bufferToggle} + * @see {@link bufferWhen} + * @see {@link window} + * + * @param {Observable} closingNotifier An Observable that signals the + * buffer to be emitted on the output Observable. + * @return {Observable} An Observable of buffers, which are arrays of + * values. + * @method buffer + * @owner Observable + */ +export function buffer(closingNotifier: Observable): OperatorFunction { + return function bufferOperatorFunction(source: Observable) { + return source.lift(new BufferOperator(closingNotifier)); + }; +} + +class BufferOperator implements Operator { + + constructor(private closingNotifier: Observable) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new BufferSubscriber(subscriber, this.closingNotifier)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferSubscriber extends OuterSubscriber { + private buffer: T[] = []; + + constructor(destination: Subscriber, closingNotifier: Observable) { + super(destination); + this.add(subscribeToResult(this, closingNotifier)); + } + + protected _next(value: T) { + this.buffer.push(value); + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + const buffer = this.buffer; + this.buffer = []; + this.destination.next(buffer); + } +} diff --git a/src/operators/bufferCount.ts b/src/operators/bufferCount.ts new file mode 100644 index 0000000000..94bfb02dda --- /dev/null +++ b/src/operators/bufferCount.ts @@ -0,0 +1,145 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { OperatorFunction } from '../interfaces'; + +/** + * Buffers the source Observable values until the size hits the maximum + * `bufferSize` given. + * + * Collects values from the past as an array, and emits + * that array only when its size reaches `bufferSize`. + * + * + * + * Buffers a number of values from the source Observable by `bufferSize` then + * emits the buffer and clears it, and starts a new buffer each + * `startBufferEvery` values. If `startBufferEvery` is not provided or is + * `null`, then new buffers are started immediately at the start of the source + * and when each buffer closes and is emitted. + * + * @example Emit the last two click events as an array + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var buffered = clicks.bufferCount(2); + * buffered.subscribe(x => console.log(x)); + * + * @example On every click, emit the last two click events as an array + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var buffered = clicks.bufferCount(2, 1); + * buffered.subscribe(x => console.log(x)); + * + * @see {@link buffer} + * @see {@link bufferTime} + * @see {@link bufferToggle} + * @see {@link bufferWhen} + * @see {@link pairwise} + * @see {@link windowCount} + * + * @param {number} bufferSize The maximum size of the buffer emitted. + * @param {number} [startBufferEvery] Interval at which to start a new buffer. + * For example if `startBufferEvery` is `2`, then a new buffer will be started + * on every other value from the source. A new buffer is started at the + * beginning of the source by default. + * @return {Observable} An Observable of arrays of buffered values. + * @method bufferCount + * @owner Observable + */ +export function bufferCount(bufferSize: number, startBufferEvery: number = null): OperatorFunction { + return function bufferCountOperatorFunction(source: Observable) { + return source.lift(new BufferCountOperator(bufferSize, startBufferEvery)); + }; +} + +class BufferCountOperator implements Operator { + private subscriberClass: any; + + constructor(private bufferSize: number, private startBufferEvery: number) { + if (!startBufferEvery || bufferSize === startBufferEvery) { + this.subscriberClass = BufferCountSubscriber; + } else { + this.subscriberClass = BufferSkipCountSubscriber; + } + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new this.subscriberClass(subscriber, this.bufferSize, this.startBufferEvery)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferCountSubscriber extends Subscriber { + private buffer: T[] = []; + + constructor(destination: Subscriber, private bufferSize: number) { + super(destination); + } + + protected _next(value: T): void { + const buffer = this.buffer; + + buffer.push(value); + + if (buffer.length == this.bufferSize) { + this.destination.next(buffer); + this.buffer = []; + } + } + + protected _complete(): void { + const buffer = this.buffer; + if (buffer.length > 0) { + this.destination.next(buffer); + } + super._complete(); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferSkipCountSubscriber extends Subscriber { + private buffers: Array = []; + private count: number = 0; + + constructor(destination: Subscriber, private bufferSize: number, private startBufferEvery: number) { + super(destination); + } + + protected _next(value: T): void { + const { bufferSize, startBufferEvery, buffers, count } = this; + + this.count++; + if (count % startBufferEvery === 0) { + buffers.push([]); + } + + for (let i = buffers.length; i--; ) { + const buffer = buffers[i]; + buffer.push(value); + if (buffer.length === bufferSize) { + buffers.splice(i, 1); + this.destination.next(buffer); + } + } + } + + protected _complete(): void { + const { buffers, destination } = this; + + while (buffers.length > 0) { + let buffer = buffers.shift(); + if (buffer.length > 0) { + destination.next(buffer); + } + } + super._complete(); + } + +} diff --git a/src/operators/bufferTime.ts b/src/operators/bufferTime.ts new file mode 100644 index 0000000000..34c4c44aca --- /dev/null +++ b/src/operators/bufferTime.ts @@ -0,0 +1,237 @@ +import { IScheduler } from '../Scheduler'; +import { Action } from '../scheduler/Action'; +import { Operator } from '../Operator'; +import { async } from '../scheduler/async'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Subscription } from '../Subscription'; +import { isScheduler } from '../util/isScheduler'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function bufferTime(bufferTimeSpan: number, scheduler?: IScheduler): OperatorFunction; +export function bufferTime(bufferTimeSpan: number, bufferCreationInterval: number, scheduler?: IScheduler): OperatorFunction; +export function bufferTime(bufferTimeSpan: number, bufferCreationInterval: number, maxBufferSize: number, scheduler?: IScheduler): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Buffers the source Observable values for a specific time period. + * + * Collects values from the past as an array, and emits + * those arrays periodically in time. + * + * + * + * Buffers values from the source for a specific time duration `bufferTimeSpan`. + * Unless the optional argument `bufferCreationInterval` is given, it emits and + * resets the buffer every `bufferTimeSpan` milliseconds. If + * `bufferCreationInterval` is given, this operator opens the buffer every + * `bufferCreationInterval` milliseconds and closes (emits and resets) the + * buffer every `bufferTimeSpan` milliseconds. When the optional argument + * `maxBufferSize` is specified, the buffer will be closed either after + * `bufferTimeSpan` milliseconds or when it contains `maxBufferSize` elements. + * + * @example Every second, emit an array of the recent click events + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var buffered = clicks.bufferTime(1000); + * buffered.subscribe(x => console.log(x)); + * + * @example Every 5 seconds, emit the click events from the next 2 seconds + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var buffered = clicks.bufferTime(2000, 5000); + * buffered.subscribe(x => console.log(x)); + * + * @see {@link buffer} + * @see {@link bufferCount} + * @see {@link bufferToggle} + * @see {@link bufferWhen} + * @see {@link windowTime} + * + * @param {number} bufferTimeSpan The amount of time to fill each buffer array. + * @param {number} [bufferCreationInterval] The interval at which to start new + * buffers. + * @param {number} [maxBufferSize] The maximum buffer size. + * @param {Scheduler} [scheduler=async] The scheduler on which to schedule the + * intervals that determine buffer boundaries. + * @return {Observable} An observable of arrays of buffered values. + * @method bufferTime + * @owner Observable + */ +export function bufferTime(bufferTimeSpan: number): OperatorFunction { + let length: number = arguments.length; + + let scheduler: IScheduler = async; + if (isScheduler(arguments[arguments.length - 1])) { + scheduler = arguments[arguments.length - 1]; + length--; + } + + let bufferCreationInterval: number = null; + if (length >= 2) { + bufferCreationInterval = arguments[1]; + } + + let maxBufferSize: number = Number.POSITIVE_INFINITY; + if (length >= 3) { + maxBufferSize = arguments[2]; + } + + return function bufferTimeOperatorFunction(source: Observable) { + return source.lift(new BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler)); + }; +} + +class BufferTimeOperator implements Operator { + constructor(private bufferTimeSpan: number, + private bufferCreationInterval: number, + private maxBufferSize: number, + private scheduler: IScheduler) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new BufferTimeSubscriber( + subscriber, this.bufferTimeSpan, this.bufferCreationInterval, this.maxBufferSize, this.scheduler + )); + } +} + +class Context { + buffer: T[] = []; + closeAction: Subscription; +} + +type CreationState = { + bufferTimeSpan: number; + bufferCreationInterval: number, + subscriber: BufferTimeSubscriber; + scheduler: IScheduler; +}; + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferTimeSubscriber extends Subscriber { + private contexts: Array> = []; + private timespanOnly: boolean; + + constructor(destination: Subscriber, + private bufferTimeSpan: number, + private bufferCreationInterval: number, + private maxBufferSize: number, + private scheduler: IScheduler) { + super(destination); + const context = this.openContext(); + this.timespanOnly = bufferCreationInterval == null || bufferCreationInterval < 0; + if (this.timespanOnly) { + const timeSpanOnlyState = { subscriber: this, context, bufferTimeSpan }; + this.add(context.closeAction = scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); + } else { + const closeState = { subscriber: this, context }; + const creationState: CreationState = { bufferTimeSpan, bufferCreationInterval, subscriber: this, scheduler }; + this.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, closeState)); + this.add(scheduler.schedule(dispatchBufferCreation, bufferCreationInterval, creationState)); + } + } + + protected _next(value: T) { + const contexts = this.contexts; + const len = contexts.length; + let filledBufferContext: Context; + for (let i = 0; i < len; i++) { + const context = contexts[i]; + const buffer = context.buffer; + buffer.push(value); + if (buffer.length == this.maxBufferSize) { + filledBufferContext = context; + } + } + + if (filledBufferContext) { + this.onBufferFull(filledBufferContext); + } + } + + protected _error(err: any) { + this.contexts.length = 0; + super._error(err); + } + + protected _complete() { + const { contexts, destination } = this; + while (contexts.length > 0) { + const context = contexts.shift(); + destination.next(context.buffer); + } + super._complete(); + } + + protected _unsubscribe() { + this.contexts = null; + } + + protected onBufferFull(context: Context) { + this.closeContext(context); + const closeAction = context.closeAction; + closeAction.unsubscribe(); + this.remove(closeAction); + + if (!this.closed && this.timespanOnly) { + context = this.openContext(); + const bufferTimeSpan = this.bufferTimeSpan; + const timeSpanOnlyState = { subscriber: this, context, bufferTimeSpan }; + this.add(context.closeAction = this.scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); + } + } + + openContext(): Context { + const context: Context = new Context(); + this.contexts.push(context); + return context; + } + + closeContext(context: Context) { + this.destination.next(context.buffer); + const contexts = this.contexts; + + const spliceIndex = contexts ? contexts.indexOf(context) : -1; + if (spliceIndex >= 0) { + contexts.splice(contexts.indexOf(context), 1); + } + } +} + +function dispatchBufferTimeSpanOnly(this: Action, state: any) { + const subscriber: BufferTimeSubscriber = state.subscriber; + + const prevContext = state.context; + if (prevContext) { + subscriber.closeContext(prevContext); + } + + if (!subscriber.closed) { + state.context = subscriber.openContext(); + state.context.closeAction = this.schedule(state, state.bufferTimeSpan); + } +} + +interface DispatchArg { + subscriber: BufferTimeSubscriber; + context: Context; +} + +function dispatchBufferCreation(this: Action>, state: CreationState) { + const { bufferCreationInterval, bufferTimeSpan, subscriber, scheduler } = state; + const context = subscriber.openContext(); + const action = >>this; + if (!subscriber.closed) { + subscriber.add(context.closeAction = scheduler.schedule>(dispatchBufferClose, bufferTimeSpan, { subscriber, context })); + action.schedule(state, bufferCreationInterval); + } +} + +function dispatchBufferClose(arg: DispatchArg) { + const { subscriber, context } = arg; + subscriber.closeContext(context); +} diff --git a/src/operators/bufferToggle.ts b/src/operators/bufferToggle.ts new file mode 100644 index 0000000000..7839ced9dd --- /dev/null +++ b/src/operators/bufferToggle.ts @@ -0,0 +1,174 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { Subscription } from '../Subscription'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { OperatorFunction } from '../interfaces'; + +/** + * Buffers the source Observable values starting from an emission from + * `openings` and ending when the output of `closingSelector` emits. + * + * Collects values from the past as an array. Starts + * collecting only when `opening` emits, and calls the `closingSelector` + * function to get an Observable that tells when to close the buffer. + * + * + * + * Buffers values from the source by opening the buffer via signals from an + * Observable provided to `openings`, and closing and sending the buffers when + * a Subscribable or Promise returned by the `closingSelector` function emits. + * + * @example Every other second, emit the click events from the next 500ms + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var openings = Rx.Observable.interval(1000); + * var buffered = clicks.bufferToggle(openings, i => + * i % 2 ? Rx.Observable.interval(500) : Rx.Observable.empty() + * ); + * buffered.subscribe(x => console.log(x)); + * + * @see {@link buffer} + * @see {@link bufferCount} + * @see {@link bufferTime} + * @see {@link bufferWhen} + * @see {@link windowToggle} + * + * @param {SubscribableOrPromise} openings A Subscribable or Promise of notifications to start new + * buffers. + * @param {function(value: O): SubscribableOrPromise} closingSelector A function that takes + * the value emitted by the `openings` observable and returns a Subscribable or Promise, + * which, when it emits, signals that the associated buffer should be emitted + * and cleared. + * @return {Observable} An observable of arrays of buffered values. + * @method bufferToggle + * @owner Observable + */ +export function bufferToggle( + openings: SubscribableOrPromise, + closingSelector: (value: O) => SubscribableOrPromise +): OperatorFunction { + return function bufferToggleOperatorFunction(source: Observable) { + return source.lift(new BufferToggleOperator(openings, closingSelector)); + }; +} + +class BufferToggleOperator implements Operator { + + constructor(private openings: SubscribableOrPromise, + private closingSelector: (value: O) => SubscribableOrPromise) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new BufferToggleSubscriber(subscriber, this.openings, this.closingSelector)); + } +} + +interface BufferContext { + buffer: T[]; + subscription: Subscription; +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferToggleSubscriber extends OuterSubscriber { + private contexts: Array> = []; + + constructor(destination: Subscriber, + private openings: SubscribableOrPromise, + private closingSelector: (value: O) => SubscribableOrPromise | void) { + super(destination); + this.add(subscribeToResult(this, openings)); + } + + protected _next(value: T): void { + const contexts = this.contexts; + const len = contexts.length; + for (let i = 0; i < len; i++) { + contexts[i].buffer.push(value); + } + } + + protected _error(err: any): void { + const contexts = this.contexts; + while (contexts.length > 0) { + const context = contexts.shift(); + context.subscription.unsubscribe(); + context.buffer = null; + context.subscription = null; + } + this.contexts = null; + super._error(err); + } + + protected _complete(): void { + const contexts = this.contexts; + while (contexts.length > 0) { + const context = contexts.shift(); + this.destination.next(context.buffer); + context.subscription.unsubscribe(); + context.buffer = null; + context.subscription = null; + } + this.contexts = null; + super._complete(); + } + + notifyNext(outerValue: any, innerValue: O, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + outerValue ? this.closeBuffer(outerValue) : this.openBuffer(innerValue); + } + + notifyComplete(innerSub: InnerSubscriber): void { + this.closeBuffer(( innerSub).context); + } + + private openBuffer(value: O): void { + try { + const closingSelector = this.closingSelector; + const closingNotifier = closingSelector.call(this, value); + if (closingNotifier) { + this.trySubscribe(closingNotifier); + } + } catch (err) { + this._error(err); + } + } + + private closeBuffer(context: BufferContext): void { + const contexts = this.contexts; + + if (contexts && context) { + const { buffer, subscription } = context; + this.destination.next(buffer); + contexts.splice(contexts.indexOf(context), 1); + this.remove(subscription); + subscription.unsubscribe(); + } + } + + private trySubscribe(closingNotifier: any): void { + const contexts = this.contexts; + + const buffer: Array = []; + const subscription = new Subscription(); + const context = { buffer, subscription }; + contexts.push(context); + + const innerSubscription = subscribeToResult(this, closingNotifier, context); + + if (!innerSubscription || innerSubscription.closed) { + this.closeBuffer(context); + } else { + ( innerSubscription).context = context; + + this.add(innerSubscription); + subscription.add(innerSubscription); + } + } +} diff --git a/src/operators/bufferWhen.ts b/src/operators/bufferWhen.ts new file mode 100644 index 0000000000..9ca4a704e8 --- /dev/null +++ b/src/operators/bufferWhen.ts @@ -0,0 +1,136 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subscription } from '../Subscription'; +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/** + * Buffers the source Observable values, using a factory function of closing + * Observables to determine when to close, emit, and reset the buffer. + * + * Collects values from the past as an array. When it + * starts collecting values, it calls a function that returns an Observable that + * tells when to close the buffer and restart collecting. + * + * + * + * Opens a buffer immediately, then closes the buffer when the observable + * returned by calling `closingSelector` function emits a value. When it closes + * the buffer, it immediately opens a new buffer and repeats the process. + * + * @example Emit an array of the last clicks every [1-5] random seconds + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var buffered = clicks.bufferWhen(() => + * Rx.Observable.interval(1000 + Math.random() * 4000) + * ); + * buffered.subscribe(x => console.log(x)); + * + * @see {@link buffer} + * @see {@link bufferCount} + * @see {@link bufferTime} + * @see {@link bufferToggle} + * @see {@link windowWhen} + * + * @param {function(): Observable} closingSelector A function that takes no + * arguments and returns an Observable that signals buffer closure. + * @return {Observable} An observable of arrays of buffered values. + * @method bufferWhen + * @owner Observable + */ +export function bufferWhen(closingSelector: () => Observable): OperatorFunction { + return function (source: Observable) { + return source.lift(new BufferWhenOperator(closingSelector)); + }; +} + +class BufferWhenOperator implements Operator { + + constructor(private closingSelector: () => Observable) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new BufferWhenSubscriber(subscriber, this.closingSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class BufferWhenSubscriber extends OuterSubscriber { + private buffer: T[]; + private subscribing: boolean = false; + private closingSubscription: Subscription; + + constructor(destination: Subscriber, private closingSelector: () => Observable) { + super(destination); + this.openBuffer(); + } + + protected _next(value: T) { + this.buffer.push(value); + } + + protected _complete() { + const buffer = this.buffer; + if (buffer) { + this.destination.next(buffer); + } + super._complete(); + } + + protected _unsubscribe() { + this.buffer = null; + this.subscribing = false; + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.openBuffer(); + } + + notifyComplete(): void { + if (this.subscribing) { + this.complete(); + } else { + this.openBuffer(); + } + } + + openBuffer() { + + let { closingSubscription } = this; + + if (closingSubscription) { + this.remove(closingSubscription); + closingSubscription.unsubscribe(); + } + + const buffer = this.buffer; + if (this.buffer) { + this.destination.next(buffer); + } + + this.buffer = []; + + const closingNotifier = tryCatch(this.closingSelector)(); + + if (closingNotifier === errorObject) { + this.error(errorObject.e); + } else { + closingSubscription = new Subscription(); + this.closingSubscription = closingSubscription; + this.add(closingSubscription); + this.subscribing = true; + closingSubscription.add(subscribeToResult(this, closingNotifier)); + this.subscribing = false; + } + } +} diff --git a/src/operators/catchError.ts b/src/operators/catchError.ts new file mode 100644 index 0000000000..ea9eec5610 --- /dev/null +++ b/src/operators/catchError.ts @@ -0,0 +1,115 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable, ObservableInput } from '../Observable'; + +import { OuterSubscriber } from '../OuterSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/** + * Catches errors on the observable to be handled by returning a new observable or throwing an error. + * + * + * + * @example Continues with a different Observable when there's an error + * + * Observable.of(1, 2, 3, 4, 5) + * .map(n => { + * if (n == 4) { + * throw 'four!'; + * } + * return n; + * }) + * .catch(err => Observable.of('I', 'II', 'III', 'IV', 'V')) + * .subscribe(x => console.log(x)); + * // 1, 2, 3, I, II, III, IV, V + * + * @example Retries the caught source Observable again in case of error, similar to retry() operator + * + * Observable.of(1, 2, 3, 4, 5) + * .map(n => { + * if (n === 4) { + * throw 'four!'; + * } + * return n; + * }) + * .catch((err, caught) => caught) + * .take(30) + * .subscribe(x => console.log(x)); + * // 1, 2, 3, 1, 2, 3, ... + * + * @example Throws a new error when the source Observable throws an error + * + * Observable.of(1, 2, 3, 4, 5) + * .map(n => { + * if (n == 4) { + * throw 'four!'; + * } + * return n; + * }) + * .catch(err => { + * throw 'error in source. Details: ' + err; + * }) + * .subscribe( + * x => console.log(x), + * err => console.log(err) + * ); + * // 1, 2, 3, error in source. Details: four! + * + * @param {function} selector a function that takes as arguments `err`, which is the error, and `caught`, which + * is the source observable, in case you'd like to "retry" that observable by returning it again. Whatever observable + * is returned by the `selector` will be used to continue the observable chain. + * @return {Observable} An observable that originates from either the source or the observable returned by the + * catch `selector` function. + * @name catchError + */ +export function catchError(selector: (err: any, caught: Observable) => ObservableInput): OperatorFunction { + return function catchErrorOperatorFunction(source: Observable): Observable { + const operator = new CatchOperator(selector); + const caught = source.lift(operator); + return (operator.caught = caught); + }; +} + +class CatchOperator implements Operator { + caught: Observable; + + constructor(private selector: (err: any, caught: Observable) => ObservableInput) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new CatchSubscriber(subscriber, this.selector, this.caught)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class CatchSubscriber extends OuterSubscriber { + constructor(destination: Subscriber, + private selector: (err: any, caught: Observable) => ObservableInput, + private caught: Observable) { + super(destination); + } + + // NOTE: overriding `error` instead of `_error` because we don't want + // to have this flag this subscriber as `isStopped`. We can mimic the + // behavior of the RetrySubscriber (from the `retry` operator), where + // we unsubscribe from our source chain, reset our Subscriber flags, + // then subscribe to the selector result. + error(err: any) { + if (!this.isStopped) { + let result: any; + try { + result = this.selector(err, this.caught); + } catch (err2) { + super.error(err2); + return; + } + this._unsubscribeAndRecycle(); + this.add(subscribeToResult(this, result)); + } + } +} diff --git a/src/operators/concat.ts b/src/operators/concat.ts new file mode 100644 index 0000000000..0021e78e32 --- /dev/null +++ b/src/operators/concat.ts @@ -0,0 +1,70 @@ +import { Observable, ObservableInput } from '../Observable'; +import { IScheduler } from '../Scheduler'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; +import { concat as concatStatic } from '../observable/concat'; + +/* tslint:disable:max-line-length */ +export function concat(scheduler?: IScheduler): MonoTypeOperatorFunction; +export function concat(v2: ObservableInput, scheduler?: IScheduler): OperatorFunction; +export function concat(v2: ObservableInput, v3: ObservableInput, scheduler?: IScheduler): OperatorFunction; +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler?: IScheduler): OperatorFunction; +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler?: IScheduler): OperatorFunction; +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler?: IScheduler): OperatorFunction; +export function concat(...observables: Array | IScheduler>): MonoTypeOperatorFunction; +export function concat(...observables: Array | IScheduler>): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Creates an output Observable which sequentially emits all values from every + * given input Observable after the current Observable. + * + * Concatenates multiple Observables together by + * sequentially emitting their values, one Observable after the other. + * + * + * + * Joins this Observable with multiple other Observables by subscribing to them + * one at a time, starting with the source, and merging their results into the + * output Observable. Will wait for each Observable to complete before moving + * on to the next. + * + * @example Concatenate a timer counting from 0 to 3 with a synchronous sequence from 1 to 10 + * var timer = Rx.Observable.interval(1000).take(4); + * var sequence = Rx.Observable.range(1, 10); + * var result = timer.concat(sequence); + * result.subscribe(x => console.log(x)); + * + * // results in: + * // 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 -immediate-> 1 ... 10 + * + * @example Concatenate 3 Observables + * var timer1 = Rx.Observable.interval(1000).take(10); + * var timer2 = Rx.Observable.interval(2000).take(6); + * var timer3 = Rx.Observable.interval(500).take(10); + * var result = timer1.concat(timer2, timer3); + * result.subscribe(x => console.log(x)); + * + * // results in the following: + * // (Prints to console sequentially) + * // -1000ms-> 0 -1000ms-> 1 -1000ms-> ... 9 + * // -2000ms-> 0 -2000ms-> 1 -2000ms-> ... 5 + * // -500ms-> 0 -500ms-> 1 -500ms-> ... 9 + * + * @see {@link concatAll} + * @see {@link concatMap} + * @see {@link concatMapTo} + * + * @param {ObservableInput} other An input Observable to concatenate after the source + * Observable. More than one input Observables may be given as argument. + * @param {Scheduler} [scheduler=null] An optional IScheduler to schedule each + * Observable subscription on. + * @return {Observable} All values of each passed Observable merged into a + * single Observable, in order, in serial fashion. + * @method concat + * @owner Observable + */ +export function concat(...observables: Array | IScheduler>): OperatorFunction { + return function concatOperatorFunction(source: Observable): Observable { + return source.lift.call(concatStatic(source, ...observables)); + }; +} diff --git a/src/operators/concatAll.ts b/src/operators/concatAll.ts new file mode 100644 index 0000000000..22365048b0 --- /dev/null +++ b/src/operators/concatAll.ts @@ -0,0 +1,55 @@ + +import { mergeAll } from './mergeAll'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Converts a higher-order Observable into a first-order Observable by + * concatenating the inner Observables in order. + * + * Flattens an Observable-of-Observables by putting one + * inner Observable after the other. + * + * + * + * Joins every Observable emitted by the source (a higher-order Observable), in + * a serial fashion. It subscribes to each inner Observable only after the + * previous inner Observable has completed, and merges all of their values into + * the returned observable. + * + * __Warning:__ If the source Observable emits Observables quickly and + * endlessly, and the inner Observables it emits generally complete slower than + * the source emits, you can run into memory issues as the incoming Observables + * collect in an unbounded buffer. + * + * Note: `concatAll` is equivalent to `mergeAll` with concurrency parameter set + * to `1`. + * + * @example For each click event, tick every second from 0 to 3, with no concurrency + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var higherOrder = clicks.map(ev => Rx.Observable.interval(1000).take(4)); + * var firstOrder = higherOrder.concatAll(); + * firstOrder.subscribe(x => console.log(x)); + * + * // Results in the following: + * // (results are not concurrent) + * // For every click on the "document" it will emit values 0 to 3 spaced + * // on a 1000ms interval + * // one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 + * + * @see {@link combineAll} + * @see {@link concat} + * @see {@link concatMap} + * @see {@link concatMapTo} + * @see {@link exhaust} + * @see {@link mergeAll} + * @see {@link switch} + * @see {@link zipAll} + * + * @return {Observable} An Observable emitting values from all the inner + * Observables concatenated. + * @method concatAll + * @owner Observable + */ +export function concatAll(): MonoTypeOperatorFunction { + return mergeAll(1); +} diff --git a/src/operators/concatMap.ts b/src/operators/concatMap.ts new file mode 100644 index 0000000000..1ffe4f819e --- /dev/null +++ b/src/operators/concatMap.ts @@ -0,0 +1,72 @@ +import { mergeMap } from './mergeMap'; +import { ObservableInput } from '../Observable'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function concatMap(project: (value: T, index: number) => ObservableInput): OperatorFunction; +export function concatMap(project: (value: T, index: number) => ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Projects each source value to an Observable which is merged in the output + * Observable, in a serialized fashion waiting for each one to complete before + * merging the next. + * + * Maps each value to an Observable, then flattens all of + * these inner Observables using {@link concatAll}. + * + * + * + * Returns an Observable that emits items based on applying a function that you + * supply to each item emitted by the source Observable, where that function + * returns an (so-called "inner") Observable. Each new inner Observable is + * concatenated with the previous inner Observable. + * + * __Warning:__ if source values arrive endlessly and faster than their + * corresponding inner Observables can complete, it will result in memory issues + * as inner Observables amass in an unbounded buffer waiting for their turn to + * be subscribed to. + * + * Note: `concatMap` is equivalent to `mergeMap` with concurrency parameter set + * to `1`. + * + * @example For each click event, tick every second from 0 to 3, with no concurrency + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.concatMap(ev => Rx.Observable.interval(1000).take(4)); + * result.subscribe(x => console.log(x)); + * + * // Results in the following: + * // (results are not concurrent) + * // For every click on the "document" it will emit values 0 to 3 spaced + * // on a 1000ms interval + * // one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 + * + * @see {@link concat} + * @see {@link concatAll} + * @see {@link concatMapTo} + * @see {@link exhaustMap} + * @see {@link mergeMap} + * @see {@link switchMap} + * + * @param {function(value: T, ?index: number): ObservableInput} project A function + * that, when applied to an item emitted by the source Observable, returns an + * Observable. + * @param {function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any} [resultSelector] + * A function to produce the value on the output Observable based on the values + * and the indices of the source (outer) emission and the inner Observable + * emission. The arguments passed to this function are: + * - `outerValue`: the value that came from the source + * - `innerValue`: the value that came from the projected Observable + * - `outerIndex`: the "index" of the value that came from the source + * - `innerIndex`: the "index" of the value from the projected Observable + * @return {Observable} An Observable that emits the result of applying the + * projection function (and the optional `resultSelector`) to each item emitted + * by the source Observable and taking values from each projected inner + * Observable sequentially. + * @method concatMap + * @owner Observable + */ +export function concatMap(project: (value: T, index: number) => ObservableInput, + resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { + return mergeMap(project, resultSelector, 1); +} diff --git a/src/operators/concatMapTo.ts b/src/operators/concatMapTo.ts new file mode 100644 index 0000000000..e92af2495e --- /dev/null +++ b/src/operators/concatMapTo.ts @@ -0,0 +1,71 @@ +import { Observable, ObservableInput } from '../Observable'; +import { concatMap } from './concatMap'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function concatMapTo(observable: ObservableInput): OperatorFunction; +export function concatMapTo(observable: ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Projects each source value to the same Observable which is merged multiple + * times in a serialized fashion on the output Observable. + * + * It's like {@link concatMap}, but maps each value + * always to the same inner Observable. + * + * + * + * Maps each source value to the given Observable `innerObservable` regardless + * of the source value, and then flattens those resulting Observables into one + * single Observable, which is the output Observable. Each new `innerObservable` + * instance emitted on the output Observable is concatenated with the previous + * `innerObservable` instance. + * + * __Warning:__ if source values arrive endlessly and faster than their + * corresponding inner Observables can complete, it will result in memory issues + * as inner Observables amass in an unbounded buffer waiting for their turn to + * be subscribed to. + * + * Note: `concatMapTo` is equivalent to `mergeMapTo` with concurrency parameter + * set to `1`. + * + * @example For each click event, tick every second from 0 to 3, with no concurrency + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.concatMapTo(Rx.Observable.interval(1000).take(4)); + * result.subscribe(x => console.log(x)); + * + * // Results in the following: + * // (results are not concurrent) + * // For every click on the "document" it will emit values 0 to 3 spaced + * // on a 1000ms interval + * // one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 + * + * @see {@link concat} + * @see {@link concatAll} + * @see {@link concatMap} + * @see {@link mergeMapTo} + * @see {@link switchMapTo} + * + * @param {ObservableInput} innerObservable An Observable to replace each value from + * the source Observable. + * @param {function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any} [resultSelector] + * A function to produce the value on the output Observable based on the values + * and the indices of the source (outer) emission and the inner Observable + * emission. The arguments passed to this function are: + * - `outerValue`: the value that came from the source + * - `innerValue`: the value that came from the projected Observable + * - `outerIndex`: the "index" of the value that came from the source + * - `innerIndex`: the "index" of the value from the projected Observable + * @return {Observable} An observable of values merged together by joining the + * passed observable with itself, one after the other, for each value emitted + * from the source. + * @method concatMapTo + * @owner Observable + */ +export function concatMapTo( + innerObservable: Observable, + resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R +): OperatorFunction { + return concatMap(() => innerObservable, resultSelector); +} diff --git a/src/operators/count.ts b/src/operators/count.ts new file mode 100644 index 0000000000..d2b19e4a8c --- /dev/null +++ b/src/operators/count.ts @@ -0,0 +1,111 @@ +import { Observable } from '../Observable'; +import { Operator } from '../Operator'; +import { Observer } from '../Observer'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction } from '../interfaces'; + +/** + * Counts the number of emissions on the source and emits that number when the + * source completes. + * + * Tells how many values were emitted, when the source + * completes. + * + * + * + * `count` transforms an Observable that emits values into an Observable that + * emits a single value that represents the number of values emitted by the + * source Observable. If the source Observable terminates with an error, `count` + * will pass this error notification along without emitting a value first. If + * the source Observable does not terminate at all, `count` will neither emit + * a value nor terminate. This operator takes an optional `predicate` function + * as argument, in which case the output emission will represent the number of + * source values that matched `true` with the `predicate`. + * + * @example Counts how many seconds have passed before the first click happened + * var seconds = Rx.Observable.interval(1000); + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var secondsBeforeClick = seconds.takeUntil(clicks); + * var result = secondsBeforeClick.count(); + * result.subscribe(x => console.log(x)); + * + * @example Counts how many odd numbers are there between 1 and 7 + * var numbers = Rx.Observable.range(1, 7); + * var result = numbers.count(i => i % 2 === 1); + * result.subscribe(x => console.log(x)); + * + * // Results in: + * // 4 + * + * @see {@link max} + * @see {@link min} + * @see {@link reduce} + * + * @param {function(value: T, i: number, source: Observable): boolean} [predicate] A + * boolean function to select what values are to be counted. It is provided with + * arguments of: + * - `value`: the value from the source Observable. + * - `index`: the (zero-based) "index" of the value from the source Observable. + * - `source`: the source Observable instance itself. + * @return {Observable} An Observable of one number that represents the count as + * described above. + * @method count + * @owner Observable + */ +export function count(predicate?: (value: T, index: number, source: Observable) => boolean): OperatorFunction { + return (source: Observable) => source.lift(new CountOperator(predicate, source)); +} + +class CountOperator implements Operator { + constructor(private predicate?: (value: T, index: number, source: Observable) => boolean, + private source?: Observable) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new CountSubscriber(subscriber, this.predicate, this.source)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class CountSubscriber extends Subscriber { + private count: number = 0; + private index: number = 0; + + constructor(destination: Observer, + private predicate?: (value: T, index: number, source: Observable) => boolean, + private source?: Observable) { + super(destination); + } + + protected _next(value: T): void { + if (this.predicate) { + this._tryPredicate(value); + } else { + this.count++; + } + } + + private _tryPredicate(value: T) { + let result: any; + + try { + result = this.predicate(value, this.index++, this.source); + } catch (err) { + this.destination.error(err); + return; + } + + if (result) { + this.count++; + } + } + + protected _complete(): void { + this.destination.next(this.count); + this.destination.complete(); + } +} diff --git a/src/operators/debounce.ts b/src/operators/debounce.ts new file mode 100644 index 0000000000..4634b48247 --- /dev/null +++ b/src/operators/debounce.ts @@ -0,0 +1,137 @@ +import { Operator } from '../Operator'; +import { Observable, SubscribableOrPromise } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Subscription, TeardownLogic } from '../Subscription'; + +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Emits a value from the source Observable only after a particular time span + * determined by another Observable has passed without another source emission. + * + * It's like {@link debounceTime}, but the time span of + * emission silence is determined by a second Observable. + * + * + * + * `debounce` delays values emitted by the source Observable, but drops previous + * pending delayed emissions if a new value arrives on the source Observable. + * This operator keeps track of the most recent value from the source + * Observable, and spawns a duration Observable by calling the + * `durationSelector` function. The value is emitted only when the duration + * Observable emits a value or completes, and if no other value was emitted on + * the source Observable since the duration Observable was spawned. If a new + * value appears before the duration Observable emits, the previous value will + * be dropped and will not be emitted on the output Observable. + * + * Like {@link debounceTime}, this is a rate-limiting operator, and also a + * delay-like operator since output emissions do not necessarily occur at the + * same time as they did on the source Observable. + * + * @example Emit the most recent click after a burst of clicks + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.debounce(() => Rx.Observable.interval(1000)); + * result.subscribe(x => console.log(x)); + * + * @see {@link audit} + * @see {@link debounceTime} + * @see {@link delayWhen} + * @see {@link throttle} + * + * @param {function(value: T): SubscribableOrPromise} durationSelector A function + * that receives a value from the source Observable, for computing the timeout + * duration for each source value, returned as an Observable or a Promise. + * @return {Observable} An Observable that delays the emissions of the source + * Observable by the specified duration Observable returned by + * `durationSelector`, and may drop some values if they occur too frequently. + * @method debounce + * @owner Observable + */ +export function debounce(durationSelector: (value: T) => SubscribableOrPromise): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new DebounceOperator(durationSelector)); +} + +class DebounceOperator implements Operator { + constructor(private durationSelector: (value: T) => SubscribableOrPromise) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DebounceSubscriber(subscriber, this.durationSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DebounceSubscriber extends OuterSubscriber { + private value: T; + private hasValue: boolean = false; + private durationSubscription: Subscription = null; + + constructor(destination: Subscriber, + private durationSelector: (value: T) => SubscribableOrPromise) { + super(destination); + } + + protected _next(value: T): void { + try { + const result = this.durationSelector.call(this, value); + + if (result) { + this._tryNext(value, result); + } + } catch (err) { + this.destination.error(err); + } + } + + protected _complete(): void { + this.emitValue(); + this.destination.complete(); + } + + private _tryNext(value: T, duration: SubscribableOrPromise): void { + let subscription = this.durationSubscription; + this.value = value; + this.hasValue = true; + if (subscription) { + subscription.unsubscribe(); + this.remove(subscription); + } + + subscription = subscribeToResult(this, duration); + if (!subscription.closed) { + this.add(this.durationSubscription = subscription); + } + } + + notifyNext(outerValue: T, innerValue: R, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.emitValue(); + } + + notifyComplete(): void { + this.emitValue(); + } + + emitValue(): void { + if (this.hasValue) { + const value = this.value; + const subscription = this.durationSubscription; + if (subscription) { + this.durationSubscription = null; + subscription.unsubscribe(); + this.remove(subscription); + } + this.value = null; + this.hasValue = false; + super._next(value); + } + } +} diff --git a/src/operators/debounceTime.ts b/src/operators/debounceTime.ts new file mode 100644 index 0000000000..2c8b8075f5 --- /dev/null +++ b/src/operators/debounceTime.ts @@ -0,0 +1,119 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { IScheduler } from '../Scheduler'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { async } from '../scheduler/async'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Emits a value from the source Observable only after a particular time span + * has passed without another source emission. + * + * It's like {@link delay}, but passes only the most + * recent value from each burst of emissions. + * + * + * + * `debounceTime` delays values emitted by the source Observable, but drops + * previous pending delayed emissions if a new value arrives on the source + * Observable. This operator keeps track of the most recent value from the + * source Observable, and emits that only when `dueTime` enough time has passed + * without any other value appearing on the source Observable. If a new value + * appears before `dueTime` silence occurs, the previous value will be dropped + * and will not be emitted on the output Observable. + * + * This is a rate-limiting operator, because it is impossible for more than one + * value to be emitted in any time window of duration `dueTime`, but it is also + * a delay-like operator since output emissions do not occur at the same time as + * they did on the source Observable. Optionally takes a {@link IScheduler} for + * managing timers. + * + * @example Emit the most recent click after a burst of clicks + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.debounceTime(1000); + * result.subscribe(x => console.log(x)); + * + * @see {@link auditTime} + * @see {@link debounce} + * @see {@link delay} + * @see {@link sampleTime} + * @see {@link throttleTime} + * + * @param {number} dueTime The timeout duration in milliseconds (or the time + * unit determined internally by the optional `scheduler`) for the window of + * time required to wait for emission silence before emitting the most recent + * source value. + * @param {Scheduler} [scheduler=async] The {@link IScheduler} to use for + * managing the timers that handle the timeout for each value. + * @return {Observable} An Observable that delays the emissions of the source + * Observable by the specified `dueTime`, and may drop some values if they occur + * too frequently. + * @method debounceTime + * @owner Observable + */ +export function debounceTime(dueTime: number, scheduler: IScheduler = async): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new DebounceTimeOperator(dueTime, scheduler)); +} + +class DebounceTimeOperator implements Operator { + constructor(private dueTime: number, private scheduler: IScheduler) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DebounceTimeSubscriber(subscriber, this.dueTime, this.scheduler)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DebounceTimeSubscriber extends Subscriber { + private debouncedSubscription: Subscription = null; + private lastValue: T = null; + private hasValue: boolean = false; + + constructor(destination: Subscriber, + private dueTime: number, + private scheduler: IScheduler) { + super(destination); + } + + protected _next(value: T) { + this.clearDebounce(); + this.lastValue = value; + this.hasValue = true; + this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext, this.dueTime, this)); + } + + protected _complete() { + this.debouncedNext(); + this.destination.complete(); + } + + debouncedNext(): void { + this.clearDebounce(); + + if (this.hasValue) { + this.destination.next(this.lastValue); + this.lastValue = null; + this.hasValue = false; + } + } + + private clearDebounce(): void { + const debouncedSubscription = this.debouncedSubscription; + + if (debouncedSubscription !== null) { + this.remove(debouncedSubscription); + debouncedSubscription.unsubscribe(); + this.debouncedSubscription = null; + } + } +} + +function dispatchNext(subscriber: DebounceTimeSubscriber) { + subscriber.debouncedNext(); +} diff --git a/src/operators/defaultIfEmpty.ts b/src/operators/defaultIfEmpty.ts new file mode 100644 index 0000000000..5629fc7673 --- /dev/null +++ b/src/operators/defaultIfEmpty.ts @@ -0,0 +1,80 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function defaultIfEmpty(defaultValue?: T): MonoTypeOperatorFunction; +export function defaultIfEmpty(defaultValue?: R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Emits a given value if the source Observable completes without emitting any + * `next` value, otherwise mirrors the source Observable. + * + * If the source Observable turns out to be empty, then + * this operator will emit a default value. + * + * + * + * `defaultIfEmpty` emits the values emitted by the source Observable or a + * specified default value if the source Observable is empty (completes without + * having emitted any `next` value). + * + * @example If no clicks happen in 5 seconds, then emit "no clicks" + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var clicksBeforeFive = clicks.takeUntil(Rx.Observable.interval(5000)); + * var result = clicksBeforeFive.defaultIfEmpty('no clicks'); + * result.subscribe(x => console.log(x)); + * + * @see {@link empty} + * @see {@link last} + * + * @param {any} [defaultValue=null] The default value used if the source + * Observable is empty. + * @return {Observable} An Observable that emits either the specified + * `defaultValue` if the source Observable emits no items, or the values emitted + * by the source Observable. + * @method defaultIfEmpty + * @owner Observable + */ +export function defaultIfEmpty(defaultValue: R = null): OperatorFunction { + return function defaultIfEmptyOperatorFunction(source: Observable) { + return source.lift(new DefaultIfEmptyOperator(defaultValue)); + }; +} + +class DefaultIfEmptyOperator implements Operator { + + constructor(private defaultValue: R) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new DefaultIfEmptySubscriber(subscriber, this.defaultValue)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DefaultIfEmptySubscriber extends Subscriber { + private isEmpty: boolean = true; + + constructor(destination: Subscriber, private defaultValue: R) { + super(destination); + } + + protected _next(value: T): void { + this.isEmpty = false; + this.destination.next(value); + } + + protected _complete(): void { + if (this.isEmpty) { + this.destination.next(this.defaultValue); + } + this.destination.complete(); + } +} \ No newline at end of file diff --git a/src/operators/delay.ts b/src/operators/delay.ts new file mode 100644 index 0000000000..25d21eef12 --- /dev/null +++ b/src/operators/delay.ts @@ -0,0 +1,149 @@ +import { async } from '../scheduler/async'; +import { isDate } from '../util/isDate'; +import { Operator } from '../Operator'; +import { IScheduler } from '../Scheduler'; +import { Subscriber } from '../Subscriber'; +import { Action } from '../scheduler/Action'; +import { Notification } from '../Notification'; +import { Observable } from '../Observable'; +import { PartialObserver } from '../Observer'; +import { TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Delays the emission of items from the source Observable by a given timeout or + * until a given Date. + * + * Time shifts each item by some specified amount of + * milliseconds. + * + * + * + * If the delay argument is a Number, this operator time shifts the source + * Observable by that amount of time expressed in milliseconds. The relative + * time intervals between the values are preserved. + * + * If the delay argument is a Date, this operator time shifts the start of the + * Observable execution until the given date occurs. + * + * @example Delay each click by one second + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var delayedClicks = clicks.delay(1000); // each click emitted after 1 second + * delayedClicks.subscribe(x => console.log(x)); + * + * @example Delay all clicks until a future date happens + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var date = new Date('March 15, 2050 12:00:00'); // in the future + * var delayedClicks = clicks.delay(date); // click emitted only after that date + * delayedClicks.subscribe(x => console.log(x)); + * + * @see {@link debounceTime} + * @see {@link delayWhen} + * + * @param {number|Date} delay The delay duration in milliseconds (a `number`) or + * a `Date` until which the emission of the source items is delayed. + * @param {Scheduler} [scheduler=async] The IScheduler to use for + * managing the timers that handle the time-shift for each item. + * @return {Observable} An Observable that delays the emissions of the source + * Observable by the specified timeout or Date. + * @method delay + * @owner Observable + */ +export function delay(delay: number|Date, + scheduler: IScheduler = async): MonoTypeOperatorFunction { + const absoluteDelay = isDate(delay); + const delayFor = absoluteDelay ? (+delay - scheduler.now()) : Math.abs(delay); + return (source: Observable) => source.lift(new DelayOperator(delayFor, scheduler)); +} + +class DelayOperator implements Operator { + constructor(private delay: number, + private scheduler: IScheduler) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler)); + } +} + +interface DelayState { + source: DelaySubscriber; + destination: PartialObserver; + scheduler: IScheduler; +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DelaySubscriber extends Subscriber { + private queue: Array> = []; + private active: boolean = false; + private errored: boolean = false; + + private static dispatch(this: Action>, state: DelayState): void { + const source = state.source; + const queue = source.queue; + const scheduler = state.scheduler; + const destination = state.destination; + + while (queue.length > 0 && (queue[0].time - scheduler.now()) <= 0) { + queue.shift().notification.observe(destination); + } + + if (queue.length > 0) { + const delay = Math.max(0, queue[0].time - scheduler.now()); + this.schedule(state, delay); + } else { + source.active = false; + } + } + + constructor(destination: Subscriber, + private delay: number, + private scheduler: IScheduler) { + super(destination); + } + + private _schedule(scheduler: IScheduler): void { + this.active = true; + this.add(scheduler.schedule>(DelaySubscriber.dispatch, this.delay, { + source: this, destination: this.destination, scheduler: scheduler + })); + } + + private scheduleNotification(notification: Notification): void { + if (this.errored === true) { + return; + } + + const scheduler = this.scheduler; + const message = new DelayMessage(scheduler.now() + this.delay, notification); + this.queue.push(message); + + if (this.active === false) { + this._schedule(scheduler); + } + } + + protected _next(value: T) { + this.scheduleNotification(Notification.createNext(value)); + } + + protected _error(err: any) { + this.errored = true; + this.queue = []; + this.destination.error(err); + } + + protected _complete() { + this.scheduleNotification(Notification.createComplete()); + } +} + +class DelayMessage { + constructor(public readonly time: number, + public readonly notification: Notification) { + } +} diff --git a/src/operators/delayWhen.ts b/src/operators/delayWhen.ts new file mode 100644 index 0000000000..f9793813ab --- /dev/null +++ b/src/operators/delayWhen.ts @@ -0,0 +1,205 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Delays the emission of items from the source Observable by a given time span + * determined by the emissions of another Observable. + * + * It's like {@link delay}, but the time span of the + * delay duration is determined by a second Observable. + * + * + * + * `delayWhen` time shifts each emitted value from the source Observable by a + * time span determined by another Observable. When the source emits a value, + * the `delayDurationSelector` function is called with the source value as + * argument, and should return an Observable, called the "duration" Observable. + * The source value is emitted on the output Observable only when the duration + * Observable emits a value or completes. + * + * Optionally, `delayWhen` takes a second argument, `subscriptionDelay`, which + * is an Observable. When `subscriptionDelay` emits its first value or + * completes, the source Observable is subscribed to and starts behaving like + * described in the previous paragraph. If `subscriptionDelay` is not provided, + * `delayWhen` will subscribe to the source Observable as soon as the output + * Observable is subscribed. + * + * @example Delay each click by a random amount of time, between 0 and 5 seconds + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var delayedClicks = clicks.delayWhen(event => + * Rx.Observable.interval(Math.random() * 5000) + * ); + * delayedClicks.subscribe(x => console.log(x)); + * + * @see {@link debounce} + * @see {@link delay} + * + * @param {function(value: T): Observable} delayDurationSelector A function that + * returns an Observable for each value emitted by the source Observable, which + * is then used to delay the emission of that item on the output Observable + * until the Observable returned from this function emits a value. + * @param {Observable} subscriptionDelay An Observable that triggers the + * subscription to the source Observable once it emits any value. + * @return {Observable} An Observable that delays the emissions of the source + * Observable by an amount of time specified by the Observable returned by + * `delayDurationSelector`. + * @method delayWhen + * @owner Observable + */ +export function delayWhen(delayDurationSelector: (value: T) => Observable, + subscriptionDelay?: Observable): MonoTypeOperatorFunction { + if (subscriptionDelay) { + return (source: Observable) => + new SubscriptionDelayObservable(source, subscriptionDelay) + .lift(new DelayWhenOperator(delayDurationSelector)); + } + return (source: Observable) => source.lift(new DelayWhenOperator(delayDurationSelector)); +} + +class DelayWhenOperator implements Operator { + constructor(private delayDurationSelector: (value: T) => Observable) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DelayWhenSubscriber(subscriber, this.delayDurationSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DelayWhenSubscriber extends OuterSubscriber { + private completed: boolean = false; + private delayNotifierSubscriptions: Array = []; + private values: Array = []; + + constructor(destination: Subscriber, + private delayDurationSelector: (value: T) => Observable) { + super(destination); + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.destination.next(outerValue); + this.removeSubscription(innerSub); + this.tryComplete(); + } + + notifyError(error: any, innerSub: InnerSubscriber): void { + this._error(error); + } + + notifyComplete(innerSub: InnerSubscriber): void { + const value = this.removeSubscription(innerSub); + if (value) { + this.destination.next(value); + } + this.tryComplete(); + } + + protected _next(value: T): void { + try { + const delayNotifier = this.delayDurationSelector(value); + if (delayNotifier) { + this.tryDelay(delayNotifier, value); + } + } catch (err) { + this.destination.error(err); + } + } + + protected _complete(): void { + this.completed = true; + this.tryComplete(); + } + + private removeSubscription(subscription: InnerSubscriber): T { + subscription.unsubscribe(); + + const subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription); + let value: T = null; + + if (subscriptionIdx !== -1) { + value = this.values[subscriptionIdx]; + this.delayNotifierSubscriptions.splice(subscriptionIdx, 1); + this.values.splice(subscriptionIdx, 1); + } + + return value; + } + + private tryDelay(delayNotifier: Observable, value: T): void { + const notifierSubscription = subscribeToResult(this, delayNotifier, value); + + if (notifierSubscription && !notifierSubscription.closed) { + this.add(notifierSubscription); + this.delayNotifierSubscriptions.push(notifierSubscription); + } + + this.values.push(value); + } + + private tryComplete(): void { + if (this.completed && this.delayNotifierSubscriptions.length === 0) { + this.destination.complete(); + } + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class SubscriptionDelayObservable extends Observable { + constructor(protected source: Observable, private subscriptionDelay: Observable) { + super(); + } + + protected _subscribe(subscriber: Subscriber) { + this.subscriptionDelay.subscribe(new SubscriptionDelaySubscriber(subscriber, this.source)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class SubscriptionDelaySubscriber extends Subscriber { + private sourceSubscribed: boolean = false; + + constructor(private parent: Subscriber, private source: Observable) { + super(); + } + + protected _next(unused: any) { + this.subscribeToSource(); + } + + protected _error(err: any) { + this.unsubscribe(); + this.parent.error(err); + } + + protected _complete() { + this.subscribeToSource(); + } + + private subscribeToSource(): void { + if (!this.sourceSubscribed) { + this.sourceSubscribed = true; + this.unsubscribe(); + this.source.subscribe(this.parent); + } + } +} diff --git a/src/operators/dematerialize.ts b/src/operators/dematerialize.ts new file mode 100644 index 0000000000..3422c7f518 --- /dev/null +++ b/src/operators/dematerialize.ts @@ -0,0 +1,72 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Notification } from '../Notification'; +import { OperatorFunction } from '../interfaces'; + +/** + * Converts an Observable of {@link Notification} objects into the emissions + * that they represent. + * + * Unwraps {@link Notification} objects as actual `next`, + * `error` and `complete` emissions. The opposite of {@link materialize}. + * + * + * + * `dematerialize` is assumed to operate an Observable that only emits + * {@link Notification} objects as `next` emissions, and does not emit any + * `error`. Such Observable is the output of a `materialize` operation. Those + * notifications are then unwrapped using the metadata they contain, and emitted + * as `next`, `error`, and `complete` on the output Observable. + * + * Use this operator in conjunction with {@link materialize}. + * + * @example Convert an Observable of Notifications to an actual Observable + * var notifA = new Rx.Notification('N', 'A'); + * var notifB = new Rx.Notification('N', 'B'); + * var notifE = new Rx.Notification('E', void 0, + * new TypeError('x.toUpperCase is not a function') + * ); + * var materialized = Rx.Observable.of(notifA, notifB, notifE); + * var upperCase = materialized.dematerialize(); + * upperCase.subscribe(x => console.log(x), e => console.error(e)); + * + * // Results in: + * // A + * // B + * // TypeError: x.toUpperCase is not a function + * + * @see {@link Notification} + * @see {@link materialize} + * + * @return {Observable} An Observable that emits items and notifications + * embedded in Notification objects emitted by the source Observable. + * @method dematerialize + * @owner Observable + */ +export function dematerialize(): OperatorFunction, T> { + return function dematerializeOperatorFunction(source: Observable>) { + return source.lift(new DeMaterializeOperator()); + }; +} + +class DeMaterializeOperator, R> implements Operator { + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new DeMaterializeSubscriber(subscriber)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DeMaterializeSubscriber> extends Subscriber { + constructor(destination: Subscriber) { + super(destination); + } + + protected _next(value: T) { + value.observe(this.destination); + } +} diff --git a/src/operators/distinctUntilChanged.ts b/src/operators/distinctUntilChanged.ts new file mode 100644 index 0000000000..7226a0700e --- /dev/null +++ b/src/operators/distinctUntilChanged.ts @@ -0,0 +1,117 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function distinctUntilChanged(compare?: (x: T, y: T) => boolean): MonoTypeOperatorFunction; +export function distinctUntilChanged(compare: (x: K, y: K) => boolean, keySelector: (x: T) => K): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from the previous item. + * + * If a comparator function is provided, then it will be called for each item to test for whether or not that value should be emitted. + * + * If a comparator function is not provided, an equality check is used by default. + * + * @example A simple example with numbers + * Observable.of(1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4) + * .distinctUntilChanged() + * .subscribe(x => console.log(x)); // 1, 2, 1, 2, 3, 4 + * + * @example An example using a compare function + * interface Person { + * age: number, + * name: string + * } + * + * Observable.of( + * { age: 4, name: 'Foo'}, + * { age: 7, name: 'Bar'}, + * { age: 5, name: 'Foo'}) + * { age: 6, name: 'Foo'}) + * .distinctUntilChanged((p: Person, q: Person) => p.name === q.name) + * .subscribe(x => console.log(x)); + * + * // displays: + * // { age: 4, name: 'Foo' } + * // { age: 7, name: 'Bar' } + * // { age: 5, name: 'Foo' } + * + * @see {@link distinct} + * @see {@link distinctUntilKeyChanged} + * + * @param {function} [compare] Optional comparison function called to test if an item is distinct from the previous item in the source. + * @return {Observable} An Observable that emits items from the source Observable with distinct values. + * @method distinctUntilChanged + * @owner Observable + */ +export function distinctUntilChanged(compare?: (x: K, y: K) => boolean, keySelector?: (x: T) => K): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new DistinctUntilChangedOperator(compare, keySelector)); +} + +class DistinctUntilChangedOperator implements Operator { + constructor(private compare: (x: K, y: K) => boolean, + private keySelector: (x: T) => K) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DistinctUntilChangedSubscriber(subscriber, this.compare, this.keySelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DistinctUntilChangedSubscriber extends Subscriber { + private key: K; + private hasKey: boolean = false; + + constructor(destination: Subscriber, + compare: (x: K, y: K) => boolean, + private keySelector: (x: T) => K) { + super(destination); + if (typeof compare === 'function') { + this.compare = compare; + } + } + + private compare(x: any, y: any): boolean { + return x === y; + } + + protected _next(value: T): void { + + const keySelector = this.keySelector; + let key: any = value; + + if (keySelector) { + key = tryCatch(this.keySelector)(value); + if (key === errorObject) { + return this.destination.error(errorObject.e); + } + } + + let result: any = false; + + if (this.hasKey) { + result = tryCatch(this.compare)(this.key, key); + if (result === errorObject) { + return this.destination.error(errorObject.e); + } + } else { + this.hasKey = true; + } + + if (Boolean(result) === false) { + this.key = key; + this.destination.next(value); + } + } +} diff --git a/src/operators/distinctUntilKeyChanged.ts b/src/operators/distinctUntilKeyChanged.ts new file mode 100644 index 0000000000..52ee021275 --- /dev/null +++ b/src/operators/distinctUntilKeyChanged.ts @@ -0,0 +1,68 @@ +import { distinctUntilChanged } from './distinctUntilChanged'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function distinctUntilKeyChanged(key: string): MonoTypeOperatorFunction; +export function distinctUntilKeyChanged(key: string, compare: (x: K, y: K) => boolean): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from the previous item, + * using a property accessed by using the key provided to check if the two items are distinct. + * + * If a comparator function is provided, then it will be called for each item to test for whether or not that value should be emitted. + * + * If a comparator function is not provided, an equality check is used by default. + * + * @example An example comparing the name of persons + * + * interface Person { + * age: number, + * name: string + * } + * + * Observable.of( + * { age: 4, name: 'Foo'}, + * { age: 7, name: 'Bar'}, + * { age: 5, name: 'Foo'}, + * { age: 6, name: 'Foo'}) + * .distinctUntilKeyChanged('name') + * .subscribe(x => console.log(x)); + * + * // displays: + * // { age: 4, name: 'Foo' } + * // { age: 7, name: 'Bar' } + * // { age: 5, name: 'Foo' } + * + * @example An example comparing the first letters of the name + * + * interface Person { + * age: number, + * name: string + * } + * + * Observable.of( + * { age: 4, name: 'Foo1'}, + * { age: 7, name: 'Bar'}, + * { age: 5, name: 'Foo2'}, + * { age: 6, name: 'Foo3'}) + * .distinctUntilKeyChanged('name', (x: string, y: string) => x.substring(0, 3) === y.substring(0, 3)) + * .subscribe(x => console.log(x)); + * + * // displays: + * // { age: 4, name: 'Foo1' } + * // { age: 7, name: 'Bar' } + * // { age: 5, name: 'Foo2' } + * + * @see {@link distinct} + * @see {@link distinctUntilChanged} + * + * @param {string} key String key for object property lookup on each item. + * @param {function} [compare] Optional comparison function called to test if an item is distinct from the previous item in the source. + * @return {Observable} An Observable that emits items from the source Observable with distinct values based on the key specified. + * @method distinctUntilKeyChanged + * @owner Observable + */ +export function distinctUntilKeyChanged(key: string, compare?: (x: T, y: T) => boolean): MonoTypeOperatorFunction { + return distinctUntilChanged((x: T, y: T) => compare ? compare(x[key], y[key]) : x[key] === y[key]); +} diff --git a/src/operators/elementAt.ts b/src/operators/elementAt.ts new file mode 100644 index 0000000000..551544bc02 --- /dev/null +++ b/src/operators/elementAt.ts @@ -0,0 +1,96 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Emits the single value at the specified `index` in a sequence of emissions + * from the source Observable. + * + * Emits only the i-th value, then completes. + * + * + * + * `elementAt` returns an Observable that emits the item at the specified + * `index` in the source Observable, or a default value if that `index` is out + * of range and the `default` argument is provided. If the `default` argument is + * not given and the `index` is out of range, the output Observable will emit an + * `ArgumentOutOfRangeError` error. + * + * @example Emit only the third click event + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.elementAt(2); + * result.subscribe(x => console.log(x)); + * + * // Results in: + * // click 1 = nothing + * // click 2 = nothing + * // click 3 = MouseEvent object logged to console + * + * @see {@link first} + * @see {@link last} + * @see {@link skip} + * @see {@link single} + * @see {@link take} + * + * @throws {ArgumentOutOfRangeError} When using `elementAt(i)`, it delivers an + * ArgumentOutOrRangeError to the Observer's `error` callback if `i < 0` or the + * Observable has completed before emitting the i-th `next` notification. + * + * @param {number} index Is the number `i` for the i-th source emission that has + * happened since the subscription, starting from the number `0`. + * @param {T} [defaultValue] The default value returned for missing indices. + * @return {Observable} An Observable that emits a single item, if it is found. + * Otherwise, will emit the default value if given. If not, then emits an error. + * @method elementAt + * @owner Observable + */ +export function elementAt(index: number, defaultValue?: T): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new ElementAtOperator(index, defaultValue)); +} + +class ElementAtOperator implements Operator { + + constructor(private index: number, private defaultValue?: T) { + if (index < 0) { + throw new ArgumentOutOfRangeError; + } + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new ElementAtSubscriber(subscriber, this.index, this.defaultValue)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class ElementAtSubscriber extends Subscriber { + + constructor(destination: Subscriber, private index: number, private defaultValue?: T) { + super(destination); + } + + protected _next(x: T) { + if (this.index-- === 0) { + this.destination.next(x); + this.destination.complete(); + } + } + + protected _complete() { + const destination = this.destination; + if (this.index >= 0) { + if (typeof this.defaultValue !== 'undefined') { + destination.next(this.defaultValue); + } else { + destination.error(new ArgumentOutOfRangeError); + } + } + destination.complete(); + } +} diff --git a/src/operators/every.ts b/src/operators/every.ts new file mode 100644 index 0000000000..c81497fdb0 --- /dev/null +++ b/src/operators/every.ts @@ -0,0 +1,75 @@ +import { Operator } from '../Operator'; +import { Observer } from '../Observer'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction } from '../interfaces'; + +/** + * Returns an Observable that emits whether or not every item of the source satisfies the condition specified. + * + * @example A simple example emitting true if all elements are less than 5, false otherwise + * Observable.of(1, 2, 3, 4, 5, 6) + * .every(x => x < 5) + * .subscribe(x => console.log(x)); // -> false + * + * @param {function} predicate A function for determining if an item meets a specified condition. + * @param {any} [thisArg] Optional object to use for `this` in the callback. + * @return {Observable} An Observable of booleans that determines if all items of the source Observable meet the condition specified. + * @method every + * @owner Observable + */ +export function every(predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any): OperatorFunction { + return (source: Observable) => source.lift(new EveryOperator(predicate, thisArg, source)); +} + +class EveryOperator implements Operator { + constructor(private predicate: (value: T, index: number, source: Observable) => boolean, + private thisArg?: any, + private source?: Observable) { + } + + call(observer: Subscriber, source: any): any { + return source.subscribe(new EverySubscriber(observer, this.predicate, this.thisArg, this.source)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class EverySubscriber extends Subscriber { + private index: number = 0; + + constructor(destination: Observer, + private predicate: (value: T, index: number, source: Observable) => boolean, + private thisArg: any, + private source?: Observable) { + super(destination); + this.thisArg = thisArg || this; + } + + private notifyComplete(everyValueMatch: boolean): void { + this.destination.next(everyValueMatch); + this.destination.complete(); + } + + protected _next(value: T): void { + let result = false; + try { + result = this.predicate.call(this.thisArg, value, this.index++, this.source); + } catch (err) { + this.destination.error(err); + return; + } + + if (!result) { + this.notifyComplete(false); + } + } + + protected _complete(): void { + this.notifyComplete(true); + } +} diff --git a/src/operators/exhaust.ts b/src/operators/exhaust.ts new file mode 100644 index 0000000000..d684516490 --- /dev/null +++ b/src/operators/exhaust.ts @@ -0,0 +1,88 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Converts a higher-order Observable into a first-order Observable by dropping + * inner Observables while the previous inner Observable has not yet completed. + * + * Flattens an Observable-of-Observables by dropping the + * next inner Observables while the current inner is still executing. + * + * + * + * `exhaust` subscribes to an Observable that emits Observables, also known as a + * higher-order Observable. Each time it observes one of these emitted inner + * Observables, the output Observable begins emitting the items emitted by that + * inner Observable. So far, it behaves like {@link mergeAll}. However, + * `exhaust` ignores every new inner Observable if the previous Observable has + * not yet completed. Once that one completes, it will accept and flatten the + * next inner Observable and repeat this process. + * + * @example Run a finite timer for each click, only if there is no currently active timer + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var higherOrder = clicks.map((ev) => Rx.Observable.interval(1000).take(5)); + * var result = higherOrder.exhaust(); + * result.subscribe(x => console.log(x)); + * + * @see {@link combineAll} + * @see {@link concatAll} + * @see {@link switch} + * @see {@link mergeAll} + * @see {@link exhaustMap} + * @see {@link zipAll} + * + * @return {Observable} An Observable that takes a source of Observables and propagates the first observable + * exclusively until it completes before subscribing to the next. + * @method exhaust + * @owner Observable + */ +export function exhaust(): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new SwitchFirstOperator()); +} + +class SwitchFirstOperator implements Operator { + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new SwitchFirstSubscriber(subscriber)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class SwitchFirstSubscriber extends OuterSubscriber { + private hasCompleted: boolean = false; + private hasSubscription: boolean = false; + + constructor(destination: Subscriber) { + super(destination); + } + + protected _next(value: T): void { + if (!this.hasSubscription) { + this.hasSubscription = true; + this.add(subscribeToResult(this, value)); + } + } + + protected _complete(): void { + this.hasCompleted = true; + if (!this.hasSubscription) { + this.destination.complete(); + } + } + + notifyComplete(innerSub: Subscription): void { + this.remove(innerSub); + this.hasSubscription = false; + if (this.hasCompleted) { + this.destination.complete(); + } + } +} diff --git a/src/operators/exhaustMap.ts b/src/operators/exhaustMap.ts new file mode 100644 index 0000000000..61ca1dcefc --- /dev/null +++ b/src/operators/exhaustMap.ts @@ -0,0 +1,152 @@ +import { Operator } from '../Operator'; +import { Observable, ObservableInput } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Subscription } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function exhaustMap(project: (value: T, index: number) => ObservableInput): OperatorFunction; +export function exhaustMap(project: (value: T, index: number) => ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Projects each source value to an Observable which is merged in the output + * Observable only if the previous projected Observable has completed. + * + * Maps each value to an Observable, then flattens all of + * these inner Observables using {@link exhaust}. + * + * + * + * Returns an Observable that emits items based on applying a function that you + * supply to each item emitted by the source Observable, where that function + * returns an (so-called "inner") Observable. When it projects a source value to + * an Observable, the output Observable begins emitting the items emitted by + * that projected Observable. However, `exhaustMap` ignores every new projected + * Observable if the previous projected Observable has not yet completed. Once + * that one completes, it will accept and flatten the next projected Observable + * and repeat this process. + * + * @example Run a finite timer for each click, only if there is no currently active timer + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.exhaustMap((ev) => Rx.Observable.interval(1000).take(5)); + * result.subscribe(x => console.log(x)); + * + * @see {@link concatMap} + * @see {@link exhaust} + * @see {@link mergeMap} + * @see {@link switchMap} + * + * @param {function(value: T, ?index: number): ObservableInput} project A function + * that, when applied to an item emitted by the source Observable, returns an + * Observable. + * @param {function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any} [resultSelector] + * A function to produce the value on the output Observable based on the values + * and the indices of the source (outer) emission and the inner Observable + * emission. The arguments passed to this function are: + * - `outerValue`: the value that came from the source + * - `innerValue`: the value that came from the projected Observable + * - `outerIndex`: the "index" of the value that came from the source + * - `innerIndex`: the "index" of the value from the projected Observable + * @return {Observable} An Observable containing projected Observables + * of each item of the source, ignoring projected Observables that start before + * their preceding Observable has completed. + * @method exhaustMap + * @owner Observable + */ +export function exhaustMap( + project: (value: T, index: number) => ObservableInput, + resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R + ): OperatorFunction { + return (source: Observable) => source.lift(new SwitchFirstMapOperator(project, resultSelector)); + } + +class SwitchFirstMapOperator implements Operator { + constructor(private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new SwitchFirstMapSubscriber(subscriber, this.project, this.resultSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class SwitchFirstMapSubscriber extends OuterSubscriber { + private hasSubscription: boolean = false; + private hasCompleted: boolean = false; + private index: number = 0; + + constructor(destination: Subscriber, + private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { + super(destination); + } + + protected _next(value: T): void { + if (!this.hasSubscription) { + this.tryNext(value); + } + } + + private tryNext(value: T): void { + const index = this.index++; + const destination = this.destination; + try { + const result = this.project(value, index); + this.hasSubscription = true; + this.add(subscribeToResult(this, result, value, index)); + } catch (err) { + destination.error(err); + } + } + + protected _complete(): void { + this.hasCompleted = true; + if (!this.hasSubscription) { + this.destination.complete(); + } + } + + notifyNext(outerValue: T, innerValue: I, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + const { resultSelector, destination } = this; + if (resultSelector) { + this.trySelectResult(outerValue, innerValue, outerIndex, innerIndex); + } else { + destination.next(innerValue); + } + } + + private trySelectResult(outerValue: T, innerValue: I, + outerIndex: number, innerIndex: number): void { + const { resultSelector, destination } = this; + try { + const result = resultSelector(outerValue, innerValue, outerIndex, innerIndex); + destination.next(result); + } catch (err) { + destination.error(err); + } + } + + notifyError(err: any): void { + this.destination.error(err); + } + + notifyComplete(innerSub: Subscription): void { + this.remove(innerSub); + + this.hasSubscription = false; + if (this.hasCompleted) { + this.destination.complete(); + } + } +} diff --git a/src/operators/expand.ts b/src/operators/expand.ts new file mode 100644 index 0000000000..78a9acbf13 --- /dev/null +++ b/src/operators/expand.ts @@ -0,0 +1,169 @@ +import { Observable } from '../Observable'; +import { IScheduler } from '../Scheduler'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { Subscription } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { MonoTypeOperatorFunction, OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function expand(project: (value: T, index: number) => Observable, concurrent?: number, scheduler?: IScheduler): MonoTypeOperatorFunction; +export function expand(project: (value: T, index: number) => Observable, concurrent?: number, scheduler?: IScheduler): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Recursively projects each source value to an Observable which is merged in + * the output Observable. + * + * It's similar to {@link mergeMap}, but applies the + * projection function to every source value as well as every output value. + * It's recursive. + * + * + * + * Returns an Observable that emits items based on applying a function that you + * supply to each item emitted by the source Observable, where that function + * returns an Observable, and then merging those resulting Observables and + * emitting the results of this merger. *Expand* will re-emit on the output + * Observable every source value. Then, each output value is given to the + * `project` function which returns an inner Observable to be merged on the + * output Observable. Those output values resulting from the projection are also + * given to the `project` function to produce new output values. This is how + * *expand* behaves recursively. + * + * @example Start emitting the powers of two on every click, at most 10 of them + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var powersOfTwo = clicks + * .mapTo(1) + * .expand(x => Rx.Observable.of(2 * x).delay(1000)) + * .take(10); + * powersOfTwo.subscribe(x => console.log(x)); + * + * @see {@link mergeMap} + * @see {@link mergeScan} + * + * @param {function(value: T, index: number) => Observable} project A function + * that, when applied to an item emitted by the source or the output Observable, + * returns an Observable. + * @param {number} [concurrent=Number.POSITIVE_INFINITY] Maximum number of input + * Observables being subscribed to concurrently. + * @param {Scheduler} [scheduler=null] The IScheduler to use for subscribing to + * each projected inner Observable. + * @return {Observable} An Observable that emits the source values and also + * result of applying the projection function to each value emitted on the + * output Observable and and merging the results of the Observables obtained + * from this transformation. + * @method expand + * @owner Observable + */ +export function expand(project: (value: T, index: number) => Observable, + concurrent: number = Number.POSITIVE_INFINITY, + scheduler: IScheduler = undefined): OperatorFunction { + concurrent = (concurrent || 0) < 1 ? Number.POSITIVE_INFINITY : concurrent; + + return (source: Observable) => source.lift(new ExpandOperator(project, concurrent, scheduler)); +} + +export class ExpandOperator implements Operator { + constructor(private project: (value: T, index: number) => Observable, + private concurrent: number, + private scheduler: IScheduler) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new ExpandSubscriber(subscriber, this.project, this.concurrent, this.scheduler)); + } +} + +interface DispatchArg { + subscriber: ExpandSubscriber; + result: Observable; + value: any; + index: number; +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class ExpandSubscriber extends OuterSubscriber { + private index: number = 0; + private active: number = 0; + private hasCompleted: boolean = false; + private buffer: any[]; + + constructor(destination: Subscriber, + private project: (value: T, index: number) => Observable, + private concurrent: number, + private scheduler: IScheduler) { + super(destination); + if (concurrent < Number.POSITIVE_INFINITY) { + this.buffer = []; + } + } + + private static dispatch(arg: DispatchArg): void { + const {subscriber, result, value, index} = arg; + subscriber.subscribeToProjection(result, value, index); + } + + protected _next(value: any): void { + const destination = this.destination; + + if (destination.closed) { + this._complete(); + return; + } + + const index = this.index++; + if (this.active < this.concurrent) { + destination.next(value); + let result = tryCatch(this.project)(value, index); + if (result === errorObject) { + destination.error(errorObject.e); + } else if (!this.scheduler) { + this.subscribeToProjection(result, value, index); + } else { + const state: DispatchArg = { subscriber: this, result, value, index }; + this.add(this.scheduler.schedule(ExpandSubscriber.dispatch, 0, state)); + } + } else { + this.buffer.push(value); + } + } + + private subscribeToProjection(result: any, value: T, index: number): void { + this.active++; + this.add(subscribeToResult(this, result, value, index)); + } + + protected _complete(): void { + this.hasCompleted = true; + if (this.hasCompleted && this.active === 0) { + this.destination.complete(); + } + } + + notifyNext(outerValue: T, innerValue: R, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this._next(innerValue); + } + + notifyComplete(innerSub: Subscription): void { + const buffer = this.buffer; + this.remove(innerSub); + this.active--; + if (buffer && buffer.length > 0) { + this._next(buffer.shift()); + } + if (this.hasCompleted && this.active === 0) { + this.destination.complete(); + } + } +} diff --git a/src/operators/filter.ts b/src/operators/filter.ts new file mode 100644 index 0000000000..5fa064a02f --- /dev/null +++ b/src/operators/filter.ts @@ -0,0 +1,99 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function filter(predicate: (value: T, index: number) => value is S, + thisArg?: any): OperatorFunction; +export function filter(predicate: (value: T, index: number) => boolean, + thisArg?: any): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Filter items emitted by the source Observable by only emitting those that + * satisfy a specified predicate. + * + * Like + * [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), + * it only emits a value from the source if it passes a criterion function. + * + * + * + * Similar to the well-known `Array.prototype.filter` method, this operator + * takes values from the source Observable, passes them through a `predicate` + * function and only emits those values that yielded `true`. + * + * @example Emit only click events whose target was a DIV element + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var clicksOnDivs = clicks.filter(ev => ev.target.tagName === 'DIV'); + * clicksOnDivs.subscribe(x => console.log(x)); + * + * @see {@link distinct} + * @see {@link distinctUntilChanged} + * @see {@link distinctUntilKeyChanged} + * @see {@link ignoreElements} + * @see {@link partition} + * @see {@link skip} + * + * @param {function(value: T, index: number): boolean} predicate A function that + * evaluates each value emitted by the source Observable. If it returns `true`, + * the value is emitted, if `false` the value is not passed to the output + * Observable. The `index` parameter is the number `i` for the i-th source + * emission that has happened since the subscription, starting from the number + * `0`. + * @param {any} [thisArg] An optional argument to determine the value of `this` + * in the `predicate` function. + * @return {Observable} An Observable of values from the source that were + * allowed by the `predicate` function. + * @method filter + * @owner Observable + */ +export function filter(predicate: (value: T, index: number) => boolean, + thisArg?: any): MonoTypeOperatorFunction { + return function filterOperatorFunction(source: Observable): Observable { + return source.lift(new FilterOperator(predicate, thisArg)); + }; +} + +class FilterOperator implements Operator { + constructor(private predicate: (value: T, index: number) => boolean, + private thisArg?: any) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class FilterSubscriber extends Subscriber { + + count: number = 0; + + constructor(destination: Subscriber, + private predicate: (value: T, index: number) => boolean, + private thisArg: any) { + super(destination); + } + + // the try catch block below is left specifically for + // optimization and perf reasons. a tryCatcher is not necessary here. + protected _next(value: T) { + let result: any; + try { + result = this.predicate.call(this.thisArg, value, this.count++); + } catch (err) { + this.destination.error(err); + return; + } + if (result) { + this.destination.next(value); + } + } +} \ No newline at end of file diff --git a/src/operators/finalize.ts b/src/operators/finalize.ts new file mode 100644 index 0000000000..cd5d905591 --- /dev/null +++ b/src/operators/finalize.ts @@ -0,0 +1,38 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { Observable } from '../Observable'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Returns an Observable that mirrors the source Observable, but will call a specified function when + * the source terminates on complete or error. + * @param {function} callback Function to be called when source terminates. + * @return {Observable} An Observable that mirrors the source, but will call the specified function on termination. + * @method finally + * @owner Observable + */ +export function finalize(callback: () => void): MonoTypeOperatorFunction { + return (source: Observable) => source.lift(new FinallyOperator(callback)); +} + +class FinallyOperator implements Operator { + constructor(private callback: () => void) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new FinallySubscriber(subscriber, this.callback)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class FinallySubscriber extends Subscriber { + constructor(destination: Subscriber, callback: () => void) { + super(destination); + this.add(new Subscription(callback)); + } +} diff --git a/src/operators/find.ts b/src/operators/find.ts new file mode 100644 index 0000000000..80fb0b213f --- /dev/null +++ b/src/operators/find.ts @@ -0,0 +1,106 @@ +import { Observable } from '../Observable'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; + +export function find(predicate: (value: T, index: number, source: Observable) => value is S, + thisArg?: any): OperatorFunction; +export function find(predicate: (value: T, index: number) => value is S, + thisArg?: any): OperatorFunction; +export function find(predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any): MonoTypeOperatorFunction; +export function find(predicate: (value: T, index: number) => boolean, + thisArg?: any): MonoTypeOperatorFunction; +/** + * Emits only the first value emitted by the source Observable that meets some + * condition. + * + * Finds the first value that passes some test and emits + * that. + * + * + * + * `find` searches for the first item in the source Observable that matches the + * specified condition embodied by the `predicate`, and returns the first + * occurrence in the source. Unlike {@link first}, the `predicate` is required + * in `find`, and does not emit an error if a valid value is not found. + * + * @example Find and emit the first click that happens on a DIV element + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.find(ev => ev.target.tagName === 'DIV'); + * result.subscribe(x => console.log(x)); + * + * @see {@link filter} + * @see {@link first} + * @see {@link findIndex} + * @see {@link take} + * + * @param {function(value: T, index: number, source: Observable): boolean} predicate + * A function called with each item to test for condition matching. + * @param {any} [thisArg] An optional argument to determine the value of `this` + * in the `predicate` function. + * @return {Observable} An Observable of the first item that matches the + * condition. + * @method find + * @owner Observable + */ +export function find(predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any): MonoTypeOperatorFunction { + if (typeof predicate !== 'function') { + throw new TypeError('predicate is not a function'); + } + return (source: Observable) => source.lift(new FindValueOperator(predicate, source, false, thisArg)); +} + +export class FindValueOperator implements Operator { + constructor(private predicate: (value: T, index: number, source: Observable) => boolean, + private source: Observable, + private yieldIndex: boolean, + private thisArg?: any) { + } + + call(observer: Subscriber, source: any): any { + return source.subscribe(new FindValueSubscriber(observer, this.predicate, this.source, this.yieldIndex, this.thisArg)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class FindValueSubscriber extends Subscriber { + private index: number = 0; + + constructor(destination: Subscriber, + private predicate: (value: T, index: number, source: Observable) => boolean, + private source: Observable, + private yieldIndex: boolean, + private thisArg?: any) { + super(destination); + } + + private notifyComplete(value: any): void { + const destination = this.destination; + + destination.next(value); + destination.complete(); + } + + protected _next(value: T): void { + const { predicate, thisArg } = this; + const index = this.index++; + try { + const result = predicate.call(thisArg || this, value, index, this.source); + if (result) { + this.notifyComplete(this.yieldIndex ? index : value); + } + } catch (err) { + this.destination.error(err); + } + } + + protected _complete(): void { + this.notifyComplete(this.yieldIndex ? -1 : undefined); + } +} diff --git a/src/operators/findIndex.ts b/src/operators/findIndex.ts new file mode 100644 index 0000000000..b33739a064 --- /dev/null +++ b/src/operators/findIndex.ts @@ -0,0 +1,41 @@ +import { Observable } from '../Observable'; +import { FindValueOperator } from '../operators/find'; +import { OperatorFunction } from '../interfaces'; +/** + * Emits only the index of the first value emitted by the source Observable that + * meets some condition. + * + * It's like {@link find}, but emits the index of the + * found value, not the value itself. + * + * + * + * `findIndex` searches for the first item in the source Observable that matches + * the specified condition embodied by the `predicate`, and returns the + * (zero-based) index of the first occurrence in the source. Unlike + * {@link first}, the `predicate` is required in `findIndex`, and does not emit + * an error if a valid value is not found. + * + * @example Emit the index of first click that happens on a DIV element + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.findIndex(ev => ev.target.tagName === 'DIV'); + * result.subscribe(x => console.log(x)); + * + * @see {@link filter} + * @see {@link find} + * @see {@link first} + * @see {@link take} + * + * @param {function(value: T, index: number, source: Observable): boolean} predicate + * A function called with each item to test for condition matching. + * @param {any} [thisArg] An optional argument to determine the value of `this` + * in the `predicate` function. + * @return {Observable} An Observable of the index of the first item that + * matches the condition. + * @method find + * @owner Observable + */ +export function findIndex(predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any): OperatorFunction { + return (source: Observable) => source.lift(new FindValueOperator(predicate, source, true, thisArg)) as Observable; +} diff --git a/src/operators/first.ts b/src/operators/first.ts new file mode 100644 index 0000000000..263b2ded40 --- /dev/null +++ b/src/operators/first.ts @@ -0,0 +1,166 @@ +import { Observable } from '../Observable'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { EmptyError } from '../util/EmptyError'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; +/* tslint:disable:max-line-length */ +export function first(predicate: (value: T, index: number, source: Observable) => value is S): OperatorFunction; +export function first(predicate: (value: T | S, index: number, source: Observable) => value is S, + resultSelector: (value: S, index: number) => R, defaultValue?: R): OperatorFunction; +export function first(predicate: (value: T, index: number, source: Observable) => value is S, + resultSelector: void, + defaultValue?: S): OperatorFunction; +export function first(predicate?: (value: T, index: number, source: Observable) => boolean): MonoTypeOperatorFunction; +export function first(predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector?: (value: T, index: number) => R, + defaultValue?: R): OperatorFunction; +export function first(predicate: (value: T, index: number, source: Observable) => boolean, + resultSelector: void, + defaultValue?: T): MonoTypeOperatorFunction; + +/** + * Emits only the first value (or the first value that meets some condition) + * emitted by the source Observable. + * + * Emits only the first value. Or emits only the first + * value that passes some test. + * + * + * + * If called with no arguments, `first` emits the first value of the source + * Observable, then completes. If called with a `predicate` function, `first` + * emits the first value of the source that matches the specified condition. It + * may also take a `resultSelector` function to produce the output value from + * the input value, and a `defaultValue` to emit in case the source completes + * before it is able to emit a valid value. Throws an error if `defaultValue` + * was not provided and a matching element is not found. + * + * @example Emit only the first click that happens on the DOM + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.first(); + * result.subscribe(x => console.log(x)); + * + * @example Emits the first click that happens on a DIV + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.first(ev => ev.target.tagName === 'DIV'); + * result.subscribe(x => console.log(x)); + * + * @see {@link filter} + * @see {@link find} + * @see {@link take} + * + * @throws {EmptyError} Delivers an EmptyError to the Observer's `error` + * callback if the Observable completes before any `next` notification was sent. + * + * @param {function(value: T, index: number, source: Observable): 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} An Observable of the first item that matches the + * condition. + * @method first + * @owner Observable + */ +export function first(predicate?: (value: T, index: number, source: Observable) => boolean, + resultSelector?: ((value: T, index: number) => R) | void, + defaultValue?: R): OperatorFunction { + return (source: Observable) => source.lift(new FirstOperator(predicate, resultSelector, defaultValue, source)); +} + +class FirstOperator implements Operator { + constructor(private predicate?: (value: T, index: number, source: Observable) => boolean, + private resultSelector?: ((value: T, index: number) => R) | void, + private defaultValue?: any, + private source?: Observable) { + } + + call(observer: Subscriber, source: any): any { + return source.subscribe(new FirstSubscriber(observer, this.predicate, this.resultSelector, this.defaultValue, this.source)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class FirstSubscriber extends Subscriber { + private index: number = 0; + private hasCompleted: boolean = false; + private _emitted: boolean = false; + + constructor(destination: Subscriber, + private predicate?: (value: T, index: number, source: Observable) => boolean, + private resultSelector?: ((value: T, index: number) => R) | void, + private defaultValue?: any, + private source?: Observable) { + super(destination); + } + + protected _next(value: T): void { + const index = this.index++; + if (this.predicate) { + this._tryPredicate(value, index); + } else { + this._emit(value, index); + } + } + + private _tryPredicate(value: T, index: number) { + let result: any; + try { + result = this.predicate(value, index, this.source); + } catch (err) { + this.destination.error(err); + 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 = (this).resultSelector(value, index); + } catch (err) { + this.destination.error(err); + return; + } + this._emitFinal(result); + } + + private _emitFinal(value: any) { + const destination = this.destination; + if (!this._emitted) { + this._emitted = true; + destination.next(value); + destination.complete(); + this.hasCompleted = true; + } + } + + protected _complete(): void { + const destination = this.destination; + if (!this.hasCompleted && typeof this.defaultValue !== 'undefined') { + destination.next(this.defaultValue); + destination.complete(); + } else if (!this.hasCompleted) { + destination.error(new EmptyError); + } + } +} diff --git a/src/operators/groupBy.ts b/src/operators/groupBy.ts new file mode 100644 index 0000000000..3ed28a6966 --- /dev/null +++ b/src/operators/groupBy.ts @@ -0,0 +1,295 @@ +import { Subscriber } from '../Subscriber'; +import { Subscription } from '../Subscription'; +import { Observable } from '../Observable'; +import { Operator } from '../Operator'; +import { Subject } from '../Subject'; +import { Map } from '../util/Map'; +import { FastMap } from '../util/FastMap'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function groupBy(keySelector: (value: T) => K): OperatorFunction>; +export function groupBy(keySelector: (value: T) => K, elementSelector: void, durationSelector: (grouped: GroupedObservable) => Observable): OperatorFunction>; +export function groupBy(keySelector: (value: T) => K, elementSelector?: (value: T) => R, durationSelector?: (grouped: GroupedObservable) => Observable): OperatorFunction>; +export function groupBy(keySelector: (value: T) => K, elementSelector?: (value: T) => R, durationSelector?: (grouped: GroupedObservable) => Observable, subjectSelector?: () => Subject): OperatorFunction>; +/* tslint:enable:max-line-length */ + +/** + * Groups the items emitted by an Observable according to a specified criterion, + * and emits these grouped items as `GroupedObservables`, one + * {@link GroupedObservable} per group. + * + * + * + * @example Group objects by id and return as array + * Observable.of({id: 1, name: 'aze1'}, + * {id: 2, name: 'sf2'}, + * {id: 2, name: 'dg2'}, + * {id: 1, name: 'erg1'}, + * {id: 1, name: 'df1'}, + * {id: 2, name: 'sfqfb2'}, + * {id: 3, name: 'qfs3'}, + * {id: 2, name: 'qsgqsfg2'} + * ) + * .groupBy(p => p.id) + * .flatMap( (group$) => group$.reduce((acc, cur) => [...acc, cur], [])) + * .subscribe(p => console.log(p)); + * + * // displays: + * // [ { id: 1, name: 'aze1' }, + * // { id: 1, name: 'erg1' }, + * // { id: 1, name: 'df1' } ] + * // + * // [ { id: 2, name: 'sf2' }, + * // { id: 2, name: 'dg2' }, + * // { id: 2, name: 'sfqfb2' }, + * // { id: 2, name: 'qsgqsfg2' } ] + * // + * // [ { id: 3, name: 'qfs3' } ] + * + * @example Pivot data on the id field + * Observable.of({id: 1, name: 'aze1'}, + * {id: 2, name: 'sf2'}, + * {id: 2, name: 'dg2'}, + * {id: 1, name: 'erg1'}, + * {id: 1, name: 'df1'}, + * {id: 2, name: 'sfqfb2'}, + * {id: 3, name: 'qfs1'}, + * {id: 2, name: 'qsgqsfg2'} + * ) + * .groupBy(p => p.id, p => p.name) + * .flatMap( (group$) => group$.reduce((acc, cur) => [...acc, cur], ["" + group$.key])) + * .map(arr => ({'id': parseInt(arr[0]), 'values': arr.slice(1)})) + * .subscribe(p => console.log(p)); + * + * // displays: + * // { id: 1, values: [ 'aze1', 'erg1', 'df1' ] } + * // { id: 2, values: [ 'sf2', 'dg2', 'sfqfb2', 'qsgqsfg2' ] } + * // { id: 3, values: [ 'qfs1' ] } + * + * @param {function(value: T): K} keySelector A function that extracts the key + * for each item. + * @param {function(value: T): R} [elementSelector] A function that extracts the + * return element for each item. + * @param {function(grouped: GroupedObservable): Observable} [durationSelector] + * A function that returns an Observable to determine how long each group should + * exist. + * @return {Observable>} An Observable that emits + * GroupedObservables, each of which corresponds to a unique key value and each + * of which emits those items from the source Observable that share that key + * value. + * @method groupBy + * @owner Observable + */ +export function groupBy(keySelector: (value: T) => K, + elementSelector?: ((value: T) => R) | void, + durationSelector?: (grouped: GroupedObservable) => Observable, + subjectSelector?: () => Subject): OperatorFunction> { + return (source: Observable) => + source.lift(new GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector)); +} + +export interface RefCountSubscription { + count: number; + unsubscribe: () => void; + closed: boolean; + attemptedToUnsubscribe: boolean; +} + +class GroupByOperator implements Operator> { + constructor(private keySelector: (value: T) => K, + private elementSelector?: ((value: T) => R) | void, + private durationSelector?: (grouped: GroupedObservable) => Observable, + private subjectSelector?: () => Subject) { + } + + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new GroupBySubscriber( + subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector + )); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class GroupBySubscriber extends Subscriber implements RefCountSubscription { + private groups: Map> = null; + public attemptedToUnsubscribe: boolean = false; + public count: number = 0; + + constructor(destination: Subscriber>, + private keySelector: (value: T) => K, + private elementSelector?: ((value: T) => R) | void, + private durationSelector?: (grouped: GroupedObservable) => Observable, + private subjectSelector?: () => Subject) { + super(destination); + } + + protected _next(value: T): void { + let key: K; + try { + key = this.keySelector(value); + } catch (err) { + this.error(err); + return; + } + + this._group(value, key); + } + + private _group(value: T, key: K) { + let groups = this.groups; + + if (!groups) { + groups = this.groups = typeof key === 'string' ? new FastMap() : new Map(); + } + + let group = groups.get(key); + + let element: R; + if (this.elementSelector) { + try { + element = this.elementSelector(value); + } catch (err) { + this.error(err); + } + } else { + element = value; + } + + if (!group) { + group = this.subjectSelector ? this.subjectSelector() : new Subject(); + groups.set(key, group); + const groupedObservable = new GroupedObservable(key, group, this); + this.destination.next(groupedObservable); + if (this.durationSelector) { + let duration: any; + try { + duration = this.durationSelector(new GroupedObservable(key, >group)); + } catch (err) { + this.error(err); + return; + } + this.add(duration.subscribe(new GroupDurationSubscriber(key, group, this))); + } + } + + if (!group.closed) { + group.next(element); + } + } + + protected _error(err: any): void { + const groups = this.groups; + if (groups) { + groups.forEach((group, key) => { + group.error(err); + }); + + groups.clear(); + } + this.destination.error(err); + } + + protected _complete(): void { + const groups = this.groups; + if (groups) { + groups.forEach((group, key) => { + group.complete(); + }); + + groups.clear(); + } + this.destination.complete(); + } + + removeGroup(key: K): void { + this.groups.delete(key); + } + + unsubscribe() { + if (!this.closed) { + this.attemptedToUnsubscribe = true; + if (this.count === 0) { + super.unsubscribe(); + } + } + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class GroupDurationSubscriber extends Subscriber { + constructor(private key: K, + private group: Subject, + private parent: GroupBySubscriber) { + super(group); + } + + protected _next(value: T): void { + this.complete(); + } + + protected _unsubscribe() { + const { parent, key } = this; + this.key = this.parent = null; + if (parent) { + parent.removeGroup(key); + } + } +} + +/** + * An Observable representing values belonging to the same group represented by + * a common key. The values emitted by a GroupedObservable come from the source + * Observable. The common key is available as the field `key` on a + * GroupedObservable instance. + * + * @class GroupedObservable + */ +export class GroupedObservable extends Observable { + constructor(public key: K, + private groupSubject: Subject, + private refCountSubscription?: RefCountSubscription) { + super(); + } + + protected _subscribe(subscriber: Subscriber) { + const subscription = new Subscription(); + const {refCountSubscription, groupSubject} = this; + if (refCountSubscription && !refCountSubscription.closed) { + subscription.add(new InnerRefCountSubscription(refCountSubscription)); + } + subscription.add(groupSubject.subscribe(subscriber)); + return subscription; + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class InnerRefCountSubscription extends Subscription { + constructor(private parent: RefCountSubscription) { + super(); + parent.count++; + } + + unsubscribe() { + const parent = this.parent; + if (!parent.closed && !this.closed) { + super.unsubscribe(); + parent.count -= 1; + if (parent.count === 0 && parent.attemptedToUnsubscribe) { + parent.unsubscribe(); + } + } + } +} diff --git a/src/operators/ignoreElements.ts b/src/operators/ignoreElements.ts new file mode 100644 index 0000000000..b0f1402f23 --- /dev/null +++ b/src/operators/ignoreElements.ts @@ -0,0 +1,38 @@ +import { Observable } from '../Observable'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { noop } from '../util/noop'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Ignores all items emitted by the source Observable and only passes calls of `complete` or `error`. + * + * + * + * @return {Observable} An empty Observable that only calls `complete` + * or `error`, based on which one is called by the source Observable. + * @method ignoreElements + * @owner Observable + */ +export function ignoreElements(): MonoTypeOperatorFunction { + return function ignoreElementsOperatorFunction(source: Observable) { + return source.lift(new IgnoreElementsOperator()); + }; +} + +class IgnoreElementsOperator implements Operator { + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new IgnoreElementsSubscriber(subscriber)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class IgnoreElementsSubscriber extends Subscriber { + protected _next(unused: T): void { + noop(); + } +} diff --git a/src/operators/index.ts b/src/operators/index.ts new file mode 100644 index 0000000000..84dfabf597 --- /dev/null +++ b/src/operators/index.ts @@ -0,0 +1,59 @@ +export { audit } from './audit'; +export { auditTime } from './auditTime'; +export { buffer } from './buffer'; +export { bufferCount } from './bufferCount'; +export { bufferTime } from './bufferTime'; +export { bufferToggle } from './bufferToggle'; +export { bufferWhen } from './bufferWhen'; +export { catchError } from './catchError'; +export { concat } from './concat'; +export { concatAll } from './concatAll'; +export { concatMap } from './concatMap'; +export { concatMapTo } from './concatMapTo'; +export { count } from './count'; +export { debounce } from './debounce'; +export { debounceTime } from './debounceTime'; +export { defaultIfEmpty } from './defaultIfEmpty'; +export { delay } from './delay'; +export { delayWhen } from './delayWhen'; +export { dematerialize } from './dematerialize'; +export { distinctUntilChanged } from './distinctUntilChanged'; +export { distinctUntilKeyChanged } from './distinctUntilKeyChanged'; +export { elementAt } from './elementAt'; +export { every } from './every'; +export { exhaust } from './exhaust'; +export { exhaustMap } from './exhaustMap'; +export { expand } from './expand'; +export { filter } from './filter'; +export { finalize } from './finalize'; +export { find } from './find'; +export { findIndex } from './findIndex'; +export { first } from './first'; +export { groupBy } from './groupBy'; +export { ignoreElements } from './ignoreElements'; +export { map } from './map'; +export { materialize } from './materialize'; +export { max } from './max'; +export { mergeAll } from './mergeAll'; +export { mergeMap } from './mergeMap'; +export { min } from './min'; +export { multicast } from './multicast'; +export { observeOn } from './observeOn'; +export { publish } from './publish'; +export { race } from './race'; +export { reduce } from './reduce'; +export { refCount } from './refCount'; +export { scan } from './scan'; +export { subscribeOn } from './subscribeOn'; +export { switchAll } from './switchAll'; +export { switchMap } from './switchMap'; +export { takeLast } from './takeLast'; +export { tap } from './tap'; +export { timestamp } from './timestamp'; +export { toArray } from './toArray'; +export { window } from './window'; +export { windowCount } from './windowCount'; +export { windowTime } from './windowTime'; +export { windowToggle } from './windowToggle'; +export { windowWhen } from './windowWhen'; +export { zip } from './zip'; diff --git a/src/operators/map.ts b/src/operators/map.ts new file mode 100644 index 0000000000..07b6b1f15b --- /dev/null +++ b/src/operators/map.ts @@ -0,0 +1,85 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { OperatorFunction } from '../interfaces'; + +/** + * Applies a given `project` function to each value emitted by the source + * Observable, and emits the resulting values as an Observable. + * + * Like [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), + * it passes each source value through a transformation function to get + * corresponding output values. + * + * + * + * Similar to the well known `Array.prototype.map` function, this operator + * applies a projection to each value and emits that projection in the output + * Observable. + * + * @example Map every click to the clientX position of that click + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var positions = clicks.map(ev => ev.clientX); + * positions.subscribe(x => console.log(x)); + * + * @see {@link mapTo} + * @see {@link pluck} + * + * @param {function(value: T, index: number): R} project The function to apply + * to each `value` emitted by the source Observable. The `index` parameter is + * the number `i` for the i-th emission that has happened since the + * subscription, starting from the number `0`. + * @param {any} [thisArg] An optional argument to define what `this` is in the + * `project` function. + * @return {Observable} An Observable that emits the values from the source + * Observable transformed by the given `project` function. + * @method map + * @owner Observable + */ +export function map(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction { + return function mapOperation(source: Observable): Observable { + if (typeof project !== 'function') { + throw new TypeError('argument is not a function. Are you looking for `mapTo()`?'); + } + return source.lift(new MapOperator(project, thisArg)); + }; +} + +export class MapOperator implements Operator { + constructor(private project: (value: T, index: number) => R, private thisArg: any) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new MapSubscriber(subscriber, this.project, this.thisArg)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class MapSubscriber extends Subscriber { + count: number = 0; + private thisArg: any; + + constructor(destination: Subscriber, + private project: (value: T, index: number) => R, + thisArg: any) { + super(destination); + this.thisArg = thisArg || this; + } + + // NOTE: This looks unoptimized, but it's actually purposefully NOT + // using try/catch optimizations. + protected _next(value: T) { + let result: any; + try { + result = this.project.call(this.thisArg, value, this.count++); + } catch (err) { + this.destination.error(err); + return; + } + this.destination.next(result); + } +} diff --git a/src/operators/materialize.ts b/src/operators/materialize.ts new file mode 100644 index 0000000000..82f71e4088 --- /dev/null +++ b/src/operators/materialize.ts @@ -0,0 +1,88 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Notification } from '../Notification'; +import { OperatorFunction } from '../interfaces'; + +/** + * Represents all of the notifications from the source Observable as `next` + * emissions marked with their original types within {@link Notification} + * objects. + * + * Wraps `next`, `error` and `complete` emissions in + * {@link Notification} objects, emitted as `next` on the output Observable. + * + * + * + * + * `materialize` returns an Observable that emits a `next` notification for each + * `next`, `error`, or `complete` emission of the source Observable. When the + * source Observable emits `complete`, the output Observable will emit `next` as + * a Notification of type "complete", and then it will emit `complete` as well. + * When the source Observable emits `error`, the output will emit `next` as a + * Notification of type "error", and then `complete`. + * + * This operator is useful for producing metadata of the source Observable, to + * be consumed as `next` emissions. Use it in conjunction with + * {@link dematerialize}. + * + * @example Convert a faulty Observable to an Observable of Notifications + * var letters = Rx.Observable.of('a', 'b', 13, 'd'); + * var upperCase = letters.map(x => x.toUpperCase()); + * var materialized = upperCase.materialize(); + * materialized.subscribe(x => console.log(x)); + * + * // Results in the following: + * // - Notification {kind: "N", value: "A", error: undefined, hasValue: true} + * // - Notification {kind: "N", value: "B", error: undefined, hasValue: true} + * // - Notification {kind: "E", value: undefined, error: TypeError: + * // x.toUpperCase is not a function at MapSubscriber.letters.map.x + * // [as project] (http://1…, hasValue: false} + * + * @see {@link Notification} + * @see {@link dematerialize} + * + * @return {Observable>} An Observable that emits + * {@link Notification} objects that wrap the original emissions from the source + * Observable with metadata. + * @method materialize + * @owner Observable + */ +export function materialize(): OperatorFunction> { + return function materializeOperatorFunction(source: Observable) { + return source.lift(new MaterializeOperator()); + }; +} + +class MaterializeOperator implements Operator> { + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new MaterializeSubscriber(subscriber)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class MaterializeSubscriber extends Subscriber { + constructor(destination: Subscriber>) { + super(destination); + } + + protected _next(value: T) { + this.destination.next(Notification.createNext(value)); + } + + protected _error(err: any) { + const destination = this.destination; + destination.next(Notification.createError(err)); + destination.complete(); + } + + protected _complete() { + const destination = this.destination; + destination.next(Notification.createComplete()); + destination.complete(); + } +} diff --git a/src/operators/max.ts b/src/operators/max.ts new file mode 100644 index 0000000000..0b8b9bafcd --- /dev/null +++ b/src/operators/max.ts @@ -0,0 +1,41 @@ +import { reduce } from './reduce'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * The Max operator operates on an Observable that emits numbers (or items that can be compared with a provided function), + * and when source Observable completes it emits a single item: the item with the largest value. + * + * + * + * @example Get the maximal value of a series of numbers + * Rx.Observable.of(5, 4, 7, 2, 8) + * .max() + * .subscribe(x => console.log(x)); // -> 8 + * + * @example Use a comparer function to get the maximal item + * interface Person { + * age: number, + * name: string + * } + * Observable.of({age: 7, name: 'Foo'}, + * {age: 5, name: 'Bar'}, + * {age: 9, name: 'Beer'}) + * .max((a: Person, b: Person) => a.age < b.age ? -1 : 1) + * .subscribe((x: Person) => console.log(x.name)); // -> 'Beer' + * } + * + * @see {@link min} + * + * @param {Function} [comparer] - Optional comparer function that it will use instead of its default to compare the + * value of two items. + * @return {Observable} An Observable that emits item with the largest value. + * @method max + * @owner Observable + */ +export function max(comparer?: (x: T, y: T) => number): MonoTypeOperatorFunction { + const max: (x: T, y: T) => T = (typeof comparer === 'function') + ? (x, y) => comparer(x, y) > 0 ? x : y + : (x, y) => x > y ? x : y; + + return reduce(max); +} diff --git a/src/operators/mergeAll.ts b/src/operators/mergeAll.ts new file mode 100644 index 0000000000..ad018ef85a --- /dev/null +++ b/src/operators/mergeAll.ts @@ -0,0 +1,52 @@ + +import { mergeMap } from './mergeMap'; +import { identity } from '../util/identity'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Converts a higher-order Observable into a first-order Observable which + * concurrently delivers all values that are emitted on the inner Observables. + * + * Flattens an Observable-of-Observables. + * + * + * + * `mergeAll` subscribes to an Observable that emits Observables, also known as + * a higher-order Observable. Each time it observes one of these emitted inner + * Observables, it subscribes to that and delivers all the values from the + * inner Observable on the output Observable. The output Observable only + * completes once all inner Observables have completed. Any error delivered by + * a inner Observable will be immediately emitted on the output Observable. + * + * @example Spawn a new interval Observable for each click event, and blend their outputs as one Observable + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var higherOrder = clicks.map((ev) => Rx.Observable.interval(1000)); + * var firstOrder = higherOrder.mergeAll(); + * firstOrder.subscribe(x => console.log(x)); + * + * @example Count from 0 to 9 every second for each click, but only allow 2 concurrent timers + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var higherOrder = clicks.map((ev) => Rx.Observable.interval(1000).take(10)); + * var firstOrder = higherOrder.mergeAll(2); + * firstOrder.subscribe(x => console.log(x)); + * + * @see {@link combineAll} + * @see {@link concatAll} + * @see {@link exhaust} + * @see {@link merge} + * @see {@link mergeMap} + * @see {@link mergeMapTo} + * @see {@link mergeScan} + * @see {@link switch} + * @see {@link zipAll} + * + * @param {number} [concurrent=Number.POSITIVE_INFINITY] Maximum number of inner + * Observables being subscribed to concurrently. + * @return {Observable} An Observable that emits values coming from all the + * inner Observables emitted by the source Observable. + * @method mergeAll + * @owner Observable + */ +export function mergeAll(concurrent: number = Number.POSITIVE_INFINITY): MonoTypeOperatorFunction { + return mergeMap(identity, null, concurrent); +} \ No newline at end of file diff --git a/src/operators/mergeMap.ts b/src/operators/mergeMap.ts new file mode 100644 index 0000000000..e64634936d --- /dev/null +++ b/src/operators/mergeMap.ts @@ -0,0 +1,179 @@ +import { Observable, ObservableInput } from '../Observable'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Subscription } from '../Subscription'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function mergeMap(project: (value: T, index: number) => ObservableInput, concurrent?: number): OperatorFunction; +export function mergeMap(project: (value: T, index: number) => ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R, concurrent?: number): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Projects each source value to an Observable which is merged in the output + * Observable. + * + * Maps each value to an Observable, then flattens all of + * these inner Observables using {@link mergeAll}. + * + * + * + * Returns an Observable that emits items based on applying a function that you + * supply to each item emitted by the source Observable, where that function + * returns an Observable, and then merging those resulting Observables and + * emitting the results of this merger. + * + * @example Map and flatten each letter to an Observable ticking every 1 second + * var letters = Rx.Observable.of('a', 'b', 'c'); + * var result = letters.mergeMap(x => + * Rx.Observable.interval(1000).map(i => x+i) + * ); + * result.subscribe(x => console.log(x)); + * + * // Results in the following: + * // a0 + * // b0 + * // c0 + * // a1 + * // b1 + * // c1 + * // continues to list a,b,c with respective ascending integers + * + * @see {@link concatMap} + * @see {@link exhaustMap} + * @see {@link merge} + * @see {@link mergeAll} + * @see {@link mergeMapTo} + * @see {@link mergeScan} + * @see {@link switchMap} + * + * @param {function(value: T, ?index: number): ObservableInput} project A function + * that, when applied to an item emitted by the source Observable, returns an + * Observable. + * @param {function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any} [resultSelector] + * A function to produce the value on the output Observable based on the values + * and the indices of the source (outer) emission and the inner Observable + * emission. The arguments passed to this function are: + * - `outerValue`: the value that came from the source + * - `innerValue`: the value that came from the projected Observable + * - `outerIndex`: the "index" of the value that came from the source + * - `innerIndex`: the "index" of the value from the projected Observable + * @param {number} [concurrent=Number.POSITIVE_INFINITY] Maximum number of input + * Observables being subscribed to concurrently. + * @return {Observable} An Observable that emits the result of applying the + * projection function (and the optional `resultSelector`) to each item emitted + * by the source Observable and merging the results of the Observables obtained + * from this transformation. + * @method mergeMap + * @owner Observable + */ +export function mergeMap(project: (value: T, index: number) => ObservableInput, + resultSelector?: ((outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) | number, + concurrent: number = Number.POSITIVE_INFINITY): OperatorFunction { + return function mergeMapOperatorFunction(source: Observable) { + if (typeof resultSelector === 'number') { + concurrent = resultSelector; + resultSelector = null; + } + return source.lift(new MergeMapOperator(project, resultSelector, concurrent)); + }; +} + +export class MergeMapOperator implements Operator { + constructor(private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R, + private concurrent: number = Number.POSITIVE_INFINITY) { + } + + call(observer: Subscriber, source: any): any { + return source.subscribe(new MergeMapSubscriber( + observer, this.project, this.resultSelector, this.concurrent + )); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class MergeMapSubscriber extends OuterSubscriber { + private hasCompleted: boolean = false; + private buffer: T[] = []; + private active: number = 0; + protected index: number = 0; + + constructor(destination: Subscriber, + private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R, + private concurrent: number = Number.POSITIVE_INFINITY) { + super(destination); + } + + protected _next(value: T): void { + if (this.active < this.concurrent) { + this._tryNext(value); + } else { + this.buffer.push(value); + } + } + + protected _tryNext(value: T) { + let result: ObservableInput; + const index = this.index++; + try { + result = this.project(value, index); + } catch (err) { + this.destination.error(err); + return; + } + this.active++; + this._innerSub(result, value, index); + } + + private _innerSub(ish: ObservableInput, value: T, index: number): void { + this.add(subscribeToResult(this, ish, value, index)); + } + + protected _complete(): void { + this.hasCompleted = true; + if (this.active === 0 && this.buffer.length === 0) { + this.destination.complete(); + } + } + + notifyNext(outerValue: T, innerValue: I, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + if (this.resultSelector) { + this._notifyResultSelector(outerValue, innerValue, outerIndex, innerIndex); + } else { + this.destination.next(innerValue); + } + } + + private _notifyResultSelector(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) { + let result: R; + try { + result = this.resultSelector(outerValue, innerValue, outerIndex, innerIndex); + } catch (err) { + this.destination.error(err); + return; + } + this.destination.next(result); + } + + notifyComplete(innerSub: Subscription): void { + const buffer = this.buffer; + this.remove(innerSub); + this.active--; + if (buffer.length > 0) { + this._next(buffer.shift()); + } else if (this.active === 0 && this.hasCompleted) { + this.destination.complete(); + } + } +} diff --git a/src/operators/min.ts b/src/operators/min.ts new file mode 100644 index 0000000000..99cdf8c1b4 --- /dev/null +++ b/src/operators/min.ts @@ -0,0 +1,40 @@ +import { reduce } from './reduce'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * The Min operator operates on an Observable that emits numbers (or items that can be compared with a provided function), + * and when source Observable completes it emits a single item: the item with the smallest value. + * + * + * + * @example Get the minimal value of a series of numbers + * Rx.Observable.of(5, 4, 7, 2, 8) + * .min() + * .subscribe(x => console.log(x)); // -> 2 + * + * @example Use a comparer function to get the minimal item + * interface Person { + * age: number, + * name: string + * } + * Observable.of({age: 7, name: 'Foo'}, + * {age: 5, name: 'Bar'}, + * {age: 9, name: 'Beer'}) + * .min( (a: Person, b: Person) => a.age < b.age ? -1 : 1) + * .subscribe((x: Person) => console.log(x.name)); // -> 'Bar' + * } + * + * @see {@link max} + * + * @param {Function} [comparer] - Optional comparer function that it will use instead of its default to compare the + * value of two items. + * @return {Observable} An Observable that emits item with the smallest value. + * @method min + * @owner Observable + */ +export function min(comparer?: (x: T, y: T) => number): MonoTypeOperatorFunction { + const min: (x: T, y: T) => T = (typeof comparer === 'function') + ? (x, y) => comparer(x, y) < 0 ? x : y + : (x, y) => x < y ? x : y; + return reduce(min); +} diff --git a/src/operators/multicast.ts b/src/operators/multicast.ts new file mode 100644 index 0000000000..2c9666edef --- /dev/null +++ b/src/operators/multicast.ts @@ -0,0 +1,67 @@ +import { Subject } from '../Subject'; +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { ConnectableObservable, connectableObservableDescriptor } from '../observable/ConnectableObservable'; +import { FactoryOrValue, MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function multicast(subjectOrSubjectFactory: FactoryOrValue>): MonoTypeOperatorFunction; +export function multicast(SubjectFactory: (this: Observable) => Subject, selector?: MonoTypeOperatorFunction): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Returns an Observable that emits the results of invoking a specified selector on items + * emitted by a ConnectableObservable that shares a single subscription to the underlying stream. + * + * + * + * @param {Function|Subject} subjectOrSubjectFactory - Factory function to create an intermediate subject through + * which the source sequence's elements will be multicast to the selector function + * or Subject to push source elements into. + * @param {Function} [selector] - Optional selector function that can use the multicasted source stream + * as many times as needed, without causing multiple subscriptions to the source stream. + * Subscribers to the given source will receive all notifications of the source from the + * time of the subscription forward. + * @return {Observable} An Observable that emits the results of invoking the selector + * on the items emitted by a `ConnectableObservable` that shares a single subscription to + * the underlying stream. + * @method multicast + * @owner Observable + */ +export function multicast(subjectOrSubjectFactory: Subject | (() => Subject), + selector?: (source: Observable) => Observable): MonoTypeOperatorFunction { + return function multicastOperatorFunction(source: Observable): Observable { + let subjectFactory: () => Subject; + if (typeof subjectOrSubjectFactory === 'function') { + subjectFactory = <() => Subject>subjectOrSubjectFactory; + } else { + subjectFactory = function subjectFactory() { + return >subjectOrSubjectFactory; + }; + } + + if (typeof selector === 'function') { + return source.lift(new MulticastOperator(subjectFactory, selector)); + } + + const connectable: any = Object.create(source, connectableObservableDescriptor); + connectable.source = source; + connectable.subjectFactory = subjectFactory; + + return > connectable; + }; +} + +export class MulticastOperator implements Operator { + constructor(private subjectFactory: () => Subject, + private selector: (source: Observable) => Observable) { + } + call(subscriber: Subscriber, source: any): any { + const { selector } = this; + const subject = this.subjectFactory(); + const subscription = selector(subject).subscribe(subscriber); + subscription.add(source.subscribe(subject)); + return subscription; + } +} diff --git a/src/operators/observeOn.ts b/src/operators/observeOn.ts new file mode 100644 index 0000000000..ab86f701f2 --- /dev/null +++ b/src/operators/observeOn.ts @@ -0,0 +1,115 @@ +import { Observable } from '../Observable'; +import { IScheduler } from '../Scheduler'; +import { Operator } from '../Operator'; +import { PartialObserver } from '../Observer'; +import { Subscriber } from '../Subscriber'; +import { Notification } from '../Notification'; +import { TeardownLogic } from '../Subscription'; +import { Action } from '../scheduler/Action'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * + * Re-emits all notifications from source Observable with specified scheduler. + * + * Ensure a specific scheduler is used, from outside of an Observable. + * + * `observeOn` is an operator that accepts a scheduler as a first parameter, which will be used to reschedule + * notifications emitted by the source Observable. It might be useful, if you do not have control over + * internal scheduler of a given Observable, but want to control when its values are emitted nevertheless. + * + * Returned Observable emits the same notifications (nexted values, complete and error events) as the source Observable, + * but rescheduled with provided scheduler. Note that this doesn't mean that source Observables internal + * scheduler will be replaced in any way. Original scheduler still will be used, but when the source Observable emits + * notification, it will be immediately scheduled again - this time with scheduler passed to `observeOn`. + * An anti-pattern would be calling `observeOn` on Observable that emits lots of values synchronously, to split + * that emissions into asynchronous chunks. For this to happen, scheduler would have to be passed into the source + * Observable directly (usually into the operator that creates it). `observeOn` simply delays notifications a + * little bit more, to ensure that they are emitted at expected moments. + * + * As a matter of fact, `observeOn` accepts second parameter, which specifies in milliseconds with what delay notifications + * will be emitted. The main difference between {@link delay} operator and `observeOn` is that `observeOn` + * will delay all notifications - including error notifications - while `delay` will pass through error + * from source Observable immediately when it is emitted. In general it is highly recommended to use `delay` operator + * for any kind of delaying of values in the stream, while using `observeOn` to specify which scheduler should be used + * for notification emissions in general. + * + * @example Ensure values in subscribe are called just before browser repaint. + * const intervals = Rx.Observable.interval(10); // Intervals are scheduled + * // with async scheduler by default... + * + * intervals + * .observeOn(Rx.Scheduler.animationFrame) // ...but we will observe on animationFrame + * .subscribe(val => { // scheduler to ensure smooth animation. + * someDiv.style.height = val + 'px'; + * }); + * + * @see {@link delay} + * + * @param {IScheduler} scheduler Scheduler that will be used to reschedule notifications from source Observable. + * @param {number} [delay] Number of milliseconds that states with what delay every notification should be rescheduled. + * @return {Observable} Observable that emits the same notifications as the source Observable, + * but with provided scheduler. + * + * @method observeOn + * @owner Observable + */ +export function observeOn(scheduler: IScheduler, delay: number = 0): MonoTypeOperatorFunction { + return function observeOnOperatorFunction(source: Observable): Observable { + return source.lift(new ObserveOnOperator(scheduler, delay)); + }; +} + +export class ObserveOnOperator implements Operator { + constructor(private scheduler: IScheduler, private delay: number = 0) { + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class ObserveOnSubscriber extends Subscriber { + static dispatch(this: Action, arg: ObserveOnMessage) { + const { notification, destination } = arg; + notification.observe(destination); + this.unsubscribe(); + } + + constructor(destination: Subscriber, + private scheduler: IScheduler, + private delay: number = 0) { + super(destination); + } + + private scheduleMessage(notification: Notification): void { + this.add(this.scheduler.schedule( + ObserveOnSubscriber.dispatch, + this.delay, + new ObserveOnMessage(notification, this.destination) + )); + } + + protected _next(value: T): void { + this.scheduleMessage(Notification.createNext(value)); + } + + protected _error(err: any): void { + this.scheduleMessage(Notification.createError(err)); + } + + protected _complete(): void { + this.scheduleMessage(Notification.createComplete()); + } +} + +export class ObserveOnMessage { + constructor(public notification: Notification, + public destination: PartialObserver) { + } +} diff --git a/src/operators/publish.ts b/src/operators/publish.ts new file mode 100644 index 0000000000..95330bab94 --- /dev/null +++ b/src/operators/publish.ts @@ -0,0 +1,27 @@ +import { Subject } from '../Subject'; +import { multicast } from './multicast'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function publish(): MonoTypeOperatorFunction; +export function publish(selector: MonoTypeOperatorFunction): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Returns a ConnectableObservable, which is a variety of Observable that waits until its connect method is called + * before it begins emitting items to those Observers that have subscribed to it. + * + * + * + * @param {Function} [selector] - Optional selector function which can use the multicasted source sequence as many times + * as needed, without causing multiple subscriptions to the source sequence. + * Subscribers to the given source will receive all notifications of the source from the time of the subscription on. + * @return A ConnectableObservable that upon connection causes the source Observable to emit items to its Observers. + * @method publish + * @owner Observable + */ +export function publish(selector?: MonoTypeOperatorFunction): MonoTypeOperatorFunction { + return selector ? + multicast(() => new Subject(), selector) : + multicast(new Subject()); +} diff --git a/src/operators/race.ts b/src/operators/race.ts new file mode 100644 index 0000000000..a6efcdf5a2 --- /dev/null +++ b/src/operators/race.ts @@ -0,0 +1,31 @@ +import { Observable } from '../Observable'; +import { isArray } from '../util/isArray'; +import { MonoTypeOperatorFunction, OperatorFunction } from '../interfaces'; +import { race as raceStatic } from '../observable/race'; + +/* tslint:disable:max-line-length */ +export function race(observables: Array>): MonoTypeOperatorFunction; +export function race(observables: Array>): OperatorFunction; +export function race(...observables: Array | Array>>): MonoTypeOperatorFunction; +export function race(...observables: Array | Array>>): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Returns an Observable that mirrors the first source Observable to emit an item + * from the combination of this Observable and supplied Observables. + * @param {...Observables} ...observables Sources used to race for which Observable emits first. + * @return {Observable} An Observable that mirrors the output of the first Observable to emit an item. + * @method race + * @owner Observable + */ +export function race(...observables: Array | Array>>): MonoTypeOperatorFunction { + return function raceOperatorFunction(source: Observable) { + // if the only argument is an array, it was most likely called with + // `pair([obs1, obs2, ...])` + if (observables.length === 1 && isArray(observables[0])) { + observables = >>observables[0]; + } + + return source.lift.call(raceStatic(source, ...observables)); + }; +} \ No newline at end of file diff --git a/src/operators/reduce.ts b/src/operators/reduce.ts new file mode 100644 index 0000000000..cc95f9cd23 --- /dev/null +++ b/src/operators/reduce.ts @@ -0,0 +1,74 @@ +import { Observable } from '../Observable'; +import { scan } from './scan'; +import { takeLast } from './takeLast'; +import { defaultIfEmpty } from './defaultIfEmpty'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; +import { pipe } from '../util/pipe'; + +/* tslint:disable:max-line-length */ +export function reduce(accumulator: (acc: T, value: T, index: number) => T, seed?: T): MonoTypeOperatorFunction; +export function reduce(accumulator: (acc: T[], value: T, index: number) => T[], seed: T[]): OperatorFunction; +export function reduce(accumulator: (acc: R, value: T, index: number) => R, seed?: R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Applies an accumulator function over the source Observable, and returns the + * accumulated result when the source completes, given an optional seed value. + * + * Combines together all values emitted on the source, + * using an accumulator function that knows how to join a new source value into + * the accumulation from the past. + * + * + * + * Like + * [Array.prototype.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), + * `reduce` applies an `accumulator` function against an accumulation and each + * value of the source Observable (from the past) to reduce it to a single + * value, emitted on the output Observable. Note that `reduce` will only emit + * one value, only when the source Observable completes. It is equivalent to + * applying operator {@link scan} followed by operator {@link last}. + * + * Returns an Observable that applies a specified `accumulator` function to each + * item emitted by the source Observable. If a `seed` value is specified, then + * that value will be used as the initial value for the accumulator. If no seed + * value is specified, the first item of the source is used as the seed. + * + * @example Count the number of click events that happened in 5 seconds + * var clicksInFiveSeconds = Rx.Observable.fromEvent(document, 'click') + * .takeUntil(Rx.Observable.interval(5000)); + * var ones = clicksInFiveSeconds.mapTo(1); + * var seed = 0; + * var count = ones.reduce((acc, one) => acc + one, seed); + * count.subscribe(x => console.log(x)); + * + * @see {@link count} + * @see {@link expand} + * @see {@link mergeScan} + * @see {@link scan} + * + * @param {function(acc: R, value: T, index: number): R} accumulator The accumulator function + * called on each source value. + * @param {R} [seed] The initial accumulation value. + * @return {Observable} An Observable that emits a single value that is the + * result of accumulating the values emitted by the source Observable. + * @method reduce + * @owner Observable + */ +export function reduce(accumulator: (acc: R, value: T, index?: number) => R, seed?: R): OperatorFunction { + // providing a seed of `undefined` *should* be valid and trigger + // hasSeed! so don't use `seed !== undefined` checks! + // For this reason, we have to check it here at the original call site + // otherwise inside Operator/Subscriber we won't know if `undefined` + // means they didn't provide anything or if they literally provided `undefined` + if (arguments.length >= 2) { + return function reduceOperatorFunctionWithSeed(source: Observable): Observable { + return pipe(scan(accumulator, seed), takeLast(1), defaultIfEmpty(seed))(source); + }; + } + return function reduceOperatorFunction(source: Observable): Observable { + return pipe(scan((acc, value, index) => { + return accumulator(acc, value, index + 1); + }), takeLast(1))(source); + }; +} diff --git a/src/operators/refCount.ts b/src/operators/refCount.ts new file mode 100644 index 0000000000..e66320e4b0 --- /dev/null +++ b/src/operators/refCount.ts @@ -0,0 +1,94 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Subscription, TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; +import { ConnectableObservable } from '../observable/ConnectableObservable'; +import { Observable } from '../Observable'; + +export function refCount(): MonoTypeOperatorFunction { + return function refCountOperatorFunction(source: ConnectableObservable): Observable { + return source.lift(new RefCountOperator(source)); + }; +} + +class RefCountOperator implements Operator { + constructor(private connectable: ConnectableObservable) { + } + call(subscriber: Subscriber, source: any): TeardownLogic { + + const { connectable } = this; + ( connectable)._refCount++; + + const refCounter = new RefCountSubscriber(subscriber, connectable); + const subscription = source.subscribe(refCounter); + + if (!refCounter.closed) { + ( refCounter).connection = connectable.connect(); + } + + return subscription; + } +} + +class RefCountSubscriber extends Subscriber { + + private connection: Subscription; + + constructor(destination: Subscriber, + private connectable: ConnectableObservable) { + super(destination); + } + + protected _unsubscribe() { + + const { connectable } = this; + if (!connectable) { + this.connection = null; + return; + } + + this.connectable = null; + const refCount = ( connectable)._refCount; + if (refCount <= 0) { + this.connection = null; + return; + } + + ( connectable)._refCount = refCount - 1; + if (refCount > 1) { + this.connection = null; + return; + } + + /// + // Compare the local RefCountSubscriber's connection Subscription to the + // connection Subscription on the shared ConnectableObservable. In cases + // where the ConnectableObservable source synchronously emits values, and + // the RefCountSubscriber's downstream Observers synchronously unsubscribe, + // execution continues to here before the RefCountOperator has a chance to + // supply the RefCountSubscriber with the shared connection Subscription. + // For example: + // ``` + // Observable.range(0, 10) + // .publish() + // .refCount() + // .take(5) + // .subscribe(); + // ``` + // In order to account for this case, RefCountSubscriber should only dispose + // the ConnectableObservable's shared connection Subscription if the + // connection Subscription exists, *and* either: + // a. RefCountSubscriber doesn't have a reference to the shared connection + // Subscription yet, or, + // b. RefCountSubscriber's connection Subscription reference is identical + // to the shared connection Subscription + /// + const { connection } = this; + const sharedConnection = ( connectable)._connection; + this.connection = null; + + if (sharedConnection && (!connection || sharedConnection === connection)) { + sharedConnection.unsubscribe(); + } + } +} diff --git a/src/operators/scan.ts b/src/operators/scan.ts new file mode 100644 index 0000000000..0e97976d37 --- /dev/null +++ b/src/operators/scan.ts @@ -0,0 +1,115 @@ +import { Operator } from '../Operator'; +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function scan(accumulator: (acc: T, value: T, index: number) => T, seed?: T): MonoTypeOperatorFunction; +export function scan(accumulator: (acc: T[], value: T, index: number) => T[], seed?: T[]): OperatorFunction; +export function scan(accumulator: (acc: R, value: T, index: number) => R, seed?: R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Applies an accumulator function over the source Observable, and returns each + * intermediate result, with an optional seed value. + * + * It's like {@link reduce}, but emits the current + * accumulation whenever the source emits a value. + * + * + * + * Combines together all values emitted on the source, using an accumulator + * function that knows how to join a new source value into the accumulation from + * the past. Is similar to {@link reduce}, but emits the intermediate + * accumulations. + * + * Returns an Observable that applies a specified `accumulator` function to each + * item emitted by the source Observable. If a `seed` value is specified, then + * that value will be used as the initial value for the accumulator. If no seed + * value is specified, the first item of the source is used as the seed. + * + * @example Count the number of click events + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var ones = clicks.mapTo(1); + * var seed = 0; + * var count = ones.scan((acc, one) => acc + one, seed); + * count.subscribe(x => console.log(x)); + * + * @see {@link expand} + * @see {@link mergeScan} + * @see {@link reduce} + * + * @param {function(acc: R, value: T, index: number): R} accumulator + * The accumulator function called on each source value. + * @param {T|R} [seed] The initial accumulation value. + * @return {Observable} An observable of the accumulated values. + * @method scan + * @owner Observable + */ +export function scan(accumulator: (acc: R, value: T, index: number) => R, seed?: T | R): OperatorFunction { + let hasSeed = false; + // providing a seed of `undefined` *should* be valid and trigger + // hasSeed! so don't use `seed !== undefined` checks! + // For this reason, we have to check it here at the original call site + // otherwise inside Operator/Subscriber we won't know if `undefined` + // means they didn't provide anything or if they literally provided `undefined` + if (arguments.length >= 2) { + hasSeed = true; + } + + return function scanOperatorFunction(source: Observable): Observable { + return source.lift(new ScanOperator(accumulator, seed, hasSeed)); + }; +} + +class ScanOperator implements Operator { + constructor(private accumulator: (acc: R, value: T, index: number) => R, private seed?: T | R, private hasSeed: boolean = false) {} + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class ScanSubscriber extends Subscriber { + private index: number = 0; + + get seed(): T | R { + return this._seed; + } + + set seed(value: T | R) { + this.hasSeed = true; + this._seed = value; + } + + constructor(destination: Subscriber, private accumulator: (acc: R, value: T, index: number) => R, private _seed: T | R, + private hasSeed: boolean) { + super(destination); + } + + protected _next(value: T): void { + if (!this.hasSeed) { + this.seed = value; + this.destination.next(value); + } else { + return this._tryNext(value); + } + } + + private _tryNext(value: T): void { + const index = this.index++; + let result: any; + try { + result = this.accumulator(this.seed, value, index); + } catch (err) { + this.destination.error(err); + } + this.seed = result; + this.destination.next(result); + } +} diff --git a/src/operators/subscribeOn.ts b/src/operators/subscribeOn.ts new file mode 100644 index 0000000000..b6d2146737 --- /dev/null +++ b/src/operators/subscribeOn.ts @@ -0,0 +1,35 @@ +import { Operator } from '../Operator'; +import { IScheduler } from '../Scheduler'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { SubscribeOnObservable } from '../observable/SubscribeOnObservable'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Asynchronously subscribes Observers to this Observable on the specified IScheduler. + * + * + * + * @param {Scheduler} scheduler - The IScheduler to perform subscription actions on. + * @return {Observable} The source Observable modified so that its subscriptions happen on the specified IScheduler. + . + * @method subscribeOn + * @owner Observable + */ +export function subscribeOn(scheduler: IScheduler, delay: number = 0): MonoTypeOperatorFunction { + return function subscribeOnOperatorFunction(source: Observable): Observable { + return source.lift(new SubscribeOnOperator(scheduler, delay)); + }; +} + +class SubscribeOnOperator implements Operator { + constructor(private scheduler: IScheduler, + private delay: number) { + } + call(subscriber: Subscriber, source: any): TeardownLogic { + return new SubscribeOnObservable( + source, this.delay, this.scheduler + ).subscribe(subscriber); + } +} diff --git a/src/operators/switchAll.ts b/src/operators/switchAll.ts new file mode 100644 index 0000000000..ec638fed74 --- /dev/null +++ b/src/operators/switchAll.ts @@ -0,0 +1,8 @@ +import { OperatorFunction } from '../interfaces'; +import { Observable } from '../Observable'; +import { switchMap } from './switchMap'; +import { identity } from '../util/identity'; + +export function switchAll(): OperatorFunction, T> { + return switchMap(identity); +} diff --git a/src/operators/switchMap.ts b/src/operators/switchMap.ts new file mode 100644 index 0000000000..84199d0970 --- /dev/null +++ b/src/operators/switchMap.ts @@ -0,0 +1,155 @@ +import { Operator } from '../Operator'; +import { Observable, ObservableInput } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { Subscription } from '../Subscription'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function switchMap(project: (value: T, index: number) => ObservableInput): OperatorFunction; +export function switchMap(project: (value: T, index: number) => ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Projects each source value to an Observable which is merged in the output + * Observable, emitting values only from the most recently projected Observable. + * + * Maps each value to an Observable, then flattens all of + * these inner Observables using {@link switch}. + * + * + * + * Returns an Observable that emits items based on applying a function that you + * supply to each item emitted by the source Observable, where that function + * returns an (so-called "inner") Observable. Each time it observes one of these + * inner Observables, the output Observable begins emitting the items emitted by + * that inner Observable. When a new inner Observable is emitted, `switchMap` + * stops emitting items from the earlier-emitted inner Observable and begins + * emitting items from the new one. It continues to behave like this for + * subsequent inner Observables. + * + * @example Rerun an interval Observable on every click event + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.switchMap((ev) => Rx.Observable.interval(1000)); + * result.subscribe(x => console.log(x)); + * + * @see {@link concatMap} + * @see {@link exhaustMap} + * @see {@link mergeMap} + * @see {@link switch} + * @see {@link switchMapTo} + * + * @param {function(value: T, ?index: number): ObservableInput} project A function + * that, when applied to an item emitted by the source Observable, returns an + * Observable. + * @param {function(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): any} [resultSelector] + * A function to produce the value on the output Observable based on the values + * and the indices of the source (outer) emission and the inner Observable + * emission. The arguments passed to this function are: + * - `outerValue`: the value that came from the source + * - `innerValue`: the value that came from the projected Observable + * - `outerIndex`: the "index" of the value that came from the source + * - `innerIndex`: the "index" of the value from the projected Observable + * @return {Observable} An Observable that emits the result of applying the + * projection function (and the optional `resultSelector`) to each item emitted + * by the source Observable and taking only the values from the most recently + * projected inner Observable. + * @method switchMap + * @owner Observable + */ +export function switchMap( + project: (value: T, index: number) => ObservableInput, + resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R +): OperatorFunction { + return function switchMapOperatorFunction(source: Observable): Observable { + return source.lift(new SwitchMapOperator(project, resultSelector)); + }; +} + +class SwitchMapOperator implements Operator { + constructor(private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new SwitchMapSubscriber(subscriber, this.project, this.resultSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class SwitchMapSubscriber extends OuterSubscriber { + private index: number = 0; + private innerSubscription: Subscription; + + constructor(destination: Subscriber, + private project: (value: T, index: number) => ObservableInput, + private resultSelector?: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R) { + super(destination); + } + + protected _next(value: T) { + let result: ObservableInput; + const index = this.index++; + try { + result = this.project(value, index); + } catch (error) { + this.destination.error(error); + return; + } + this._innerSub(result, value, index); + } + + private _innerSub(result: ObservableInput, value: T, index: number) { + const innerSubscription = this.innerSubscription; + if (innerSubscription) { + innerSubscription.unsubscribe(); + } + this.add(this.innerSubscription = subscribeToResult(this, result, value, index)); + } + + protected _complete(): void { + const {innerSubscription} = this; + if (!innerSubscription || innerSubscription.closed) { + super._complete(); + } + } + + protected _unsubscribe() { + this.innerSubscription = null; + } + + notifyComplete(innerSub: Subscription): void { + this.remove(innerSub); + this.innerSubscription = null; + if (this.isStopped) { + super._complete(); + } + } + + notifyNext(outerValue: T, innerValue: I, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + if (this.resultSelector) { + this._tryNotifyNext(outerValue, innerValue, outerIndex, innerIndex); + } else { + this.destination.next(innerValue); + } + } + + private _tryNotifyNext(outerValue: T, innerValue: I, outerIndex: number, innerIndex: number): void { + let result: R; + try { + result = this.resultSelector(outerValue, innerValue, outerIndex, innerIndex); + } catch (err) { + this.destination.error(err); + return; + } + this.destination.next(result); + } +} diff --git a/src/operators/takeLast.ts b/src/operators/takeLast.ts new file mode 100644 index 0000000000..3e408b4da7 --- /dev/null +++ b/src/operators/takeLast.ts @@ -0,0 +1,109 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; +import { EmptyObservable } from '../observable/EmptyObservable'; +import { Observable } from '../Observable'; +import { TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/** + * Emits only the last `count` values emitted by the source Observable. + * + * Remembers the latest `count` values, then emits those + * only when the source completes. + * + * + * + * `takeLast` returns an Observable that emits at most the last `count` values + * emitted by the source Observable. If the source emits fewer than `count` + * values then all of its values are emitted. This operator must wait until the + * `complete` notification emission from the source in order to emit the `next` + * values on the output Observable, because otherwise it is impossible to know + * whether or not more values will be emitted on the source. For this reason, + * all values are emitted synchronously, followed by the complete notification. + * + * @example Take the last 3 values of an Observable with many values + * var many = Rx.Observable.range(1, 100); + * var lastThree = many.takeLast(3); + * lastThree.subscribe(x => console.log(x)); + * + * @see {@link take} + * @see {@link takeUntil} + * @see {@link takeWhile} + * @see {@link skip} + * + * @throws {ArgumentOutOfRangeError} When using `takeLast(i)`, it delivers an + * ArgumentOutOrRangeError to the Observer's `error` callback if `i < 0`. + * + * @param {number} count The maximum number of values to emit from the end of + * the sequence of values emitted by the source Observable. + * @return {Observable} An Observable that emits at most the last count + * values emitted by the source Observable. + * @method takeLast + * @owner Observable + */ +export function takeLast(count: number): MonoTypeOperatorFunction { + return function takeLastOperatorFunction(source: Observable): Observable { + if (count === 0) { + return new EmptyObservable(); + } else { + return source.lift(new TakeLastOperator(count)); + } + }; +} + +class TakeLastOperator implements Operator { + constructor(private total: number) { + if (this.total < 0) { + throw new ArgumentOutOfRangeError; + } + } + + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new TakeLastSubscriber(subscriber, this.total)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class TakeLastSubscriber extends Subscriber { + private ring: Array = new Array(); + private count: number = 0; + + constructor(destination: Subscriber, private total: number) { + super(destination); + } + + protected _next(value: T): void { + const ring = this.ring; + const total = this.total; + const count = this.count++; + + if (ring.length < total) { + ring.push(value); + } else { + const index = count % total; + ring[index] = value; + } + } + + protected _complete(): void { + const destination = this.destination; + let count = this.count; + + if (count > 0) { + const total = this.count >= this.total ? this.total : this.count; + const ring = this.ring; + + for (let i = 0; i < total; i++) { + const idx = (count++) % total; + destination.next(ring[idx]); + } + } + + destination.complete(); + } +} diff --git a/src/operators/tap.ts b/src/operators/tap.ts new file mode 100644 index 0000000000..a88c7d2018 --- /dev/null +++ b/src/operators/tap.ts @@ -0,0 +1,122 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { PartialObserver } from '../Observer'; +import { TeardownLogic } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function tap(next: (x: T) => void, error?: (e: any) => void, complete?: () => void): MonoTypeOperatorFunction; +export function tap(observer: PartialObserver): MonoTypeOperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * Perform a side effect for every emission on the source Observable, but return + * an Observable that is identical to the source. + * + * Intercepts each emission on the source and runs a + * function, but returns an output which is identical to the source as long as errors don't occur. + * + * + * + * Returns a mirrored Observable of the source Observable, but modified so that + * the provided Observer is called to perform a side effect for every value, + * error, and completion emitted by the source. Any errors that are thrown in + * the aforementioned Observer or handlers are safely sent down the error path + * of the output Observable. + * + * This operator is useful for debugging your Observables for the correct values + * or performing other side effects. + * + * Note: this is different to a `subscribe` on the Observable. If the Observable + * returned by `do` is not subscribed, the side effects specified by the + * Observer will never happen. `do` therefore simply spies on existing + * execution, it does not trigger an execution to happen like `subscribe` does. + * + * @example Map every click to the clientX position of that click, while also logging the click event + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var positions = clicks + * .do(ev => console.log(ev)) + * .map(ev => ev.clientX); + * positions.subscribe(x => console.log(x)); + * + * @see {@link map} + * @see {@link subscribe} + * + * @param {Observer|function} [nextOrObserver] A normal Observer object or a + * callback for `next`. + * @param {function} [error] Callback for errors in the source. + * @param {function} [complete] Callback for the completion of the source. + * @return {Observable} An Observable identical to the source, but runs the + * specified Observer or callback(s) for each item. + * @name tap + */ +export function tap(nextOrObserver?: PartialObserver | ((x: T) => void), + error?: (e: any) => void, + complete?: () => void): MonoTypeOperatorFunction { + return function tapOperatorFunction(source: Observable): Observable { + return source.lift(new DoOperator(nextOrObserver, error, complete)); + }; +} + +class DoOperator implements Operator { + constructor(private nextOrObserver?: PartialObserver | ((x: T) => void), + private error?: (e: any) => void, + private complete?: () => void) { + } + call(subscriber: Subscriber, source: any): TeardownLogic { + return source.subscribe(new DoSubscriber(subscriber, this.nextOrObserver, this.error, this.complete)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class DoSubscriber extends Subscriber { + + private safeSubscriber: Subscriber; + + constructor(destination: Subscriber, + nextOrObserver?: PartialObserver | ((x: T) => void), + error?: (e: any) => void, + complete?: () => void) { + super(destination); + + const safeSubscriber = new Subscriber(nextOrObserver, error, complete); + safeSubscriber.syncErrorThrowable = true; + this.add(safeSubscriber); + this.safeSubscriber = safeSubscriber; + } + + protected _next(value: T): void { + const { safeSubscriber } = this; + safeSubscriber.next(value); + if (safeSubscriber.syncErrorThrown) { + this.destination.error(safeSubscriber.syncErrorValue); + } else { + this.destination.next(value); + } + } + + protected _error(err: any): void { + const { safeSubscriber } = this; + safeSubscriber.error(err); + if (safeSubscriber.syncErrorThrown) { + this.destination.error(safeSubscriber.syncErrorValue); + } else { + this.destination.error(err); + } + } + + protected _complete(): void { + const { safeSubscriber } = this; + safeSubscriber.complete(); + if (safeSubscriber.syncErrorThrown) { + this.destination.error(safeSubscriber.syncErrorValue); + } else { + this.destination.complete(); + } + } +} diff --git a/src/operators/timestamp.ts b/src/operators/timestamp.ts new file mode 100644 index 0000000000..abb3121a6d --- /dev/null +++ b/src/operators/timestamp.ts @@ -0,0 +1,21 @@ + +import { IScheduler } from '../Scheduler'; +import { async } from '../scheduler/async'; +import { OperatorFunction } from '../interfaces'; +import { map } from './map'; + +/** + * @param scheduler + * @return {Observable>|WebSocketSubject|Observable} + * @method timestamp + * @owner Observable + */ +export function timestamp(scheduler: IScheduler = async): OperatorFunction> { + return map((value: T) => new Timestamp(value, scheduler.now())); + // return (source: Observable) => source.lift(new TimestampOperator(scheduler)); +} + +export class Timestamp { + constructor(public value: T, public timestamp: number) { + } +}; diff --git a/src/operators/toArray.ts b/src/operators/toArray.ts new file mode 100644 index 0000000000..9ca61b63af --- /dev/null +++ b/src/operators/toArray.ts @@ -0,0 +1,11 @@ +import { reduce } from './reduce'; +import { OperatorFunction } from '../interfaces'; + +function toArrayReducer(arr: T[], item: T, index: number) { + arr.push(item); + return arr; +} + +export function toArray(): OperatorFunction { + return reduce(toArrayReducer, []); +} diff --git a/src/operators/window.ts b/src/operators/window.ts new file mode 100644 index 0000000000..54556694a9 --- /dev/null +++ b/src/operators/window.ts @@ -0,0 +1,122 @@ +import { Observable } from '../Observable'; +import { OperatorFunction } from '../interfaces'; +import { Subject } from '../Subject'; +import { Subscriber } from '../Subscriber'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { Operator } from '../Operator'; + +/** + * Branch out the source Observable values as a nested Observable whenever + * `windowBoundaries` emits. + * + * It's like {@link buffer}, but emits a nested Observable + * instead of an array. + * + * + * + * Returns an Observable that emits windows of items it collects from the source + * Observable. The output Observable emits connected, non-overlapping + * windows. It emits the current window and opens a new one whenever the + * Observable `windowBoundaries` emits an item. Because each window is an + * Observable, the output is a higher-order Observable. + * + * @example In every window of 1 second each, emit at most 2 click events + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var interval = Rx.Observable.interval(1000); + * var result = clicks.window(interval) + * .map(win => win.take(2)) // each window has at most 2 emissions + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @see {@link windowCount} + * @see {@link windowTime} + * @see {@link windowToggle} + * @see {@link windowWhen} + * @see {@link buffer} + * + * @param {Observable} windowBoundaries An Observable that completes the + * previous window and starts a new window. + * @return {Observable>} An Observable of windows, which are + * Observables emitting values of the source Observable. + * @method window + * @owner Observable + */ +export function window(windowBoundaries: Observable): OperatorFunction> { + return function windowOperatorFunction(source: Observable) { + return source.lift(new WindowOperator(windowBoundaries)); + }; +} + +class WindowOperator implements Operator> { + + constructor(private windowBoundaries: Observable) { + } + + call(subscriber: Subscriber>, source: any): any { + const windowSubscriber = new WindowSubscriber(subscriber); + const sourceSubscription = source.subscribe(windowSubscriber); + if (!sourceSubscription.closed) { + windowSubscriber.add(subscribeToResult(windowSubscriber, this.windowBoundaries)); + } + return sourceSubscription; + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class WindowSubscriber extends OuterSubscriber { + + private window: Subject = new Subject(); + + constructor(destination: Subscriber>) { + super(destination); + destination.next(this.window); + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.openWindow(); + } + + notifyError(error: any, innerSub: InnerSubscriber): void { + this._error(error); + } + + notifyComplete(innerSub: InnerSubscriber): void { + this._complete(); + } + + protected _next(value: T): void { + this.window.next(value); + } + + protected _error(err: any): void { + this.window.error(err); + this.destination.error(err); + } + + protected _complete(): void { + this.window.complete(); + this.destination.complete(); + } + + protected _unsubscribe() { + this.window = null; + } + + private openWindow(): void { + const prevWindow = this.window; + if (prevWindow) { + prevWindow.complete(); + } + const destination = this.destination; + const newWindow = this.window = new Subject(); + destination.next(newWindow); + } +} diff --git a/src/operators/windowCount.ts b/src/operators/windowCount.ts new file mode 100644 index 0000000000..a670eda41f --- /dev/null +++ b/src/operators/windowCount.ts @@ -0,0 +1,134 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subject } from '../Subject'; +import { OperatorFunction } from '../interfaces'; + +/** + * Branch out the source Observable values as a nested Observable with each + * nested Observable emitting at most `windowSize` values. + * + * It's like {@link bufferCount}, but emits a nested + * Observable instead of an array. + * + * + * + * Returns an Observable that emits windows of items it collects from the source + * Observable. The output Observable emits windows every `startWindowEvery` + * items, each containing no more than `windowSize` items. When the source + * Observable completes or encounters an error, the output Observable emits + * the current window and propagates the notification from the source + * Observable. If `startWindowEvery` is not provided, then new windows are + * started immediately at the start of the source and when each window completes + * with size `windowSize`. + * + * @example Ignore every 3rd click event, starting from the first one + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.windowCount(3) + * .map(win => win.skip(1)) // skip first of every 3 clicks + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @example Ignore every 3rd click event, starting from the third one + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.windowCount(2, 3) + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @see {@link window} + * @see {@link windowTime} + * @see {@link windowToggle} + * @see {@link windowWhen} + * @see {@link bufferCount} + * + * @param {number} windowSize The maximum number of values emitted by each + * window. + * @param {number} [startWindowEvery] Interval at which to start a new window. + * For example if `startWindowEvery` is `2`, then a new window will be started + * on every other value from the source. A new window is started at the + * beginning of the source by default. + * @return {Observable>} An Observable of windows, which in turn + * are Observable of values. + * @method windowCount + * @owner Observable + */ +export function windowCount(windowSize: number, + startWindowEvery: number = 0): OperatorFunction> { + return function windowCountOperatorFunction(source: Observable) { + return source.lift(new WindowCountOperator(windowSize, startWindowEvery)); + }; +} + +class WindowCountOperator implements Operator> { + + constructor(private windowSize: number, + private startWindowEvery: number) { + } + + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new WindowCountSubscriber(subscriber, this.windowSize, this.startWindowEvery)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class WindowCountSubscriber extends Subscriber { + private windows: Subject[] = [ new Subject() ]; + private count: number = 0; + + constructor(protected destination: Subscriber>, + private windowSize: number, + private startWindowEvery: number) { + super(destination); + destination.next(this.windows[0]); + } + + protected _next(value: T) { + const startWindowEvery = (this.startWindowEvery > 0) ? this.startWindowEvery : this.windowSize; + const destination = this.destination; + const windowSize = this.windowSize; + const windows = this.windows; + const len = windows.length; + + for (let i = 0; i < len && !this.closed; i++) { + windows[i].next(value); + } + const c = this.count - windowSize + 1; + if (c >= 0 && c % startWindowEvery === 0 && !this.closed) { + windows.shift().complete(); + } + if (++this.count % startWindowEvery === 0 && !this.closed) { + const window = new Subject(); + windows.push(window); + destination.next(window); + } + } + + protected _error(err: any) { + const windows = this.windows; + if (windows) { + while (windows.length > 0 && !this.closed) { + windows.shift().error(err); + } + } + this.destination.error(err); + } + + protected _complete() { + const windows = this.windows; + if (windows) { + while (windows.length > 0 && !this.closed) { + windows.shift().complete(); + } + } + this.destination.complete(); + } + + protected _unsubscribe() { + this.count = 0; + this.windows = null; + } +} diff --git a/src/operators/windowTime.ts b/src/operators/windowTime.ts new file mode 100644 index 0000000000..3dc6de4257 --- /dev/null +++ b/src/operators/windowTime.ts @@ -0,0 +1,262 @@ +import { IScheduler } from '../Scheduler'; +import { Action } from '../scheduler/Action'; +import { Subject } from '../Subject'; +import { Operator } from '../Operator'; +import { async } from '../scheduler/async'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subscription } from '../Subscription'; +import { isNumeric } from '../util/isNumeric'; +import { isScheduler } from '../util/isScheduler'; +import { OperatorFunction } from '../interfaces'; + +/** + * Branch out the source Observable values as a nested Observable periodically + * in time. + * + * It's like {@link bufferTime}, but emits a nested + * Observable instead of an array. + * + * + * + * Returns an Observable that emits windows of items it collects from the source + * Observable. The output Observable starts a new window periodically, as + * determined by the `windowCreationInterval` argument. It emits each window + * after a fixed timespan, specified by the `windowTimeSpan` argument. When the + * source Observable completes or encounters an error, the output Observable + * emits the current window and propagates the notification from the source + * Observable. If `windowCreationInterval` is not provided, the output + * Observable starts a new window when the previous window of duration + * `windowTimeSpan` completes. If `maxWindowCount` is provided, each window + * will emit at most fixed number of values. Window will complete immediately + * after emitting last value and next one still will open as specified by + * `windowTimeSpan` and `windowCreationInterval` arguments. + * + * @example In every window of 1 second each, emit at most 2 click events + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.windowTime(1000) + * .map(win => win.take(2)) // each window has at most 2 emissions + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @example Every 5 seconds start a window 1 second long, and emit at most 2 click events per window + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.windowTime(1000, 5000) + * .map(win => win.take(2)) // each window has at most 2 emissions + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @example Same as example above but with maxWindowCount instead of take + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks.windowTime(1000, 5000, 2) // each window has still at most 2 emissions + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + + * @see {@link window} + * @see {@link windowCount} + * @see {@link windowToggle} + * @see {@link windowWhen} + * @see {@link bufferTime} + * + * @param {number} windowTimeSpan The amount of time to fill each window. + * @param {number} [windowCreationInterval] The interval at which to start new + * windows. + * @param {number} [maxWindowSize=Number.POSITIVE_INFINITY] Max number of + * values each window can emit before completion. + * @param {Scheduler} [scheduler=async] The scheduler on which to schedule the + * intervals that determine window boundaries. + * @return {Observable>} An observable of windows, which in turn + * are Observables. + * @method windowTime + * @owner Observable + */ +export function windowTime(windowTimeSpan: number, + scheduler?: IScheduler): OperatorFunction>; +export function windowTime(windowTimeSpan: number, + windowCreationInterval: number, + scheduler?: IScheduler): OperatorFunction>; +export function windowTime(windowTimeSpan: number, + windowCreationInterval: number, + maxWindowSize: number, + scheduler?: IScheduler): OperatorFunction>; + +export function windowTime(windowTimeSpan: number): OperatorFunction> { + let scheduler: IScheduler = async; + let windowCreationInterval: number = null; + let maxWindowSize: number = Number.POSITIVE_INFINITY; + + if (isScheduler(arguments[3])) { + scheduler = arguments[3]; + } + + if (isScheduler(arguments[2])) { + scheduler = arguments[2]; + } else if (isNumeric(arguments[2])) { + maxWindowSize = arguments[2]; + } + + if (isScheduler(arguments[1])) { + scheduler = arguments[1]; + } else if (isNumeric(arguments[1])) { + windowCreationInterval = arguments[1]; + } + + return function windowTimeOperatorFunction(source: Observable) { + return source.lift(new WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)); + }; +} + +class WindowTimeOperator implements Operator> { + + constructor(private windowTimeSpan: number, + private windowCreationInterval: number | null, + private maxWindowSize: number, + private scheduler: IScheduler) { + } + + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new WindowTimeSubscriber( + subscriber, this.windowTimeSpan, this.windowCreationInterval, this.maxWindowSize, this.scheduler + )); + } +} + +interface CreationState { + windowTimeSpan: number; + windowCreationInterval: number; + subscriber: WindowTimeSubscriber; + scheduler: IScheduler; +} + +interface TimeSpanOnlyState { + window: CountedSubject; + windowTimeSpan: number; + subscriber: WindowTimeSubscriber; + } + +interface CloseWindowContext { + action: Action>; + subscription: Subscription; +} + +interface CloseState { + subscriber: WindowTimeSubscriber; + window: CountedSubject; + context: CloseWindowContext; +} + +class CountedSubject extends Subject { + private _numberOfNextedValues: number = 0; + + next(value?: T): void { + this._numberOfNextedValues++; + super.next(value); + } + + get numberOfNextedValues(): number { + return this._numberOfNextedValues; + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class WindowTimeSubscriber extends Subscriber { + private windows: CountedSubject[] = []; + + constructor(protected destination: Subscriber>, + private windowTimeSpan: number, + private windowCreationInterval: number | null, + private maxWindowSize: number, + private scheduler: IScheduler) { + super(destination); + + const window = this.openWindow(); + if (windowCreationInterval !== null && windowCreationInterval >= 0) { + const closeState: CloseState = { subscriber: this, window, context: null }; + const creationState: CreationState = { windowTimeSpan, windowCreationInterval, subscriber: this, scheduler }; + this.add(scheduler.schedule(dispatchWindowClose, windowTimeSpan, closeState)); + this.add(scheduler.schedule(dispatchWindowCreation, windowCreationInterval, creationState)); + } else { + const timeSpanOnlyState: TimeSpanOnlyState = { subscriber: this, window, windowTimeSpan }; + this.add(scheduler.schedule(dispatchWindowTimeSpanOnly, windowTimeSpan, timeSpanOnlyState)); + } + } + + protected _next(value: T): void { + const windows = this.windows; + const len = windows.length; + for (let i = 0; i < len; i++) { + const window = windows[i]; + if (!window.closed) { + window.next(value); + if (window.numberOfNextedValues >= this.maxWindowSize) { + this.closeWindow(window); + } + } + } + } + + protected _error(err: any): void { + const windows = this.windows; + while (windows.length > 0) { + windows.shift().error(err); + } + this.destination.error(err); + } + + protected _complete(): void { + const windows = this.windows; + while (windows.length > 0) { + const window = windows.shift(); + if (!window.closed) { + window.complete(); + } + } + this.destination.complete(); + } + + public openWindow(): CountedSubject { + const window = new CountedSubject(); + this.windows.push(window); + const destination = this.destination; + destination.next(window); + return window; + } + + public closeWindow(window: CountedSubject): void { + window.complete(); + const windows = this.windows; + windows.splice(windows.indexOf(window), 1); + } +} + +function dispatchWindowTimeSpanOnly(this: Action>, state: TimeSpanOnlyState): void { + const { subscriber, windowTimeSpan, window } = state; + if (window) { + subscriber.closeWindow(window); + } + state.window = subscriber.openWindow(); + this.schedule(state, windowTimeSpan); +} + +function dispatchWindowCreation(this: Action>, state: CreationState): void { + const { windowTimeSpan, subscriber, scheduler, windowCreationInterval } = state; + const window = subscriber.openWindow(); + const action = this; + let context: CloseWindowContext = { action, subscription: null }; + const timeSpanState: CloseState = { subscriber, window, context }; + context.subscription = scheduler.schedule(dispatchWindowClose, windowTimeSpan, timeSpanState); + action.add(context.subscription); + action.schedule(state, windowCreationInterval); +} + +function dispatchWindowClose(state: CloseState): void { + const { subscriber, window, context } = state; + if (context && context.action && context.subscription) { + context.action.remove(context.subscription); + } + subscriber.closeWindow(window); +} diff --git a/src/operators/windowToggle.ts b/src/operators/windowToggle.ts new file mode 100644 index 0000000000..0c91b61d77 --- /dev/null +++ b/src/operators/windowToggle.ts @@ -0,0 +1,206 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subject } from '../Subject'; +import { Subscription } from '../Subscription'; +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/** + * Branch out the source Observable values as a nested Observable starting from + * an emission from `openings` and ending when the output of `closingSelector` + * emits. + * + * It's like {@link bufferToggle}, but emits a nested + * Observable instead of an array. + * + * + * + * Returns an Observable that emits windows of items it collects from the source + * Observable. The output Observable emits windows that contain those items + * emitted by the source Observable between the time when the `openings` + * Observable emits an item and when the Observable returned by + * `closingSelector` emits an item. + * + * @example Every other second, emit the click events from the next 500ms + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var openings = Rx.Observable.interval(1000); + * var result = clicks.windowToggle(openings, i => + * i % 2 ? Rx.Observable.interval(500) : Rx.Observable.empty() + * ).mergeAll(); + * result.subscribe(x => console.log(x)); + * + * @see {@link window} + * @see {@link windowCount} + * @see {@link windowTime} + * @see {@link windowWhen} + * @see {@link bufferToggle} + * + * @param {Observable} openings An observable of notifications to start new + * windows. + * @param {function(value: O): Observable} closingSelector A function that takes + * the value emitted by the `openings` observable and returns an Observable, + * which, when it emits (either `next` or `complete`), signals that the + * associated window should complete. + * @return {Observable>} An observable of windows, which in turn + * are Observables. + * @method windowToggle + * @owner Observable + */ +export function windowToggle(openings: Observable, + closingSelector: (openValue: O) => Observable): OperatorFunction> { + return (source: Observable) => source.lift(new WindowToggleOperator(openings, closingSelector)); +} + +class WindowToggleOperator implements Operator> { + + constructor(private openings: Observable, + private closingSelector: (openValue: O) => Observable) { + } + + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new WindowToggleSubscriber( + subscriber, this.openings, this.closingSelector + )); + } +} + +interface WindowContext { + window: Subject; + subscription: Subscription; +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class WindowToggleSubscriber extends OuterSubscriber { + private contexts: WindowContext[] = []; + private openSubscription: Subscription; + + constructor(destination: Subscriber>, + private openings: Observable, + private closingSelector: (openValue: O) => Observable) { + super(destination); + this.add(this.openSubscription = subscribeToResult(this, openings, openings)); + } + + protected _next(value: T) { + const { contexts } = this; + if (contexts) { + const len = contexts.length; + for (let i = 0; i < len; i++) { + contexts[i].window.next(value); + } + } + } + + protected _error(err: any) { + + const { contexts } = this; + this.contexts = null; + + if (contexts) { + const len = contexts.length; + let index = -1; + + while (++index < len) { + const context = contexts[index]; + context.window.error(err); + context.subscription.unsubscribe(); + } + } + + super._error(err); + } + + protected _complete() { + const { contexts } = this; + this.contexts = null; + if (contexts) { + const len = contexts.length; + let index = -1; + while (++index < len) { + const context = contexts[index]; + context.window.complete(); + context.subscription.unsubscribe(); + } + } + super._complete(); + } + + protected _unsubscribe() { + const { contexts } = this; + this.contexts = null; + if (contexts) { + const len = contexts.length; + let index = -1; + while (++index < len) { + const context = contexts[index]; + context.window.unsubscribe(); + context.subscription.unsubscribe(); + } + } + } + + notifyNext(outerValue: any, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + + if (outerValue === this.openings) { + + const { closingSelector } = this; + const closingNotifier = tryCatch(closingSelector)(innerValue); + + if (closingNotifier === errorObject) { + return this.error(errorObject.e); + } else { + const window = new Subject(); + const subscription = new Subscription(); + const context = { window, subscription }; + this.contexts.push(context); + const innerSubscription = subscribeToResult(this, closingNotifier, context); + + if (innerSubscription.closed) { + this.closeWindow(this.contexts.length - 1); + } else { + ( innerSubscription).context = context; + subscription.add(innerSubscription); + } + + this.destination.next(window); + + } + } else { + this.closeWindow(this.contexts.indexOf(outerValue)); + } + } + + notifyError(err: any): void { + this.error(err); + } + + notifyComplete(inner: Subscription): void { + if (inner !== this.openSubscription) { + this.closeWindow(this.contexts.indexOf(( inner).context)); + } + } + + private closeWindow(index: number): void { + if (index === -1) { + return; + } + + const { contexts } = this; + const context = contexts[index]; + const { window, subscription } = context; + contexts.splice(index, 1); + window.complete(); + subscription.unsubscribe(); + } +} diff --git a/src/operators/windowWhen.ts b/src/operators/windowWhen.ts new file mode 100644 index 0000000000..3b629044b9 --- /dev/null +++ b/src/operators/windowWhen.ts @@ -0,0 +1,140 @@ +import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { Observable } from '../Observable'; +import { Subject } from '../Subject'; +import { Subscription } from '../Subscription'; +import { tryCatch } from '../util/tryCatch'; +import { errorObject } from '../util/errorObject'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { OperatorFunction } from '../interfaces'; + +/** + * Branch out the source Observable values as a nested Observable using a + * factory function of closing Observables to determine when to start a new + * window. + * + * It's like {@link bufferWhen}, but emits a nested + * Observable instead of an array. + * + * + * + * Returns an Observable that emits windows of items it collects from the source + * Observable. The output Observable emits connected, non-overlapping windows. + * It emits the current window and opens a new one whenever the Observable + * produced by the specified `closingSelector` function emits an item. The first + * window is opened immediately when subscribing to the output Observable. + * + * @example Emit only the first two clicks events in every window of [1-5] random seconds + * var clicks = Rx.Observable.fromEvent(document, 'click'); + * var result = clicks + * .windowWhen(() => Rx.Observable.interval(1000 + Math.random() * 4000)) + * .map(win => win.take(2)) // each window has at most 2 emissions + * .mergeAll(); // flatten the Observable-of-Observables + * result.subscribe(x => console.log(x)); + * + * @see {@link window} + * @see {@link windowCount} + * @see {@link windowTime} + * @see {@link windowToggle} + * @see {@link bufferWhen} + * + * @param {function(): Observable} closingSelector A function that takes no + * arguments and returns an Observable that signals (on either `next` or + * `complete`) when to close the previous window and start a new one. + * @return {Observable>} An observable of windows, which in turn + * are Observables. + * @method windowWhen + * @owner Observable + */ +export function windowWhen(closingSelector: () => Observable): OperatorFunction> { + return function windowWhenOperatorFunction(source: Observable) { + return source.lift(new WindowOperator(closingSelector)); + }; +} + +class WindowOperator implements Operator> { + constructor(private closingSelector: () => Observable) { + } + + call(subscriber: Subscriber>, source: any): any { + return source.subscribe(new WindowSubscriber(subscriber, this.closingSelector)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class WindowSubscriber extends OuterSubscriber { + private window: Subject; + private closingNotification: Subscription; + + constructor(protected destination: Subscriber>, + private closingSelector: () => Observable) { + super(destination); + this.openWindow(); + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.openWindow(innerSub); + } + + notifyError(error: any, innerSub: InnerSubscriber): void { + this._error(error); + } + + notifyComplete(innerSub: InnerSubscriber): void { + this.openWindow(innerSub); + } + + protected _next(value: T): void { + this.window.next(value); + } + + protected _error(err: any): void { + this.window.error(err); + this.destination.error(err); + this.unsubscribeClosingNotification(); + } + + protected _complete(): void { + this.window.complete(); + this.destination.complete(); + this.unsubscribeClosingNotification(); + } + + private unsubscribeClosingNotification(): void { + if (this.closingNotification) { + this.closingNotification.unsubscribe(); + } + } + + private openWindow(innerSub: InnerSubscriber = null): void { + if (innerSub) { + this.remove(innerSub); + innerSub.unsubscribe(); + } + + const prevWindow = this.window; + if (prevWindow) { + prevWindow.complete(); + } + + const window = this.window = new Subject(); + this.destination.next(window); + + const closingNotifier = tryCatch(this.closingSelector)(); + if (closingNotifier === errorObject) { + const err = errorObject.e; + this.destination.error(err); + this.window.error(err); + } else { + this.add(this.closingNotification = subscribeToResult(this, closingNotifier)); + } + } +} diff --git a/src/operators/zip.ts b/src/operators/zip.ts new file mode 100644 index 0000000000..8316d52dc4 --- /dev/null +++ b/src/operators/zip.ts @@ -0,0 +1,344 @@ +import { Observable, ObservableInput } from '../Observable'; +import { ArrayObservable } from '../observable/ArrayObservable'; +import { isArray } from '../util/isArray'; +import { Operator } from '../Operator'; +import { PartialObserver } from '../Observer'; +import { Subscriber } from '../Subscriber'; +import { OuterSubscriber } from '../OuterSubscriber'; +import { InnerSubscriber } from '../InnerSubscriber'; +import { subscribeToResult } from '../util/subscribeToResult'; +import { iterator as Symbol_iterator } from '../symbol/iterator'; +import { OperatorFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function zip(project: (v1: T) => R): OperatorFunction; +export function zip(v2: ObservableInput, project: (v1: T, v2: T2) => R): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, project: (v1: T, v2: T2, v3: T3) => R): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4) => R): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5) => R): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6) => R): OperatorFunction ; +export function zip(v2: ObservableInput): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): OperatorFunction; +export function zip(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): OperatorFunction ; +export function zip(...observables: Array | ((...values: Array) => R)>): OperatorFunction; +export function zip(array: Array>): OperatorFunction; +export function zip(array: Array>, project: (v1: T, ...values: Array) => R): OperatorFunction; +/* tslint:enable:max-line-length */ + +/** + * @param observables + * @return {Observable} + * @method zip + * @owner Observable + */ +export function zip(...observables: Array | ((...values: Array) => R)>): OperatorFunction { + return function zipOperatorFunction(source: Observable) { + return source.lift.call(zipStatic(source, ...observables)); + }; +} + +/* tslint:disable:max-line-length */ +export function zipStatic(v1: ObservableInput, v2: ObservableInput): Observable<[T, T2]>; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput): Observable<[T, T2, T3]>; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): Observable<[T, T2, T3, T4]>; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): Observable<[T, T2, T3, T4, T5]>; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): Observable<[T, T2, T3, T4, T5, T6]>; + +export function zipStatic(v1: ObservableInput, project: (v1: T) => R): Observable; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, project: (v1: T, v2: T2) => R): Observable; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, project: (v1: T, v2: T2, v3: T3) => R): Observable; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4) => R): Observable; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5) => R): Observable; +export function zipStatic(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6) => R): Observable; + +export function zipStatic(array: ObservableInput[]): Observable; +export function zipStatic(array: ObservableInput[]): Observable; +export function zipStatic(array: ObservableInput[], project: (...values: Array) => R): Observable; +export function zipStatic(array: ObservableInput[], project: (...values: Array) => R): Observable; + +export function zipStatic(...observables: Array>): Observable; +export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable; +export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable; +/* tslint:enable:max-line-length */ + +/** + * Combines multiple Observables to create an Observable whose values are calculated from the values, in order, of each + * of its input Observables. + * + * If the latest parameter is a function, this function is used to compute the created value from the input values. + * Otherwise, an array of the input values is returned. + * + * @example Combine age and name from different sources + * + * let age$ = Observable.of(27, 25, 29); + * let name$ = Observable.of('Foo', 'Bar', 'Beer'); + * let isDev$ = Observable.of(true, true, false); + * + * Observable + * .zip(age$, + * name$, + * isDev$, + * (age: number, name: string, isDev: boolean) => ({ age, name, isDev })) + * .subscribe(x => console.log(x)); + * + * // outputs + * // { age: 27, name: 'Foo', isDev: true } + * // { age: 25, name: 'Bar', isDev: true } + * // { age: 29, name: 'Beer', isDev: false } + * + * @param observables + * @return {Observable} + * @static true + * @name zip + * @owner Observable + */ +export function zipStatic(...observables: Array | ((...values: Array) => R)>): Observable { + const project = <((...ys: Array) => R)> observables[observables.length - 1]; + if (typeof project === 'function') { + observables.pop(); + } + return new ArrayObservable(observables).lift(new ZipOperator(project)); +} + +export class ZipOperator implements Operator { + + project: (...values: Array) => R; + + constructor(project?: (...values: Array) => R) { + this.project = project; + } + + call(subscriber: Subscriber, source: any): any { + return source.subscribe(new ZipSubscriber(subscriber, this.project)); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class ZipSubscriber extends Subscriber { + private values: any; + private project: (...values: Array) => R; + private iterators: LookAheadIterator[] = []; + private active = 0; + + constructor(destination: Subscriber, + project?: (...values: Array) => R, + values: any = Object.create(null)) { + super(destination); + this.project = (typeof project === 'function') ? project : null; + this.values = values; + } + + protected _next(value: any) { + const iterators = this.iterators; + if (isArray(value)) { + iterators.push(new StaticArrayIterator(value)); + } else if (typeof value[Symbol_iterator] === 'function') { + iterators.push(new StaticIterator(value[Symbol_iterator]())); + } else { + iterators.push(new ZipBufferIterator(this.destination, this, value)); + } + } + + protected _complete() { + const iterators = this.iterators; + const len = iterators.length; + + if (len === 0) { + this.destination.complete(); + return; + } + + this.active = len; + for (let i = 0; i < len; i++) { + let iterator: ZipBufferIterator = iterators[i]; + if (iterator.stillUnsubscribed) { + this.add(iterator.subscribe(iterator, i)); + } else { + this.active--; // not an observable + } + } + } + + notifyInactive() { + this.active--; + if (this.active === 0) { + this.destination.complete(); + } + } + + checkIterators() { + const iterators = this.iterators; + const len = iterators.length; + const destination = this.destination; + + // abort if not all of them have values + for (let i = 0; i < len; i++) { + let iterator = iterators[i]; + if (typeof iterator.hasValue === 'function' && !iterator.hasValue()) { + return; + } + } + + let shouldComplete = false; + const args: any[] = []; + for (let i = 0; i < len; i++) { + let iterator = iterators[i]; + let result = iterator.next(); + + // check to see if it's completed now that you've gotten + // the next value. + if (iterator.hasCompleted()) { + shouldComplete = true; + } + + if (result.done) { + destination.complete(); + return; + } + + args.push(result.value); + } + + if (this.project) { + this._tryProject(args); + } else { + destination.next(args); + } + + if (shouldComplete) { + destination.complete(); + } + } + + protected _tryProject(args: any[]) { + let result: any; + try { + result = this.project.apply(this, args); + } catch (err) { + this.destination.error(err); + return; + } + this.destination.next(result); + } +} + +interface LookAheadIterator extends Iterator { + hasValue(): boolean; + hasCompleted(): boolean; +} + +class StaticIterator implements LookAheadIterator { + private nextResult: IteratorResult; + + constructor(private iterator: Iterator) { + this.nextResult = iterator.next(); + } + + hasValue() { + return true; + } + + next(): IteratorResult { + const result = this.nextResult; + this.nextResult = this.iterator.next(); + return result; + } + + hasCompleted() { + const nextResult = this.nextResult; + return nextResult && nextResult.done; + } +} + +class StaticArrayIterator implements LookAheadIterator { + private index = 0; + private length = 0; + + constructor(private array: T[]) { + this.length = array.length; + } + + [Symbol_iterator]() { + return this; + } + + next(value?: any): IteratorResult { + const i = this.index++; + const array = this.array; + return i < this.length ? { value: array[i], done: false } : { value: null, done: true }; + } + + hasValue() { + return this.array.length > this.index; + } + + hasCompleted() { + return this.array.length === this.index; + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +class ZipBufferIterator extends OuterSubscriber implements LookAheadIterator { + stillUnsubscribed = true; + buffer: T[] = []; + isComplete = false; + + constructor(destination: PartialObserver, + private parent: ZipSubscriber, + private observable: Observable) { + super(destination); + } + + [Symbol_iterator]() { + return this; + } + + // NOTE: there is actually a name collision here with Subscriber.next and Iterator.next + // this is legit because `next()` will never be called by a subscription in this case. + next(): IteratorResult { + const buffer = this.buffer; + if (buffer.length === 0 && this.isComplete) { + return { value: null, done: true }; + } else { + return { value: buffer.shift(), done: false }; + } + } + + hasValue() { + return this.buffer.length > 0; + } + + hasCompleted() { + return this.buffer.length === 0 && this.isComplete; + } + + notifyComplete() { + if (this.buffer.length > 0) { + this.isComplete = true; + this.parent.notifyInactive(); + } else { + this.destination.complete(); + } + } + + notifyNext(outerValue: T, innerValue: any, + outerIndex: number, innerIndex: number, + innerSub: InnerSubscriber): void { + this.buffer.push(innerValue); + this.parent.checkIterators(); + } + + subscribe(value: any, index: number) { + return subscribeToResult(this, this.observable, this, index); + } +} diff --git a/src/util/identity.ts b/src/util/identity.ts new file mode 100644 index 0000000000..6589842c03 --- /dev/null +++ b/src/util/identity.ts @@ -0,0 +1,3 @@ +export function identity(x: T): T { + return x; +} diff --git a/src/util/pipe.ts b/src/util/pipe.ts new file mode 100644 index 0000000000..c67e10c5e5 --- /dev/null +++ b/src/util/pipe.ts @@ -0,0 +1,34 @@ +import { noop } from './noop'; +import { UnaryFunction } from '../interfaces'; + +/* tslint:disable:max-line-length */ +export function pipe(): UnaryFunction; +export function pipe(op1: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction, op5: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction, op5: UnaryFunction, op6: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction, op5: UnaryFunction, op6: UnaryFunction, op7: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction, op5: UnaryFunction, op6: UnaryFunction, op7: UnaryFunction, op8: UnaryFunction): UnaryFunction; +export function pipe(op1: UnaryFunction, op2: UnaryFunction, op3: UnaryFunction, op4: UnaryFunction, op5: UnaryFunction, op6: UnaryFunction, op7: UnaryFunction, op8: UnaryFunction, op9: UnaryFunction): UnaryFunction; +/* tslint:enable:max-line-length */ + +export function pipe(...fns: Array>): UnaryFunction { + return pipeFromArray(fns); +} + +/* @internal */ +export function pipeFromArray(fns: Array>): UnaryFunction { + if (!fns) { + return noop as UnaryFunction; + } + + if (fns.length === 1) { + return fns[0]; + } + + return function piped(input: T): R { + return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input); + }; +}