diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index fcc54fcd84..10dac1e61b 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -21,16 +21,15 @@ export declare const async: AsyncScheduler; export declare const asyncScheduler: AsyncScheduler; export declare class AsyncSubject extends Subject { - _subscribe(subscriber: Subscriber): Subscription; + protected _checkFinalizedStatuses(subscriber: Subscriber): void; complete(): void; - error(error: any): void; next(value: T): void; } export declare class BehaviorSubject extends Subject { get value(): T; constructor(_value: T); - _subscribe(subscriber: Subscriber): Subscription; + protected _subscribe(subscriber: Subscriber): Subscription; getValue(): T; next(value: T): void; } @@ -197,14 +196,14 @@ export declare const config: { }; export declare class ConnectableObservable extends Observable { - protected _connection: Subscription | null | undefined; - _isComplete: boolean; + protected _connection: Subscription | null; protected _refCount: number; - protected _subject: Subject | undefined; + protected _subject: Subject | null; source: Observable; protected subjectFactory: () => Subject; constructor(source: Observable, subjectFactory: () => Subject); - _subscribe(subscriber: Subscriber): Subscription; + protected _subscribe(subscriber: Subscriber): Subscription; + protected _teardown(): void; connect(): Subscription; protected getSubject(): Subject; refCount(): Observable; @@ -275,10 +274,8 @@ export declare function generate(initialState: S, condition: ConditionFunc export declare function generate(options: GenerateBaseOptions): Observable; export declare function generate(options: GenerateOptions): Observable; -export declare class GroupedObservable extends Observable { - key: K; - constructor(key: K, groupSubject: Subject, refCountSubscription?: RefCountSubscription | undefined); - _subscribe(subscriber: Subscriber): Subscription; +export interface GroupedObservable extends Observable { + readonly key: K; } export declare type Head = ((...args: X) => any) extends ((arg: infer U, ...rest: any[]) => any) ? U : never; @@ -310,17 +307,17 @@ export declare function merge(v1: ObservableInput, v2: Obs export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler: SchedulerLike): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; export declare function merge(v1: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, concurrent: number): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, v2: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, v2: ObservableInput, concurrent: number): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, concurrent: number): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, concurrent: number): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, concurrent: number): Observable; export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): Observable; -export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent?: number): Observable; +export declare function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent: number): Observable; export declare function merge(...observables: (ObservableInput | number)[]): Observable; export declare function merge(...observables: (ObservableInput | SchedulerLike | number)[]): Observable; export declare function merge(...observables: (ObservableInput | number)[]): Observable; @@ -498,7 +495,8 @@ export declare function range(start?: number, count?: number, scheduler?: Schedu export declare class ReplaySubject extends Subject { constructor(bufferSize?: number, windowTime?: number, timestampProvider?: TimestampProvider); - _subscribe(subscriber: Subscriber): Subscription; + protected _subscribe(subscriber: Subscriber): Subscription; + next(value: T): void; } export declare function scheduled(input: ObservableInput, scheduler: SchedulerLike): Observable; @@ -530,8 +528,11 @@ export declare class Subject extends Observable implements SubscriptionLik observers: Observer[]; thrownError: any; constructor(); - _subscribe(subscriber: Subscriber): Subscription; - _trySubscribe(subscriber: Subscriber): TeardownLogic; + protected _checkFinalizedStatuses(subscriber: Subscriber): void; + protected _innerSubscribe(subscriber: Subscriber): Subscription; + protected _subscribe(subscriber: Subscriber): Subscription; + protected _throwIfClosed(): void; + protected _trySubscribe(subscriber: Subscriber): TeardownLogic; asObservable(): Observable; complete(): void; error(err: any): void; diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index ff43e7dbf5..68c73b4872 100644 --- a/api_guard/dist/types/operators/index.d.ts +++ b/api_guard/dist/types/operators/index.d.ts @@ -12,7 +12,7 @@ export declare function bufferTime(bufferTimeSpan: number, bufferCreationInte export declare function bufferToggle(openings: SubscribableOrPromise, closingSelector: (value: O) => SubscribableOrPromise): OperatorFunction; -export declare function bufferWhen(closingSelector: () => Observable): OperatorFunction; +export declare function bufferWhen(closingSelector: () => ObservableInput): OperatorFunction; export declare function catchError>(selector: (err: any, caught: Observable) => O): OperatorFunction>; @@ -180,7 +180,7 @@ export declare function mergeMapTo>(innerOb export declare function mergeScan(accumulator: (acc: R, value: T, index: number) => ObservableInput, seed: R, concurrent?: number): OperatorFunction; export declare function mergeWith(): OperatorFunction; -export declare function mergeWith[]>(...otherSources: A): OperatorFunction)>; +export declare function mergeWith[]>(...otherSources: A): OperatorFunction>; export declare function min(comparer?: (x: T, y: T) => number): MonoTypeOperatorFunction; @@ -260,7 +260,7 @@ export declare function single(predicate?: (value: T, index: number, source: export declare function skip(count: number): MonoTypeOperatorFunction; -export declare function skipLast(count: number): MonoTypeOperatorFunction; +export declare function skipLast(skipCount: number): MonoTypeOperatorFunction; export declare function skipUntil(notifier: Observable): MonoTypeOperatorFunction; @@ -292,7 +292,7 @@ export declare function take(count: number): MonoTypeOperatorFunction; export declare function takeLast(count: number): MonoTypeOperatorFunction; -export declare function takeUntil(notifier: Observable): MonoTypeOperatorFunction; +export declare function takeUntil(notifier: ObservableInput): MonoTypeOperatorFunction; export declare function takeWhile(predicate: (value: T, index: number) => value is S): OperatorFunction; export declare function takeWhile(predicate: (value: T, index: number) => value is S, inclusive: false): OperatorFunction; @@ -304,11 +304,11 @@ export declare function tap(next: (value: T) => void, error: null | undefined export declare function tap(next?: (x: T) => void, error?: (e: any) => void, complete?: () => void): MonoTypeOperatorFunction; export declare function tap(observer: PartialObserver): MonoTypeOperatorFunction; -export declare function throttle(durationSelector: (value: T) => SubscribableOrPromise, config?: ThrottleConfig): MonoTypeOperatorFunction; +export declare function throttle(durationSelector: (value: T) => SubscribableOrPromise, { leading, trailing }?: ThrottleConfig): MonoTypeOperatorFunction; -export declare function throttleTime(duration: number, scheduler?: SchedulerLike, config?: ThrottleConfig): MonoTypeOperatorFunction; +export declare function throttleTime(duration: number, scheduler?: SchedulerLike, { leading, trailing }?: ThrottleConfig): MonoTypeOperatorFunction; -export declare function throwIfEmpty(errorFactory?: (() => any)): MonoTypeOperatorFunction; +export declare function throwIfEmpty(errorFactory?: () => any): MonoTypeOperatorFunction; export declare function timeInterval(scheduler?: SchedulerLike): OperatorFunction>; @@ -332,11 +332,11 @@ export declare function windowCount(windowSize: number, startWindowEvery?: nu export declare function windowTime(windowTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction>; export declare function windowTime(windowTimeSpan: number, windowCreationInterval: number, scheduler?: SchedulerLike): OperatorFunction>; -export declare function windowTime(windowTimeSpan: number, windowCreationInterval: number, maxWindowSize: number, scheduler?: SchedulerLike): OperatorFunction>; +export declare function windowTime(windowTimeSpan: number, windowCreationInterval: number | null | void, maxWindowSize: number, scheduler?: SchedulerLike): OperatorFunction>; -export declare function windowToggle(openings: Observable, closingSelector: (openValue: O) => Observable): OperatorFunction>; +export declare function windowToggle(openings: ObservableInput, closingSelector: (openValue: O) => ObservableInput): OperatorFunction>; -export declare function windowWhen(closingSelector: () => Observable): OperatorFunction>; +export declare function windowWhen(closingSelector: () => ObservableInput): OperatorFunction>; export declare function withLatestFrom(project: (v1: T) => R): OperatorFunction; export declare function withLatestFrom, R>(source2: O2, project: (v1: T, v2: ObservedValueOf) => R): OperatorFunction; diff --git a/spec/observables/IteratorObservable-spec.ts b/spec/observables/IteratorObservable-spec.ts deleted file mode 100644 index 7359f6d148..0000000000 --- a/spec/observables/IteratorObservable-spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { expect } from 'chai'; -import { fromIterable } from 'rxjs/internal/observable/fromIterable'; -import { iterator as symbolIterator } from 'rxjs/internal/symbol/iterator'; -import { TestScheduler } from 'rxjs/testing'; -import { Notification, queueScheduler, Subscriber } from 'rxjs'; -import { observeOn, materialize, take, toArray } from 'rxjs/operators'; - -declare const expectObservable: any; -declare const rxTestScheduler: TestScheduler; - -describe('fromIterable', () => { - it('should not accept null (or truthy-equivalent to null) iterator', () => { - expect(() => { - fromIterable(null as any, undefined); - }).to.throw(Error, 'Iterable cannot be null'); - expect(() => { - fromIterable(void 0 as any, undefined); - }).to.throw(Error, 'Iterable cannot be null'); - }); - - it('should emit members of an array iterator', (done) => { - const expected = [10, 20, 30, 40]; - fromIterable([10, 20, 30, 40], undefined) - .subscribe( - (x) => { expect(x).to.equal(expected.shift()); }, - (x) => { - done(new Error('should not be called')); - }, () => { - expect(expected.length).to.equal(0); - done(); - } - ); - }); - - it('should get new iterator for each subscription', () => { - const expected = [ - Notification.createNext(10), - Notification.createNext(20), - Notification.createComplete() - ]; - - const e1 = fromIterable(new Int32Array([10, 20]), undefined).pipe(observeOn(rxTestScheduler)); - - let v1, v2: Array>; - e1.pipe(materialize(), toArray()).subscribe((x) => v1 = x); - e1.pipe(materialize(), toArray()).subscribe((x) => v2 = x); - - rxTestScheduler.flush(); - expect(v1).to.deep.equal(expected); - expect(v2!).to.deep.equal(expected); - }); - - it('should finalize generators if the subscription ends', () => { - const iterator = { - finalized: false, - next() { - return { value: 'duck', done: false }; - }, - return() { - this.finalized = true; - } - }; - - const iterable = { - [symbolIterator]() { - return iterator; - } - }; - - const results: any[] = []; - - fromIterable(iterable as any, undefined) - .pipe(take(3)) - .subscribe( - x => results.push(x), - null, - () => results.push('GOOSE!') - ); - - expect(results).to.deep.equal(['duck', 'duck', 'duck', 'GOOSE!']); - expect(iterator.finalized).to.be.true; - }); - - it('should finalize generators if the subscription and it is scheduled', () => { - const iterator = { - finalized: false, - next() { - return { value: 'duck', done: false }; - }, - return() { - this.finalized = true; - } - }; - - const iterable = { - [symbolIterator]() { - return iterator; - } - }; - - const results: any[] = []; - - fromIterable(iterable as any, queueScheduler) - .pipe(take(3)) - .subscribe( - x => results.push(x), - null, - () => results.push('GOOSE!') - ); - - expect(results).to.deep.equal(['duck', 'duck', 'duck', 'GOOSE!']); - expect(iterator.finalized).to.be.true; - }); - - it('should emit members of an array iterator on a particular scheduler', () => { - const source = fromIterable( - [10, 20, 30, 40], - rxTestScheduler - ); - - const values = { a: 10, b: 20, c: 30, d: 40 }; - - expectObservable(source).toBe('(abcd|)', values); - }); - - it('should emit members of an array iterator on a particular scheduler, ' + - 'but is unsubscribed early', (done) => { - const expected = [10, 20, 30, 40]; - - const source = fromIterable( - [10, 20, 30, 40], - queueScheduler - ); - - const subscriber = Subscriber.create( - (x) => { - expect(x).to.equal(expected.shift()); - if (x === 30) { - subscriber.unsubscribe(); - done(); - } - }, (x) => { - done(new Error('should not be called')); - }, () => { - done(new Error('should not be called')); - }); - - source.subscribe(subscriber); - }); - - it('should emit characters of a string iterator', (done) => { - const expected = ['f', 'o', 'o']; - fromIterable('foo', undefined) - .subscribe( - (x) => { expect(x).to.equal(expected.shift()); }, - (x) => { - done(new Error('should not be called')); - }, () => { - expect(expected.length).to.equal(0); - done(); - } - ); - }); - - it('should be possible to unsubscribe in the middle of the iteration', (done) => { - const expected = [10, 20, 30]; - - const subscriber = Subscriber.create( - (x) => { - expect(x).to.equal(expected.shift()); - if (x === 30) { - subscriber.unsubscribe(); - done(); - } - }, (x) => { - done(new Error('should not be called')); - }, () => { - done(new Error('should not be called')); - } - ); - - fromIterable([10, 20, 30, 40, 50, 60], undefined).subscribe(subscriber); - }); -}); diff --git a/spec/observables/fromEvent-spec.ts b/spec/observables/fromEvent-spec.ts index cffdb64992..ee76bb72ca 100644 --- a/spec/observables/fromEvent-spec.ts +++ b/spec/observables/fromEvent-spec.ts @@ -392,4 +392,49 @@ describe('fromEvent', () => { }).to.not.throw(TypeError); }); + it('should handle adding events to an arraylike of targets', () => { + const nodeList = { + [0]: { + addEventListener(...args: any[]) { + this._addEventListenerArgs = args; + }, + removeEventListener(...args: any[]) { + this._removeEventListenerArgs = args; + }, + _addEventListenerArgs: null as any, + _removeEventListenerArgs: null as any, + }, + [1]: { + addEventListener(...args: any[]) { + this._addEventListenerArgs = args; + }, + removeEventListener(...args: any[]) { + this._removeEventListenerArgs = args; + }, + _addEventListenerArgs: null as any, + _removeEventListenerArgs: null as any, + }, + length: 2 + }; + + const options = {}; + + const subscription = fromEvent(nodeList, 'click', options).subscribe(); + + expect(nodeList[0]._addEventListenerArgs[0]).to.equal('click'); + expect(nodeList[0]._addEventListenerArgs[1]).to.be.a('function'); + expect(nodeList[0]._addEventListenerArgs[2]).to.equal(options); + + expect(nodeList[1]._addEventListenerArgs[0]).to.equal('click'); + expect(nodeList[1]._addEventListenerArgs[1]).to.be.a('function'); + expect(nodeList[1]._addEventListenerArgs[2]).to.equal(options); + + expect(nodeList[0]._removeEventListenerArgs).to.be.null; + expect(nodeList[1]._removeEventListenerArgs).to.be.null; + + subscription.unsubscribe(); + + expect(nodeList[0]._removeEventListenerArgs).to.deep.equal(nodeList[0]._addEventListenerArgs); + expect(nodeList[1]._removeEventListenerArgs).to.deep.equal(nodeList[1]._addEventListenerArgs); + }); }); diff --git a/spec/operators/debounce-spec.ts b/spec/operators/debounce-spec.ts index a8af7c209e..61d1a3f386 100644 --- a/spec/operators/debounce-spec.ts +++ b/spec/operators/debounce-spec.ts @@ -258,9 +258,9 @@ describe('debounce operator', () => { const e1subs = '^ !'; const expected = '---------a---------b---------c-------#'; const selector = [cold( '-x-y-'), - cold( '--x-y-'), - cold( '---x-y-'), - cold( '----x-y-')]; + cold( '--x-y-'), + cold( '---x-y-'), + cold( '----x-y-')]; const selectorSubs = [' ^! ', ' ^ ! ', diff --git a/spec/operators/delay-spec.ts b/spec/operators/delay-spec.ts index c16134d607..a921b3289c 100644 --- a/spec/operators/delay-spec.ts +++ b/spec/operators/delay-spec.ts @@ -28,11 +28,11 @@ describe('delay operator', () => { }); it('should delay by absolute time period', () => { - testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { - const e1 = hot(' --a--b--| '); - const t = 3; // ---| - const expected = '-----a--(b|)'; - const subs = ' ^-------! '; + testScheduler.run(({ hot, time, expectObservable, expectSubscriptions }) => { + const e1 = hot(' --a--b-------------c----d--| '); + const t = time(' -------|'); + const expected = '-------(ab)--------c----d--|'; + const subs = ' ^--------------------------! '; const absoluteDelay = new Date(testScheduler.now() + t); const result = e1.pipe(delay(absoluteDelay, testScheduler)); @@ -42,11 +42,11 @@ describe('delay operator', () => { }); }); - it('should delay by absolute time period after subscription', () => { - testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + it('should delay by absolute time period after complete', () => { + testScheduler.run(({ hot, time, expectObservable, expectSubscriptions }) => { const e1 = hot(' ---^--a--b--| '); - const t = 3; // ---| - const expected = ' ------a--(b|)'; + const t = time(' ------------|') + const expected = ' ------------(ab|)'; const subs = ' ^--------! '; const absoluteDelay = new Date(testScheduler.now() + t); @@ -72,10 +72,10 @@ describe('delay operator', () => { }); it('should raise error when source raises error', () => { - testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + testScheduler.run(({ hot, time, expectObservable, expectSubscriptions }) => { const e1 = hot(' --a--b--#'); - const t = 3; // ---| - const expected = '-----a--#'; + const t = time(' -----------|'); + const expected = '--------#'; const subs = ' ^-------!'; const absoluteDelay = new Date(testScheduler.now() + t); @@ -86,12 +86,12 @@ describe('delay operator', () => { }); }); - it('should raise error when source raises error after subscription', () => { - testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { - const e1 = hot(' ---^---a---b---#'); - const t = 3; // ---| - const expected = ' -------a---b#'; - const e1Sub = ' ^-----------!'; + it('should raise error when source raises error after subscription when Date is passed', () => { + testScheduler.run(({ hot, time, expectObservable, expectSubscriptions }) => { + const e1 = hot(' ---^---a---b-------c----#'); + const t = time(' ---------|') + const expected = ' ---------(ab)---c----#'; + const e1Sub = ' ^--------------------!'; const absoluteDelay = new Date(testScheduler.now() + t); const result = e1.pipe(delay(absoluteDelay, testScheduler)); diff --git a/spec/operators/expand-spec.ts b/spec/operators/expand-spec.ts index 5086f7cd84..13ffa83df5 100644 --- a/spec/operators/expand-spec.ts +++ b/spec/operators/expand-spec.ts @@ -7,7 +7,7 @@ import { Subscribable, EMPTY, Observable, of, Observer, asapScheduler, asyncSche declare const rxTestScheduler: TestScheduler; /** @test {expand} */ -describe('expand operator', () => { +describe('expand', () => { it('should recursively map-and-flatten each item to an Observable', () => { const e1 = hot('--x----| ', {x: 1}); const e1subs = '^ ! '; diff --git a/spec/operators/find-spec.ts b/spec/operators/find-spec.ts index 4108933a07..8db38c965a 100644 --- a/spec/operators/find-spec.ts +++ b/spec/operators/find-spec.ts @@ -24,12 +24,6 @@ describe('find operator', () => { expectSubscriptions(source.subscriptions).toBe(subs); }); - it('should throw if not provided a function', () => { - expect(() => { - of('yut', 'yee', 'sam').pipe(find('yee' as any)); - }).to.throw(TypeError, 'predicate is not a function'); - }); - it('should not emit if source does not emit', () => { const source = hot('-'); const subs = '^'; diff --git a/spec/operators/groupBy-spec.ts b/spec/operators/groupBy-spec.ts index 3c98dd4721..df671f32ad 100644 --- a/spec/operators/groupBy-spec.ts +++ b/spec/operators/groupBy-spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; -import { groupBy, delay, tap, map, take, mergeMap, materialize, skip } from 'rxjs/operators'; +import { groupBy, delay, tap, map, take, mergeMap, materialize, skip, ignoreElements } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; -import { ReplaySubject, of, GroupedObservable, Observable, Operator, Observer } from 'rxjs'; +import { ReplaySubject, of, Observable, Operator, Observer, interval, Subject } from 'rxjs'; import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; import { createNotification } from 'rxjs/internal/Notification'; @@ -216,7 +216,7 @@ describe('groupBy operator', () => { groupBy((val: string) => val.toLowerCase().trim()), tap((group: any) => { expect(group.key).to.equal('foo'); - expect(group instanceof GroupedObservable).to.be.true; + expect(group instanceof Observable).to.be.true; }), map((group: any) => { return group.key; }) ); @@ -1371,54 +1371,35 @@ describe('groupBy operator', () => { expectSubscriptions(e1.subscriptions).toBe(e1subs); }); - it('should return inner that does not throw when faulty outer is unsubscribed early', - () => { - const values = { - a: ' foo', - b: ' FoO ', - d: 'foO ', - i: 'FOO ', - l: ' fOo ' + it('should not error for late subscribed inners if outer is unsubscribed before inners are subscribed', () => { + const source = hot('-----^----a----b-----a------b----a----b---#'); + // Unsubscribe before the error happens. + const unsub = ' !'; + // Used to hold two subjects we're going to use to subscribe to our groups + const subjects: Record> = { + a: new Subject(), + b: new Subject() }; - const e1 = hot('-1--2--^-a-b---d---------i-----l-#', values); - const unsub = ' !'; - const expectedSubs = '^ !'; - const expected = '--g----'; - const innerSub = ' ^'; - const g = '-'; - - const expectedGroups = { - g: TestScheduler.parseMarbles(g, values) - }; - - const innerSubscriptionFrame = TestScheduler - .parseMarblesAsSubscriptions(innerSub) - .subscribedFrame; - - const source = e1.pipe( - groupBy( - (val: string) => val.toLowerCase().trim(), - (val: string) => val, - (group: any) => group.pipe(skip(7)) - ), - map((group: any) => { - const arr: any[] = []; - - rxTestScheduler.schedule(() => { - group.pipe( - phonyMarbelize() - ).subscribe((value: any) => { - arr.push(value); - }); - }, innerSubscriptionFrame - rxTestScheduler.frame); - - return arr; - }) + const result = source.pipe( + groupBy(char => char), + tap({ + // The real test is here, schedule each group to be subscribed to + // long after the source errors and long after the unsubscription happens. + next: group => { + rxTestScheduler.schedule( + () => group.subscribe(subjects[group.key]), 1000 + ); + } + }), + // We don't are about what the outer is emitting + ignoreElements() ); - - expectObservable(source, unsub).toBe(expected, expectedGroups); - expectSubscriptions(e1.subscriptions).toBe(expectedSubs); - }); + // Just to get the test going. + expectObservable(result, unsub).toBe('-'); + // Our two groups should error immediately upon subscription. + expectObservable(subjects.a).toBe('-'); + expectObservable(subjects.b).toBe('-'); + }) it('should not break lift() composability', (done: MochaDone) => { class MyCustomObservable extends Observable { diff --git a/spec/operators/map-spec.ts b/spec/operators/map-spec.ts index 691402cdc7..55d62b9e8a 100644 --- a/spec/operators/map-spec.ts +++ b/spec/operators/map-spec.ts @@ -1,11 +1,10 @@ import { expect } from 'chai'; import { map, tap, mergeMap, take } from 'rxjs/operators'; import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; -import { of, Observable } from 'rxjs'; +import { of, Observable, identity } from 'rxjs'; // function shortcuts const addDrama = function (x: number | string) { return x + '!'; }; -const identity = function (x: T) { return x; }; /** @test {map} */ describe('map operator', () => { @@ -31,12 +30,6 @@ describe('map operator', () => { expectSubscriptions(a.subscriptions).toBe(asubs); }); - it('should throw an error if not passed a function', () => { - expect(() => { - of(1, 2, 3).pipe(map('potato')); - }).to.throw(TypeError, 'argument is not a function. Are you looking for `mapTo()`?'); - }); - it('should map multiple values', () => { const a = cold('--1--2--3--|'); const asubs = '^ !'; diff --git a/spec/operators/observeOn-spec.ts b/spec/operators/observeOn-spec.ts index 7448136a0a..8e78af2d63 100644 --- a/spec/operators/observeOn-spec.ts +++ b/spec/operators/observeOn-spec.ts @@ -81,48 +81,6 @@ describe('observeOn operator', () => { expectSubscriptions(e1.subscriptions).toBe(sub); }); - it('should clean up subscriptions created by async scheduling (prevent memory leaks #2244)', (done) => { - //HACK: Deep introspection to make sure we're cleaning up notifications in scheduling. - // as the architecture changes, this test may become brittle. - const results: number[] = []; - // This is to build a scheduled observable with a slightly more stable - // subscription structure, since we're going to hack in to analyze it in this test. - const subscription: any = new Observable(observer => { - let i = 1; - return asapScheduler.schedule(function () { - if (i > 3) { - observer.complete(); - } else { - observer.next(i++); - this.schedule(); - } - }); - }) - .pipe(observeOn(asapScheduler)) - .subscribe( - x => { - // see #4106 - inner subscriptions are now added to destinations - // so the subscription will contain an ObserveOnSubscriber and a subscription for the scheduled action - expect(subscription._teardowns.length).to.equal(2); - const actionSubscription = subscription._teardowns[1]; - expect(actionSubscription.state.notification.kind).to.equal('N'); - expect(actionSubscription.state.notification.value).to.equal(x); - results.push(x); - }, - err => done(err), - () => { - // now that the last nexted value is done, there should only be a complete notification scheduled - // the consumer will have been unsubscribed via Subscriber#_parentSubscription - expect(subscription._teardowns.length).to.equal(1); - const actionSubscription = subscription._teardowns[0]; - expect(actionSubscription.state.notification.kind).to.equal('C'); - // After completion, the entire _teardowns list is nulled out anyhow, so we can't test much further than this. - expect(results).to.deep.equal([1, 2, 3]); - done(); - } - ); - }); - it('should stop listening to a synchronous observable when unsubscribed', () => { const sideEffects: number[] = []; const synchronousObservable = new Observable(subscriber => { diff --git a/spec/operators/scan-spec.ts b/spec/operators/scan-spec.ts index 334d109b9c..241fa118f2 100644 --- a/spec/operators/scan-spec.ts +++ b/spec/operators/scan-spec.ts @@ -42,6 +42,16 @@ describe('scan operator', () => { expectSubscriptions(e1.subscriptions).toBe(e1subs); }); + it('should provide the proper index if seed is skipped', () => { + const expected = [1, 2]; + of(3, 3, 3).pipe( + scan((_: any, __, i) => { + expect(i).to.equal(expected.shift()); + return null; + }) + ).subscribe(); + }); + it('should scan with a seed of undefined', () => { const e1 = hot('--a--^--b--c--d--e--f--g--|'); const e1subs = '^ !'; diff --git a/spec/operators/skipLast-spec.ts b/spec/operators/skipLast-spec.ts index 3ab6efdb9f..f29b563f6b 100644 --- a/spec/operators/skipLast-spec.ts +++ b/spec/operators/skipLast-spec.ts @@ -132,11 +132,6 @@ describe('skipLast operator', () => { expectSubscriptions(e1.subscriptions).toBe(e1subs); }); - it('should throw if total is less than zero', () => { - expect(() => { range(0, 10).pipe(skipLast(-1)); }) - .to.throw(ArgumentOutOfRangeError); - }); - it('should not break unsubscription chain when unsubscribed explicitly', () => { const e1 = hot('---^--a--b-----c--d--e--|'); const unsub = ' ! '; diff --git a/spec/operators/take-spec.ts b/spec/operators/take-spec.ts index b666bd3e06..f3a47c23c9 100644 --- a/spec/operators/take-spec.ts +++ b/spec/operators/take-spec.ts @@ -5,34 +5,13 @@ import { TestScheduler } from 'rxjs/testing'; import { observableMatcher } from '../helpers/observableMatcher'; /** @test {take} */ -describe('take operator', () => { +describe('take', () => { let testScheduler: TestScheduler; beforeEach(() => { testScheduler = new TestScheduler(observableMatcher); }); - it('should error when a non-number is passed to it, or when no argument is passed (Non-TS case)', () => { - expect(() => { - of(1, 2, 3).pipe( - (take as any)() - ); - }).to.throw(TypeError, `'count' is not a number`); - - expect(() => { - of(1, 2, 3).pipe( - (take as any)('banana') - ); - }).to.throw(TypeError, `'count' is not a number`); - - // Standard type coersion behavior in JS. - expect(() => { - of(1, 2, 3).pipe( - (take as any)('1') - ); - }).not.to.throw(); - }); - it('should take two values of an observable with many values', () => { testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { const e1 = cold(' --a-----b----c---d--|'); @@ -155,11 +134,6 @@ describe('take operator', () => { }); }); - it('should throw if total is less than zero', () => { - expect(() => { range(0, 10).pipe(take(-1)); }) - .to.throw(ArgumentOutOfRangeError); - }); - it('should not break unsubscription chain when unsubscribed explicitly', () => { testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { const e1 = hot('---^--a--b-----c--d--e--|'); diff --git a/spec/operators/takeLast-spec.ts b/spec/operators/takeLast-spec.ts index 3966096dc6..d2164a4749 100644 --- a/spec/operators/takeLast-spec.ts +++ b/spec/operators/takeLast-spec.ts @@ -12,20 +12,6 @@ describe('takeLast operator', () => { rxTest = new TestScheduler(observableMatcher); }); - it('should error for invalid arguments', () => { - expect(() => { - of(1, 2, 3).pipe((takeLast as any)()); - }).to.throw(TypeError, `'count' is not a number`); - - expect(() => { - of(1, 2, 3).pipe((takeLast as any)('banana')); - }).to.throw(TypeError, `'count' is not a number`); - - expect(() => { - of(1, 2, 3).pipe((takeLast as any)('3')); - }).not.to.throw(); - }); - it('should take two values of an observable with many values', () => { rxTest.run(({ cold, expectObservable, expectSubscriptions }) => { const e1 = cold('--a-----b----c---d--| '); @@ -190,12 +176,6 @@ describe('takeLast operator', () => { }); }); - it('should throw if total is less than zero', () => { - expect(() => { - range(0, 10).pipe(takeLast(-1)); - }).to.throw(ArgumentOutOfRangeError); - }); - it('should not break unsubscription chain when unsubscribed explicitly', () => { rxTest.run(({ hot, expectObservable, expectSubscriptions }) => { const e1 = hot('---^--a--b-----c--d--e--|'); diff --git a/spec/operators/takeWhile-spec.ts b/spec/operators/takeWhile-spec.ts index d45fb5d120..96a72dcdde 100644 --- a/spec/operators/takeWhile-spec.ts +++ b/spec/operators/takeWhile-spec.ts @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; import { takeWhile, tap, mergeMap } from 'rxjs/operators'; import { of, Observable, from } from 'rxjs'; -import { values } from 'lodash'; /** @test {takeWhile} */ describe('takeWhile operator', () => { diff --git a/spec/operators/windowTime-spec.ts b/spec/operators/windowTime-spec.ts index a4bf49bf17..347114a337 100644 --- a/spec/operators/windowTime-spec.ts +++ b/spec/operators/windowTime-spec.ts @@ -4,7 +4,7 @@ import { of, Observable } from 'rxjs'; import { observableMatcher } from '../helpers/observableMatcher'; /** @test {windowTime} */ -describe('windowTime operator', () => { +describe('windowTime', () => { let rxTestScheduler: TestScheduler; beforeEach(() => { @@ -32,19 +32,26 @@ describe('windowTime operator', () => { }); }); + // NOTE: This test and behavior were broken in 5.x and 6.x, to where + // Not passing a creationInterval would not cause new windows to open + // when old ones closed. it('should close windows after max count is reached', () => { rxTestScheduler.run(({ hot, time, cold, expectObservable, expectSubscriptions }) => { const source = hot('--1--2--^--a--b--c--d--e--f--g-----|'); const subs = '^--------------------------!'; const timeSpan = time( '----------|'); - // 10 frames 0---------1---------2------| - const expected = 'x---------y---------z------|'; - const x = cold( '---a--(b|) '); - const y = cold( '--d--(e|) '); - const z = cold( '-g-----|'); - const values = { x, y, z }; - - const result = source.pipe(windowTime(timeSpan, null as any, 2, rxTestScheduler)); + // ----------| + // ----------| + // ----------| + // --------- + const expected = 'w-----x-----y-----z--------|'; + const w = cold( '---a--(b|) '); + const x = cold( '---c--(d|) '); + const y = cold( '---e--(f|) '); + const z = cold( '---g-----|') + const values = { w, x, y, z }; + + const result = source.pipe(windowTime(timeSpan, null, 2, rxTestScheduler)); expectObservable(result).toBe(expected, values); expectSubscriptions(source.subscriptions).toBe(subs); diff --git a/spec/subjects/AsyncSubject-spec.ts b/spec/subjects/AsyncSubject-spec.ts index 20b65d7f8b..81ef7800a9 100644 --- a/spec/subjects/AsyncSubject-spec.ts +++ b/spec/subjects/AsyncSubject-spec.ts @@ -191,4 +191,42 @@ describe('AsyncSubject', () => { subject.subscribe(observer); expect(observer.results).to.deep.equal([expected]); }); + + it('should not be reentrant via complete', () => { + const subject = new AsyncSubject(); + let calls = 0; + subject.subscribe({ + next: value => { + calls++; + if (calls < 2) { + // if this is more than 1, we're reentrant, and that's bad. + subject.complete(); + } + } + }); + + subject.next(1); + subject.complete(); + + expect(calls).to.equal(1); + }); + + it('should not be reentrant via next', () => { + const subject = new AsyncSubject(); + let calls = 0; + subject.subscribe({ + next: value => { + calls++; + if (calls < 2) { + // if this is more than 1, we're reentrant, and that's bad. + subject.next(value + 1); + } + } + }); + + subject.next(1); + subject.complete(); + + expect(calls).to.equal(1); + }); }); diff --git a/src/internal/AsyncSubject.ts b/src/internal/AsyncSubject.ts index a9369eb79b..2951e88a3a 100644 --- a/src/internal/AsyncSubject.ts +++ b/src/internal/AsyncSubject.ts @@ -1,3 +1,4 @@ +/** @prettier */ import { Subject } from './Subject'; import { Subscriber } from './Subscriber'; import { Subscription } from './Subscription'; @@ -10,40 +11,32 @@ import { Subscription } from './Subscription'; */ export class AsyncSubject extends Subject { private value: T | null = null; - private hasNext: boolean = false; - private hasCompleted: boolean = false; + private hasValue = false; + private isComplete = false; - /** @deprecated This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber): Subscription { - if (this.hasError) { - subscriber.error(this.thrownError); - return Subscription.EMPTY; - } else if (this.hasCompleted && this.hasNext) { - subscriber.next(this.value); + protected _checkFinalizedStatuses(subscriber: Subscriber) { + const { hasError, hasValue, value, thrownError, isStopped } = this; + if (hasError) { + subscriber.error(thrownError); + } else if (isStopped) { + hasValue && subscriber.next(value!); subscriber.complete(); - return Subscription.EMPTY; } - return super._subscribe(subscriber); } next(value: T): void { - if (!this.hasCompleted) { + if (!this.isStopped) { this.value = value; - this.hasNext = true; - } - } - - error(error: any): void { - if (!this.hasCompleted) { - super.error(error); + this.hasValue = true; } } complete(): void { - this.hasCompleted = true; - if (this.hasNext) { - super.next(this.value!); + const { hasValue, value, isComplete } = this; + if (!isComplete) { + this.isComplete = true; + hasValue && super.next(value!); + super.complete(); } - super.complete(); } } diff --git a/src/internal/BehaviorSubject.ts b/src/internal/BehaviorSubject.ts index 20de21c668..d2b6ec2737 100644 --- a/src/internal/BehaviorSubject.ts +++ b/src/internal/BehaviorSubject.ts @@ -1,8 +1,7 @@ +/** @prettier */ import { Subject } from './Subject'; import { Subscriber } from './Subscriber'; import { Subscription } from './Subscription'; -import { SubscriptionLike } from './types'; -import { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError'; /** * A variant of Subject that requires an initial value and emits its current @@ -11,7 +10,6 @@ import { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError'; * @class BehaviorSubject */ export class BehaviorSubject extends Subject { - constructor(private _value: T) { super(); } @@ -21,25 +19,22 @@ export class BehaviorSubject extends Subject { } /** @deprecated This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber): Subscription { + protected _subscribe(subscriber: Subscriber): Subscription { const subscription = super._subscribe(subscriber); - if (subscription && !(subscription).closed) { - subscriber.next(this._value); - } + !subscription.closed && subscriber.next(this._value); return subscription; } getValue(): T { - if (this.hasError) { - throw this.thrownError; - } else if (this.closed) { - throw new ObjectUnsubscribedError(); - } else { - return this._value; + const { hasError, thrownError, _value } = this; + if (hasError) { + throw thrownError; } + this._throwIfClosed(); + return _value; } next(value: T): void { - super.next(this._value = value); + super.next((this._value = value)); } } diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index 1d600f1446..7aea05b3e1 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -1,10 +1,5 @@ -import { - PartialObserver, - ObservableNotification, - CompleteNotification, - NextNotification, - ErrorNotification, -} from './types'; +/** @prettier */ +import { PartialObserver, ObservableNotification, CompleteNotification, NextNotification, ErrorNotification } from './types'; import { Observable } from './Observable'; import { EMPTY } from './observable/empty'; import { of } from './observable/of'; @@ -75,17 +70,7 @@ export class Notification { * @param observer The observer to notify. */ observe(observer: PartialObserver): void { - switch (this.kind) { - case 'N': - observer.next?.(this.value!); - break; - case 'E': - observer.error?.(this.error); - break; - case 'C': - observer.complete?.(); - break; - } + return observeNotification(this as ObservableNotification, observer); } /** @@ -114,19 +99,9 @@ export class Notification { * @deprecated remove in v8. use {@link Notification.prototype.observe} instead. */ do(next: (value: T) => void): void; - do(next: (value: T) => void, error?: (err: any) => void, complete?: () => void): void { - const kind = this.kind; - switch (kind) { - case 'N': - next?.(this.value!); - break; - case 'E': - error?.(this.error); - break; - case 'C': - complete?.(); - break; - } + do(nextHandler: (value: T) => void, errorHandler?: (err: any) => void, completeHandler?: () => void): void { + const { kind, value, error } = this; + return kind === 'N' ? nextHandler?.(value!) : kind === 'E' ? errorHandler?.(error) : completeHandler?.(); } /** @@ -165,11 +140,9 @@ export class Notification { */ accept(observer: PartialObserver): void; accept(nextOrObserver: PartialObserver | ((value: T) => void), error?: (err: any) => void, complete?: () => void) { - if (nextOrObserver && typeof (>nextOrObserver).next === 'function') { - return this.observe(>nextOrObserver); - } else { - return this.do(<(value: T) => void>nextOrObserver, error as any, complete as any); - } + return typeof (nextOrObserver as any)?.next === 'function' + ? this.observe(nextOrObserver as PartialObserver) + : this.do(nextOrObserver as (value: T) => void, error as any, complete as any); } /** @@ -181,16 +154,29 @@ export class Notification { * being removed as it has limited usefulness, and we're trying to streamline the library. */ toObservable(): Observable { - const kind = this.kind; - switch (kind) { - case 'N': - return of(this.value!); - case 'E': - return throwError(this.error); - case 'C': - return EMPTY; + const { kind, value, error } = this; + // Select the observable to return by `kind` + const result = + kind === 'N' + ? // Next kind. Return an observable of that value. + of(value!) + : // + kind === 'E' + ? // Error kind. Return an observable that emits the error. + throwError(error) + : // + kind === 'C' + ? // Completion kind. Kind is "C", return an observable that just completes. + EMPTY + : // Unknown kind, return falsy, so we error below. + 0; + if (!result) { + // TODO: consider removing this check. The only way to cause this would be to + // use the Notification constructor directly in a way that is not type-safe. + // and direct use of the Notification constructor is deprecated. + throw new TypeError(`Unexpected notification kind ${kind}`); } - throw new Error('unexpected notification kind value'); + return result; } private static completeNotification = new Notification('C') as Notification & CompleteNotification; @@ -245,20 +231,11 @@ export class Notification { * @param observer The observer to notify. */ export function observeNotification(notification: ObservableNotification, observer: PartialObserver) { - if (typeof notification.kind !== 'string') { + const { kind, value, error } = notification as any; + if (typeof kind !== 'string') { throw new TypeError('Invalid notification, missing "kind"'); } - switch (notification.kind) { - case 'N': - observer.next?.(notification.value!); - break; - case 'E': - observer.error?.(notification.error); - break; - case 'C': - observer.complete?.(); - break; - } + kind === 'N' ? observer.next?.(value!) : kind === 'E' ? observer.error?.(error) : observer.complete?.(); } /** diff --git a/src/internal/Observable.ts b/src/internal/Observable.ts index 396e30ffc2..77e8872e2b 100644 --- a/src/internal/Observable.ts +++ b/src/internal/Observable.ts @@ -237,13 +237,9 @@ export class Observable implements Subscribable { if (config.useDeprecatedSynchronousErrorHandling) { throw err; } else { - if (canReportError(sink)) { - sink.error(err); - } else { - // If an error is thrown during subscribe, but our subscriber is closed, so we cannot notify via the - // subscription "error" channel, it is an unhandled error and we need to report it appropriately. - reportUnhandledError(err); - } + // If an error is thrown during subscribe, but our subscriber is closed, so we cannot notify via the + // subscription "error" channel, it is an unhandled error and we need to report it appropriately. + canReportError(sink) ? sink.error(err) : reportUnhandledError(err); } } } @@ -320,9 +316,7 @@ export class Observable implements Subscribable { next(value); } catch (err) { reject(err); - if (subscription) { - subscription.unsubscribe(); - } + subscription?.unsubscribe(); } }, reject, @@ -502,11 +496,8 @@ export function canReportError(subscriber: Subscriber): boolean { const { closed, destination, isStopped } = subscriber as any; if (closed || isStopped) { return false; - } else if (destination && destination instanceof Subscriber) { - subscriber = destination; - } else { - subscriber = null!; } + subscriber = destination && destination instanceof Subscriber ? destination : null!; } return true; } diff --git a/src/internal/ReplaySubject.ts b/src/internal/ReplaySubject.ts index 18214a8b34..5f05404bbc 100644 --- a/src/internal/ReplaySubject.ts +++ b/src/internal/ReplaySubject.ts @@ -1,10 +1,9 @@ +/** @prettier */ import { Subject } from './Subject'; import { TimestampProvider } from './types'; import { Subscriber } from './Subscriber'; import { Subscription } from './Subscription'; -import { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError'; -import { SubjectSubscription } from './SubjectSubscription'; -import { dateTimestampProvider } from "./scheduler/dateTimestampProvider"; +import { dateTimestampProvider } from './scheduler/dateTimestampProvider'; /** * A variant of {@link Subject} that "replays" old values to new subscribers by emitting them when they first subscribe. @@ -37,10 +36,8 @@ import { dateTimestampProvider } from "./scheduler/dateTimestampProvider"; * {@see shareReplay} */ export class ReplaySubject extends Subject { - private _events: (ReplayEvent | T)[] = []; - private _bufferSize: number; - private _windowTime: number; - private _infiniteTimeWindow: boolean = false; + private buffer: (T | number)[] = []; + private infiniteTimeWindow = true; /** * @param bufferSize The size of the buffer to replay on subscription @@ -48,117 +45,67 @@ export class ReplaySubject extends Subject { * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to * calculate the amount of time something has been buffered. */ - constructor(bufferSize: number = Infinity, - windowTime: number = Infinity, - private timestampProvider: TimestampProvider = dateTimestampProvider) { + constructor( + private bufferSize = Infinity, + private windowTime = Infinity, + private timestampProvider: TimestampProvider = dateTimestampProvider + ) { super(); - this._bufferSize = bufferSize < 1 ? 1 : bufferSize; - this._windowTime = windowTime < 1 ? 1 : windowTime; - - if (windowTime === Infinity) { - this._infiniteTimeWindow = true; - /** @override */ - this.next = this.nextInfiniteTimeWindow; - } else { - this.next = this.nextTimeWindow; - } - } - - private nextInfiniteTimeWindow(value: T): void { - if (!this.isStopped) { - const _events = this._events; - _events.push(value); - // Since this method is invoked in every next() call than the buffer - // can overgrow the max size only by one item - if (_events.length > this._bufferSize) { - _events.shift(); - } - } - super.next(value); + this.infiniteTimeWindow = windowTime === Infinity; + this.bufferSize = Math.max(1, bufferSize); + this.windowTime = Math.max(1, windowTime); } - private nextTimeWindow(value: T): void { - if (!this.isStopped) { - this._events.push({ time: this._getNow(), value }); - this._trimBufferThenGetEvents(); + next(value: T): void { + const { isStopped, buffer, infiniteTimeWindow, timestampProvider, windowTime } = this; + if (!isStopped) { + buffer.push(value); + !infiniteTimeWindow && buffer.push(timestampProvider.now() + windowTime); } + this.trimBuffer(); super.next(value); } /** @deprecated Remove in v8. This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber): Subscription { - // When `_infiniteTimeWindow === true` then the buffer is already trimmed - const _infiniteTimeWindow = this._infiniteTimeWindow; - const _events = _infiniteTimeWindow ? this._events : this._trimBufferThenGetEvents(); - const len = _events.length; - let subscription: Subscription; - - if (this.closed) { - throw new ObjectUnsubscribedError(); - } else if (this.isStopped || this.hasError) { - subscription = Subscription.EMPTY; - } else { - this.observers.push(subscriber); - subscription = new SubjectSubscription(this, subscriber); - } - - if (_infiniteTimeWindow) { - for (let i = 0; i < len && !subscriber.closed; i++) { - subscriber.next(_events[i]); - } - } else { - for (let i = 0; i < len && !subscriber.closed; i++) { - subscriber.next((>_events[i]).value); - } + protected _subscribe(subscriber: Subscriber): Subscription { + this._throwIfClosed(); + this.trimBuffer(); + + const subscription = this._innerSubscribe(subscriber); + + const { infiniteTimeWindow, buffer } = this; + // We use a copy here, so reentrant code does not mutate our array while we're + // emitting it to a new subscriber. + const copy = buffer.slice(); + for (let i = 0; i < copy.length && !subscriber.closed; i += infiniteTimeWindow ? 1 : 2) { + subscriber.next(copy[i] as T); } - if (this.hasError) { - subscriber.error(this.thrownError); - } else if (this.isStopped) { - subscriber.complete(); - } + this._checkFinalizedStatuses(subscriber); return subscription; } - private _getNow(): number { - const { timestampProvider: scheduler } = this; - return scheduler ? scheduler.now() : dateTimestampProvider.now(); - } - - private _trimBufferThenGetEvents(): ReplayEvent[] { - const now = this._getNow(); - const _bufferSize = this._bufferSize; - const _windowTime = this._windowTime; - const _events = []>this._events; - - const eventsCount = _events.length; - let spliceCount = 0; - - // Trim events that fall out of the time window. - // Start at the front of the list. Break early once - // we encounter an event that falls within the window. - while (spliceCount < eventsCount) { - if ((now - _events[spliceCount].time) < _windowTime) { - break; + private trimBuffer() { + const { bufferSize, timestampProvider, buffer, infiniteTimeWindow } = this; + // If we don't have an infinite buffer size, and we're over the length, + // use splice to truncate the old buffer values off. Note that we have to + // double the size for instances where we're not using an infinite time window + // because we're storing the values and the timestamps in the same array. + const adjustedBufferSize = (infiniteTimeWindow ? 1 : 2) * bufferSize; + bufferSize < Infinity && adjustedBufferSize < buffer.length && buffer.splice(0, buffer.length - adjustedBufferSize); + + // Now, if we're not in an infinite time window, remove all values where the time is + // older than what is allowed. + if (!infiniteTimeWindow) { + const now = timestampProvider.now(); + let last = 0; + // Search the array for the first timestamp that isn't expired and + // truncate the buffer up to that point. + for (let i = 1; i < buffer.length && (buffer[i] as number) <= now; i += 2) { + last = i; } - spliceCount++; - } - - if (eventsCount > _bufferSize) { - spliceCount = Math.max(spliceCount, eventsCount - _bufferSize); + last && buffer.splice(0, last + 1); } - - if (spliceCount > 0) { - _events.splice(0, spliceCount); - } - - return _events; } - -} - -interface ReplayEvent { - time: number; - value: T; } diff --git a/src/internal/Subject.ts b/src/internal/Subject.ts index 16de75692d..ada7029c89 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -1,10 +1,11 @@ +/** @prettier */ import { Operator } from './Operator'; import { Observable } from './Observable'; import { Subscriber } from './Subscriber'; -import { Subscription } from './Subscription'; +import { Subscription, EMPTY_SUBSCRIPTION } from './Subscription'; import { Observer, SubscriptionLike, TeardownLogic } from './types'; import { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError'; -import { SubjectSubscription } from './SubjectSubscription'; +import { arrRemove } from './util/arrRemove'; /** * A Subject is a special type of Observable that allows values to be @@ -32,7 +33,7 @@ export class Subject extends Observable implements SubscriptionLike { */ static create: Function = (destination: Observer, source: Observable): AnonymousSubject => { return new AnonymousSubject(destination, source); - } + }; constructor() { // NOTE: This must be here to obscure Observable's constructor. @@ -41,85 +42,82 @@ export class Subject extends Observable implements SubscriptionLike { lift(operator: Operator): Observable { const subject = new AnonymousSubject(this, this); - subject.operator = operator; - return subject; + subject.operator = operator as any; + return subject as any; } - next(value: T) { + protected _throwIfClosed() { if (this.closed) { throw new ObjectUnsubscribedError(); } + } + + next(value: T) { + this._throwIfClosed(); if (!this.isStopped) { - const { observers } = this; - const len = observers.length; - const copy = observers.slice(); - for (let i = 0; i < len; i++) { - copy[i].next(value!); + const copy = this.observers.slice(); + for (const observer of copy) { + observer.next(value); } } } error(err: any) { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } - this.hasError = true; - this.thrownError = err; - this.isStopped = true; - const { observers } = this; - const len = observers.length; - const copy = observers.slice(); - for (let i = 0; i < len; i++) { - copy[i].error(err); + this._throwIfClosed(); + if (!this.isStopped) { + this.hasError = this.isStopped = true; + this.thrownError = err; + const { observers } = this; + while (observers.length) { + observers.shift()!.error(err); + } } - this.observers.length = 0; } complete() { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } - this.isStopped = true; - const { observers } = this; - const len = observers.length; - const copy = observers.slice(); - for (let i = 0; i < len; i++) { - copy[i].complete(); + this._throwIfClosed(); + if (!this.isStopped) { + this.isStopped = true; + const { observers } = this; + while (observers.length) { + observers.shift()!.complete(); + } } - this.observers.length = 0; } unsubscribe() { - this.isStopped = true; - this.closed = true; + this.isStopped = this.closed = true; this.observers = null!; } /** @deprecated This is an internal implementation detail, do not use. */ - _trySubscribe(subscriber: Subscriber): TeardownLogic { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } else { - return super._trySubscribe(subscriber); - } + protected _trySubscribe(subscriber: Subscriber): TeardownLogic { + this._throwIfClosed(); + return super._trySubscribe(subscriber); } /** @deprecated This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber): Subscription { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } else if (this.hasError) { - subscriber.error(this.thrownError); - return Subscription.EMPTY; - } else if (this.isStopped) { + protected _subscribe(subscriber: Subscriber): Subscription { + this._throwIfClosed(); + this._checkFinalizedStatuses(subscriber); + return this._innerSubscribe(subscriber); + } + + protected _innerSubscribe(subscriber: Subscriber) { + const { hasError, isStopped, observers } = this; + return hasError || isStopped + ? EMPTY_SUBSCRIPTION + : (observers.push(subscriber), new Subscription(() => arrRemove(this.observers, subscriber))); + } + + protected _checkFinalizedStatuses(subscriber: Subscriber) { + const { hasError, thrownError, isStopped } = this; + if (hasError) { + subscriber.error(thrownError); + } else if (isStopped) { subscriber.complete(); - return Subscription.EMPTY; - } else { - this.observers.push(subscriber); - return new SubjectSubscription(this, subscriber); } } - /** * Creates a new Observable with this Subject as the source. You can do this * to create customize Observer-side logic of the Subject and conceal it from @@ -127,8 +125,8 @@ export class Subject extends Observable implements SubscriptionLike { * @return {Observable} Observable that the Subject casts to */ asObservable(): Observable { - const observable = new Observable(); - (observable).source = this; + const observable: any = new Observable(); + observable.source = this; return observable; } } @@ -143,33 +141,19 @@ export class AnonymousSubject extends Subject { } next(value: T) { - const { destination } = this; - if (destination && destination.next) { - destination.next(value); - } + this.destination?.next?.(value); } error(err: any) { - const { destination } = this; - if (destination && destination.error) { - this.destination!.error(err); - } + this.destination?.error?.(err); } complete() { - const { destination } = this; - if (destination && destination.complete) { - this.destination!.complete(); - } + this.destination?.complete?.(); } /** @deprecated This is an internal implementation detail, do not use. */ _subscribe(subscriber: Subscriber): Subscription { - const { source } = this; - if (source) { - return this.source!.subscribe(subscriber); - } else { - return Subscription.EMPTY; - } + return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION; } } diff --git a/src/internal/SubjectSubscription.ts b/src/internal/SubjectSubscription.ts deleted file mode 100644 index cd9b1e5a3e..0000000000 --- a/src/internal/SubjectSubscription.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Subject } from './Subject'; -import { Observer } from './types'; -import { Subscription } from './Subscription'; - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class SubjectSubscription extends Subscription { - closed: boolean = false; - - constructor(public subject: Subject, public subscriber: Observer) { - super(); - } - - unsubscribe() { - if (this.closed) { - return; - } - - this.closed = true; - - const subject = this.subject; - const observers = subject.observers; - - this.subject = null!; - - if (!observers || observers.length === 0 || subject.isStopped || subject.closed) { - return; - } - - const subscriberIndex = observers.indexOf(this.subscriber); - - if (subscriberIndex !== -1) { - observers.splice(subscriberIndex, 1); - } - } -} diff --git a/src/internal/Subscriber.ts b/src/internal/Subscriber.ts index 595cd16a4e..82c8debbc4 100644 --- a/src/internal/Subscriber.ts +++ b/src/internal/Subscriber.ts @@ -235,4 +235,4 @@ export class SafeSubscriber extends Subscriber { super.unsubscribe(); } } -} +} \ No newline at end of file diff --git a/src/internal/Subscription.ts b/src/internal/Subscription.ts index 22bcff7fa2..aafea7b018 100644 --- a/src/internal/Subscription.ts +++ b/src/internal/Subscription.ts @@ -2,6 +2,7 @@ import { isFunction } from './util/isFunction'; import { UnsubscriptionError } from './util/UnsubscriptionError'; import { SubscriptionLike, TeardownLogic, Unsubscribable } from './types'; +import { arrRemove } from './util/arrRemove'; /** * Represents a disposable resource, such as the execution of an Observable. A @@ -166,10 +167,7 @@ export class Subscription implements SubscriptionLike { if (_parentage === parent) { this._parentage = null; } else if (Array.isArray(_parentage)) { - const index = _parentage.indexOf(parent); - if (0 <= index) { - _parentage.splice(index, 1); - } + arrRemove(_parentage, parent); } } @@ -189,12 +187,7 @@ export class Subscription implements SubscriptionLike { */ remove(teardown: Exclude): void { const { _teardowns } = this; - if (_teardowns) { - const index = _teardowns.indexOf(teardown); - if (index >= 0) { - _teardowns.splice(index, 1); - } - } + _teardowns && arrRemove(_teardowns, teardown); if (teardown instanceof Subscription) { teardown._removeParent(this); @@ -202,6 +195,8 @@ export class Subscription implements SubscriptionLike { } } +export const EMPTY_SUBSCRIPTION = Subscription.EMPTY; + export function isSubscription(value: any): value is Subscription { return ( value instanceof Subscription || diff --git a/src/internal/innerSubscribe.ts b/src/internal/innerSubscribe.ts deleted file mode 100644 index 53311c5241..0000000000 --- a/src/internal/innerSubscribe.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** @prettier */ -import { Subscription } from './Subscription'; -import { Subscriber } from './Subscriber'; -import { Observable } from './Observable'; -import { subscribeTo } from './util/subscribeTo'; - -interface SimpleOuterSubscriberLike { - /** - * A handler for inner next notifications from the inner subscription - * @param innerValue the value nexted by the inner producer - */ - notifyNext(innerValue: T): void; - /** - * A handler for inner error notifications from the inner subscription - * @param err the error from the inner producer - */ - notifyError(err: any): void; - /** - * A handler for inner complete notifications from the inner subscription. - */ - notifyComplete(): void; -} - -export class SimpleInnerSubscriber extends Subscriber { - constructor(private parent: SimpleOuterSubscriberLike) { - super(); - } - - protected _next(value: T): void { - this.parent.notifyNext(value); - } - - protected _error(error: any): void { - this.parent.notifyError(error); - this.unsubscribe(); - } - - protected _complete(): void { - this.parent.notifyComplete(); - this.unsubscribe(); - } -} - -export class ComplexInnerSubscriber extends Subscriber { - constructor(private parent: ComplexOuterSubscriber, public outerValue: T, public outerIndex: number) { - super(); - } - - protected _next(value: R): void { - this.parent.notifyNext(this.outerValue, value, this.outerIndex, this); - } - - protected _error(error: any): void { - this.parent.notifyError(error); - this.unsubscribe(); - } - - protected _complete(): void { - this.parent.notifyComplete(this); - this.unsubscribe(); - } -} - -export class SimpleOuterSubscriber extends Subscriber implements SimpleOuterSubscriberLike { - notifyNext(innerValue: R): void { - this.destination.next(innerValue); - } - - notifyError(err: any): void { - this.destination.error(err); - } - - notifyComplete(): void { - this.destination.complete(); - } -} - -/** - * DO NOT USE (formerly "OuterSubscriber") - * TODO: We want to refactor this and remove it. It is retaining values it shouldn't for long - * periods of time. - */ -export class ComplexOuterSubscriber extends Subscriber { - /** - * @param _outerValue Used by: bufferToggle, delayWhen, windowToggle - * @param innerValue Used by: subclass default, combineLatest, race, bufferToggle, windowToggle, withLatestFrom - * @param _outerIndex Used by: combineLatest, race, withLatestFrom - * @param _innerSub Used by: delayWhen - */ - notifyNext(_outerValue: T, innerValue: R, _outerIndex: number, _innerSub: ComplexInnerSubscriber): void { - this.destination.next(innerValue); - } - - notifyError(error: any): void { - this.destination.error(error); - } - - /** - * @param _innerSub Used by: race, bufferToggle, delayWhen, windowToggle, windowWhen - */ - notifyComplete(_innerSub: ComplexInnerSubscriber): void { - this.destination.complete(); - } -} - -export function innerSubscribe(result: any, innerSubscriber: Subscriber): Subscription | undefined { - if (innerSubscriber.closed) { - return undefined; - } - if (result instanceof Observable) { - return result.subscribe(innerSubscriber); - } - return subscribeTo(result)(innerSubscriber) as Subscription; -} diff --git a/src/internal/observable/ConnectableObservable.ts b/src/internal/observable/ConnectableObservable.ts index 33be396e52..0650cfcdf5 100644 --- a/src/internal/observable/ConnectableObservable.ts +++ b/src/internal/observable/ConnectableObservable.ts @@ -4,23 +4,21 @@ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; import { refCount as higherOrderRefCount } from '../operators/refCount'; +import { OperatorSubscriber } from '../operators/OperatorSubscriber'; /** * @class ConnectableObservable */ export class ConnectableObservable extends Observable { - protected _subject: Subject | undefined; + protected _subject: Subject | null = null; protected _refCount: number = 0; - protected _connection: Subscription | null | undefined; - /** @internal */ - _isComplete = false; + protected _connection: Subscription | null = null; constructor(public source: Observable, protected subjectFactory: () => Subject) { super(); } - /** @deprecated This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber) { + protected _subscribe(subscriber: Subscriber) { return this.getSubject().subscribe(subscriber); } @@ -32,12 +30,36 @@ export class ConnectableObservable extends Observable { return this._subject!; } + protected _teardown() { + this._refCount = 0; + const { _connection } = this; + this._subject = this._connection = null; + _connection?.unsubscribe(); + } + connect(): Subscription { let connection = this._connection; if (!connection) { - this._isComplete = false; connection = this._connection = new Subscription(); - connection.add(this.source.subscribe(new ConnectableSubscriber(this.getSubject(), this))); + const subject = this.getSubject(); + connection.add( + this.source.subscribe( + new OperatorSubscriber( + subject as any, + undefined, + (err) => { + this._teardown(); + subject.error(err); + }, + () => { + this._teardown(); + subject.complete(); + }, + () => this._teardown() + ) + ) + ); + if (connection.closed) { this._connection = null; connection = Subscription.EMPTY; @@ -50,56 +72,3 @@ export class ConnectableObservable extends Observable { return higherOrderRefCount()(this) as Observable; } } - -export const connectableObservableDescriptor: PropertyDescriptorMap = (() => { - const connectableProto = ConnectableObservable.prototype; - return { - operator: { value: null as null }, - _refCount: { value: 0, writable: true }, - _subject: { value: null as null, writable: true }, - _connection: { value: null as null, writable: true }, - _subscribe: { value: connectableProto._subscribe }, - _isComplete: { value: connectableProto._isComplete, writable: true }, - getSubject: { value: connectableProto.getSubject }, - connect: { value: connectableProto.connect }, - refCount: { value: connectableProto.refCount }, - }; -})(); - -class ConnectableSubscriber extends Subscriber { - constructor(protected destination: Subject, private connectable: ConnectableObservable) { - super(); - } - - protected _error(err: any): void { - this._teardown(); - super._error(err); - } - - protected _complete(): void { - this.connectable._isComplete = true; - this._teardown(); - super._complete(); - } - - private _teardown() { - const connectable = this.connectable as any; - if (connectable) { - this.connectable = null!; - const connection = connectable._connection; - connectable._refCount = 0; - connectable._subject = null; - connectable._connection = null; - if (connection) { - connection.unsubscribe(); - } - } - } - - unsubscribe() { - if (!this.closed) { - this._teardown(); - super.unsubscribe(); - } - } -} diff --git a/src/internal/observable/concat.ts b/src/internal/observable/concat.ts index a8ab2da7b2..97fcbeeb5f 100644 --- a/src/internal/observable/concat.ts +++ b/src/internal/observable/concat.ts @@ -2,6 +2,8 @@ import { Observable } from '../Observable'; import { ObservableInput, SchedulerLike, ObservedValueOf, ObservedValueUnionFromArray } from '../types'; import { of } from './of'; import { concatAll } from '../operators/concatAll'; +import { isScheduler } from '../util/isScheduler'; +import { fromArray } from './fromArray'; /* tslint:disable:max-line-length */ /** @deprecated remove in v8. Passing a scheduler to concat is deprecated, please use {@link scheduled} and {@link concatAll} `scheduled([o1, o2], scheduler).pipe(concatAll())` */ @@ -125,7 +127,12 @@ export function concat[]>(...observables: A): Obs * @param scheduler An optional {@link SchedulerLike} to schedule each * Observable subscription on. */ -export function concat>(...observables: Array): Observable> { - // The cast with `as` below is due to the SchedulerLike, once this is removed, it will no longer be a problem. - return concatAll>()(of(...observables) as Observable>); +export function concat(...args: any[]): Observable { + let scheduler: SchedulerLike | undefined; + + if (isScheduler(args[args.length - 1])) { + scheduler = args.pop() as SchedulerLike; + } + + return concatAll()(fromArray(args, scheduler)); } diff --git a/src/internal/observable/from.ts b/src/internal/observable/from.ts index 697e92860b..c7a77b756d 100644 --- a/src/internal/observable/from.ts +++ b/src/internal/observable/from.ts @@ -1,5 +1,17 @@ +import { subscribeToArray } from '../util/subscribeToArray'; +import { subscribeToPromise } from '../util/subscribeToPromise'; +import { subscribeToIterable } from '../util/subscribeToIterable'; +import { subscribeToObservable } from '../util/subscribeToObservable'; +import { isArrayLike } from '../util/isArrayLike'; +import { isPromise } from '../util/isPromise'; +import { isObject } from '../util/isObject'; +import { iterator as Symbol_iterator } from '../symbol/iterator'; +import { observable as Symbol_observable } from '../symbol/observable'; +import { Subscription } from '../Subscription'; +import { Subscriber } from '../Subscriber'; +import { subscribeToAsyncIterable } from '../util/subscribeToAsyncIterable'; + import { Observable } from '../Observable'; -import { subscribeTo } from '../util/subscribeTo'; import { ObservableInput, SchedulerLike, ObservedValueOf } from '../types'; import { scheduled } from '../scheduled/scheduled'; @@ -116,3 +128,25 @@ export function from(input: ObservableInput, scheduler?: SchedulerLike): O return scheduled(input, scheduler); } } + +function subscribeTo(result: ObservableInput): (subscriber: Subscriber) => Subscription | void { + if (result && typeof (result as any)[Symbol_observable] === 'function') { + return subscribeToObservable(result as any); + } else if (isArrayLike(result)) { + return subscribeToArray(result); + } else if (isPromise(result)) { + return subscribeToPromise(result); + } else if (result && typeof (result as any)[Symbol_iterator] === 'function') { + return subscribeToIterable(result as any); + } else if ( + Symbol && Symbol.asyncIterator && + !!result && typeof (result as any)[Symbol.asyncIterator] === 'function' + ) { + return subscribeToAsyncIterable(result as any); + } else { + const value = isObject(result) ? 'an invalid object' : `'${result}'`; + const msg = `You provided ${value} where a stream was expected.` + + ' You can provide an Observable, Promise, Array, AsyncIterable, or Iterable.'; + throw new TypeError(msg); + } +}; \ No newline at end of file diff --git a/src/internal/observable/fromEvent.ts b/src/internal/observable/fromEvent.ts index 6aedee96ca..01add21a57 100644 --- a/src/internal/observable/fromEvent.ts +++ b/src/internal/observable/fromEvent.ts @@ -1,7 +1,10 @@ +/** @prettier */ import { Observable } from '../Observable'; +import { mergeMap } from '../operators/mergeMap'; +import { isArrayLike } from '../util/isArrayLike'; import { isFunction } from '../util/isFunction'; -import { Subscriber } from '../Subscriber'; import { mapOneOrManyArgs } from '../util/mapOneOrManyArgs'; +import { fromArray } from './fromArray'; export interface NodeStyleEventEmitter { addListener: (eventName: string | symbol, handler: NodeEventHandler) => this; @@ -49,7 +52,12 @@ export function fromEvent(target: FromEventTarget, eventName: string): Obs export function fromEvent(target: FromEventTarget, eventName: string, resultSelector?: (...args: any[]) => T): Observable; export function fromEvent(target: FromEventTarget, eventName: string, options?: EventListenerOptions): Observable; /** @deprecated resultSelector no longer supported, pipe to map instead */ -export function fromEvent(target: FromEventTarget, eventName: string, options: EventListenerOptions, resultSelector: (...args: any[]) => T): Observable; +export function fromEvent( + target: FromEventTarget, + eventName: string, + options: EventListenerOptions, + resultSelector: (...args: any[]) => T +): Observable; /* tslint:enable:max-line-length */ /** @@ -175,9 +183,8 @@ export function fromEvent( target: FromEventTarget, eventName: string, options?: EventListenerOptions | ((...args: any[]) => T), - resultSelector?: ((...args: any[]) => T) + resultSelector?: (...args: any[]) => T ): Observable { - if (isFunction(options)) { // DEPRECATED PATH resultSelector = options; @@ -185,48 +192,36 @@ export function fromEvent( } if (resultSelector) { // DEPRECATED PATH - return fromEvent(target, eventName, options as EventListenerOptions | undefined).pipe( - mapOneOrManyArgs(resultSelector) - ); + return fromEvent(target, eventName, options as EventListenerOptions | undefined).pipe(mapOneOrManyArgs(resultSelector)); } - return new Observable(subscriber => { - function handler(e: T) { - if (arguments.length > 1) { - subscriber.next(Array.prototype.slice.call(arguments) as any); - } else { - subscriber.next(e); - } + return new Observable((subscriber) => { + const handler = (...args: any[]) => subscriber.next(args.length > 1 ? args : args[0]); + + if (isEventTarget(target)) { + target.addEventListener(eventName, handler, options as EventListenerOptions); + return () => target.removeEventListener(eventName, handler, options as EventListenerOptions); } - setupSubscription(target, eventName, handler, subscriber, options as EventListenerOptions); - }); -} -function setupSubscription(sourceObj: FromEventTarget, eventName: string, - handler: (...args: any[]) => void, subscriber: Subscriber, - options?: EventListenerOptions) { - let unsubscribe: (() => void) | undefined; - if (isEventTarget(sourceObj)) { - const source = sourceObj; - sourceObj.addEventListener(eventName, handler, options); - unsubscribe = () => source.removeEventListener(eventName, handler, options); - } else if (isJQueryStyleEventEmitter(sourceObj)) { - const source = sourceObj; - sourceObj.on(eventName, handler); - unsubscribe = () => source.off(eventName, handler); - } else if (isNodeStyleEventEmitter(sourceObj)) { - const source = sourceObj; - sourceObj.addListener(eventName, handler as NodeEventHandler); - unsubscribe = () => source.removeListener(eventName, handler as NodeEventHandler); - } else if (sourceObj && (sourceObj as any).length) { - for (let i = 0, len = (sourceObj as any).length; i < len; i++) { - setupSubscription((sourceObj as any)[i], eventName, handler, subscriber, options); + if (isJQueryStyleEventEmitter(target)) { + target.on(eventName, handler); + return () => target.off(eventName, handler); + } + + if (isNodeStyleEventEmitter(target)) { + target.addListener(eventName, handler); + return () => target.removeListener(eventName, handler); } - } else { - throw new TypeError('Invalid event target'); - } - subscriber.add(unsubscribe); + if (isArrayLike(target)) { + return (mergeMap((target: any) => fromEvent(target, eventName, options as any))(fromArray(target)) as Observable).subscribe( + subscriber + ); + } + + subscriber.error(new TypeError('Invalid event target')); + return; + }); } function isNodeStyleEventEmitter(sourceObj: any): sourceObj is NodeStyleEventEmitter { diff --git a/src/internal/observable/fromIterable.ts b/src/internal/observable/fromIterable.ts deleted file mode 100644 index e7ffd2b2aa..0000000000 --- a/src/internal/observable/fromIterable.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Observable } from '../Observable'; -import { SchedulerLike } from '../types'; -import { subscribeToIterable } from '../util/subscribeToIterable'; -import { scheduleIterable } from '../scheduled/scheduleIterable'; - -export function fromIterable(input: Iterable, scheduler?: SchedulerLike) { - if (!input) { - throw new Error('Iterable cannot be null'); - } - if (!scheduler) { - return new Observable(subscribeToIterable(input)); - } else { - return scheduleIterable(input, scheduler); - } -} diff --git a/src/internal/observable/fromObservable.ts b/src/internal/observable/fromObservable.ts deleted file mode 100644 index 6a297b4469..0000000000 --- a/src/internal/observable/fromObservable.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Observable } from '../Observable'; -import { subscribeToObservable } from '../util/subscribeToObservable'; -import { InteropObservable, SchedulerLike } from '../types'; -import { scheduleObservable } from '../scheduled/scheduleObservable'; - -export function fromObservable(input: InteropObservable, scheduler?: SchedulerLike) { - if (!scheduler) { - return new Observable(subscribeToObservable(input)); - } else { - return scheduleObservable(input, scheduler); - } -} diff --git a/src/internal/observable/fromPromise.ts b/src/internal/observable/fromPromise.ts deleted file mode 100644 index 28ebef65ea..0000000000 --- a/src/internal/observable/fromPromise.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Observable } from '../Observable'; -import { SchedulerLike } from '../types'; -import { subscribeToPromise } from '../util/subscribeToPromise'; -import { schedulePromise } from '../scheduled/schedulePromise'; - -export function fromPromise(input: PromiseLike, scheduler?: SchedulerLike) { - if (!scheduler) { - return new Observable(subscribeToPromise(input)); - } else { - return schedulePromise(input, scheduler); - } -} diff --git a/src/internal/observable/generate.ts b/src/internal/observable/generate.ts index ee917614af..8ead55ec08 100644 --- a/src/internal/observable/generate.ts +++ b/src/internal/observable/generate.ts @@ -1,22 +1,13 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Subscriber } from '../Subscriber'; import { identity } from '../util/identity'; -import { SchedulerAction, SchedulerLike } from '../types'; +import { SchedulerLike } from '../types'; import { isScheduler } from '../util/isScheduler'; export type ConditionFunc = (state: S) => boolean; export type IterateFunc = (state: S) => S; export type ResultFunc = (state: S) => T; -interface SchedulerState { - needIterate?: boolean; - state: S; - subscriber: Subscriber; - condition?: ConditionFunc; - iterate: IterateFunc; - resultSelector: ResultFunc; -} - export interface GenerateBaseOptions { /** * Initial state. @@ -94,12 +85,15 @@ export interface GenerateOptions extends GenerateBaseOptions { * @param {function (state: S): T} resultSelector Selector function for results produced in the sequence. (deprecated) * @param {SchedulerLike} [scheduler] A {@link SchedulerLike} on which to run the generator loop. If not provided, defaults to emit immediately. * @returns {Observable} The generated sequence. + * @deprecated Removing in v8. Use configuration object argument instead. */ - export function generate(initialState: S, - condition: ConditionFunc, - iterate: IterateFunc, - resultSelector: ResultFunc, - scheduler?: SchedulerLike): Observable; +export function generate( + initialState: S, + condition: ConditionFunc, + iterate: IterateFunc, + resultSelector: ResultFunc, + scheduler?: SchedulerLike +): Observable; /** * Generates an Observable by running a state-driven loop @@ -242,11 +236,14 @@ export interface GenerateOptions extends GenerateBaseOptions { * @param {function (state: S): T} [resultSelector] Selector function for results produced in the sequence. * @param {Scheduler} [scheduler] A {@link Scheduler} on which to run the generator loop. If not provided, defaults to emitting immediately. * @return {Observable} The generated sequence. + * @deprecated Removing in v8. Use configuration object argument instead. */ -export function generate(initialState: S, - condition: ConditionFunc, - iterate: IterateFunc, - scheduler?: SchedulerLike): Observable; +export function generate( + initialState: S, + condition: ConditionFunc, + iterate: IterateFunc, + scheduler?: SchedulerLike +): Observable; /** * Generates an observable sequence by running a state-driven loop @@ -333,124 +330,67 @@ export function generate(options: GenerateBaseOptions): Observable; */ export function generate(options: GenerateOptions): Observable; -export function generate(initialStateOrOptions: S | GenerateOptions, - condition?: ConditionFunc, - iterate?: IterateFunc, - resultSelectorOrScheduler?: (ResultFunc) | SchedulerLike, - scheduler?: SchedulerLike): Observable { - +export function generate( + initialStateOrOptions: S | GenerateOptions, + condition?: ConditionFunc, + iterate?: IterateFunc, + resultSelectorOrScheduler?: ResultFunc | SchedulerLike, + scheduler?: SchedulerLike +): Observable { let resultSelector: ResultFunc; let initialState: S; + // TODO: Remove this as we move away from deprecated signatures + // and move towards a configuration object argument. if (arguments.length == 1) { const options = initialStateOrOptions as GenerateOptions; initialState = options.initialState; condition = options.condition; iterate = options.iterate; - resultSelector = options.resultSelector || identity as ResultFunc; + resultSelector = options.resultSelector || (identity as ResultFunc); scheduler = options.scheduler; - } else if (resultSelectorOrScheduler === undefined || isScheduler(resultSelectorOrScheduler)) { - initialState = initialStateOrOptions as S; - resultSelector = identity as ResultFunc; - scheduler = resultSelectorOrScheduler as SchedulerLike; } else { initialState = initialStateOrOptions as S; - resultSelector = resultSelectorOrScheduler as ResultFunc; + if (!resultSelectorOrScheduler || isScheduler(resultSelectorOrScheduler)) { + resultSelector = identity as ResultFunc; + scheduler = resultSelectorOrScheduler as SchedulerLike; + } else { + resultSelector = resultSelectorOrScheduler as ResultFunc; + } } - return new Observable(subscriber => { + return new Observable((subscriber) => { let state = initialState; if (scheduler) { - return scheduler.schedule>(dispatch as any, 0, { - subscriber, - iterate: iterate!, - condition, - resultSelector, - state + let needIterate = false; + return scheduler.schedule(function () { + if (!subscriber.closed) { + try { + needIterate ? (state = iterate!(state)) : (needIterate = true); + condition && !condition(state) ? subscriber.complete() : subscriber.next(resultSelector(state)); + } catch (err) { + subscriber.error(err); + } + if (!subscriber.closed) { + this.schedule(state); + } + } }); } - do { - if (condition) { - let conditionResult: boolean; - try { - conditionResult = condition(state); - } catch (err) { - subscriber.error(err); - return undefined; - } - if (!conditionResult) { + try { + do { + if (condition && !condition(state)) { subscriber.complete(); - break; + } else { + subscriber.next(resultSelector(state)); + !subscriber.closed && (state = iterate!(state)); } - } - let value: T; - try { - value = resultSelector(state); - } catch (err) { - subscriber.error(err); - return undefined; - } - subscriber.next(value); - if (subscriber.closed) { - break; - } - try { - state = iterate!(state); - } catch (err) { - subscriber.error(err); - return undefined; - } - } while (true); - - return undefined; - }); -} - -function dispatch(this: SchedulerAction>, state: SchedulerState) { - const { subscriber, condition } = state; - if (subscriber.closed) { - return undefined; - } - if (state.needIterate) { - try { - state.state = state.iterate(state.state); + } while (!subscriber.closed); } catch (err) { subscriber.error(err); - return undefined; - } - } else { - state.needIterate = true; - } - if (condition) { - let conditionResult: boolean; - try { - conditionResult = condition(state.state); - } catch (err) { - subscriber.error(err); - return undefined; - } - if (!conditionResult) { - subscriber.complete(); - return undefined; - } - if (subscriber.closed) { - return undefined; } - } - let value: T; - try { - value = state.resultSelector(state.state); - } catch (err) { - subscriber.error(err); - return undefined; - } - if (subscriber.closed) { - return undefined; - } - subscriber.next(value); - if (subscriber.closed) { + return undefined; - } - return this.schedule(state); + }); } diff --git a/src/internal/observable/merge.ts b/src/internal/observable/merge.ts index 7c248930ff..04df785475 100644 --- a/src/internal/observable/merge.ts +++ b/src/internal/observable/merge.ts @@ -1,8 +1,12 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { ObservableInput, SchedulerLike} from '../types'; +import { ObservableInput, SchedulerLike } from '../types'; import { isScheduler } from '../util/isScheduler'; import { mergeAll } from '../operators/mergeAll'; import { fromArray } from './fromArray'; +import { argsOrArgArray } from '../util/argsOrArgArray'; +import { from } from './from'; +import { EMPTY } from './empty'; /* tslint:disable:max-line-length */ /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ @@ -12,36 +16,141 @@ export function merge(v1: ObservableInput, concurrent: number, scheduler: /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ export function merge(v1: ObservableInput, v2: ObservableInput, scheduler: SchedulerLike): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + concurrent: number, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + concurrent: number, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + concurrent: number, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + concurrent: number, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput, + scheduler: SchedulerLike +): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent: number, scheduler: SchedulerLike): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput, + concurrent: number, + scheduler: SchedulerLike +): Observable; export function merge(v1: ObservableInput): Observable; -export function merge(v1: ObservableInput, concurrent?: number): Observable; +export function merge(v1: ObservableInput, concurrent: number): Observable; export function merge(v1: ObservableInput, v2: ObservableInput): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, concurrent?: number): Observable; +export function merge(v1: ObservableInput, v2: ObservableInput, concurrent: number): Observable; export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, concurrent?: number): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, concurrent?: number): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, concurrent?: number): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): Observable; -export function merge(v1: ObservableInput, v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent?: number): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + concurrent: number +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + concurrent: number +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + concurrent: number +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput +): Observable; +export function merge( + v1: ObservableInput, + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput, + concurrent: number +): Observable; export function merge(...observables: (ObservableInput | number)[]): Observable; /** @deprecated use {@link scheduled} and {@link mergeAll} (e.g. `scheduled([ob1, ob2, ob3], scheduler).pipe(mergeAll())*/ export function merge(...observables: (ObservableInput | SchedulerLike | number)[]): Observable; @@ -119,22 +228,26 @@ export function merge(...observables: (ObservableInput | SchedulerLik * @name merge * @owner Observable */ -export function merge(...observables: Array | SchedulerLike | number | undefined>): Observable { - let concurrent = Infinity; - let scheduler: SchedulerLike | undefined = undefined; - let last: any = observables[observables.length - 1]; - if (isScheduler(last)) { - scheduler = observables.pop(); - if (observables.length > 1 && typeof observables[observables.length - 1] === 'number') { - concurrent = observables.pop(); - } - } else if (typeof last === 'number') { - concurrent = observables.pop(); +export function merge(...args: (ObservableInput | SchedulerLike | number)[]): Observable { + let concurrent = Infinity; + let scheduler: SchedulerLike | undefined = undefined; + + if (isScheduler(args[args.length - 1])) { + scheduler = args.pop() as SchedulerLike; } - if (!scheduler && observables.length === 1 && observables[0] instanceof Observable) { - return >observables[0]; + if (typeof args[args.length - 1] === 'number') { + concurrent = args.pop() as number; } - return mergeAll(concurrent)(fromArray(observables, scheduler)); + args = argsOrArgArray(args); + + return !args.length + ? // No source provided + EMPTY + : args.length === 1 + ? // One source? Just return it. + from(args[0] as ObservableInput) + : // Merge all sources + mergeAll(concurrent)(fromArray(args as ObservableInput[], scheduler)); } diff --git a/src/internal/observable/partition.ts b/src/internal/observable/partition.ts index 637172fc86..7c3ce21d6c 100644 --- a/src/internal/observable/partition.ts +++ b/src/internal/observable/partition.ts @@ -1,8 +1,8 @@ import { not } from '../util/not'; -import { subscribeTo } from '../util/subscribeTo'; import { filter } from '../operators/filter'; import { ObservableInput } from '../types'; import { Observable } from '../Observable'; +import { from } from './from'; /** * Splits the source Observable into two, one with values that satisfy a @@ -61,7 +61,7 @@ export function partition( thisArg?: any ): [Observable, Observable] { return [ - filter(predicate, thisArg)(new Observable(subscribeTo(source))), - filter(not(predicate, thisArg) as any)(new Observable(subscribeTo(source))) + filter(predicate, thisArg)(from(source)), + filter(not(predicate, thisArg))(from(source)) ] as [Observable, Observable]; } diff --git a/src/internal/observable/race.ts b/src/internal/observable/race.ts index 836b849b7d..5caee47c1b 100644 --- a/src/internal/observable/race.ts +++ b/src/internal/observable/race.ts @@ -1,11 +1,11 @@ +/** @prettier */ import { Observable } from '../Observable'; import { from } from './from'; -import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; import { ObservableInput, ObservedValueUnionFromArray } from '../types'; -import { ComplexOuterSubscriber, innerSubscribe, ComplexInnerSubscriber } from '../innerSubscribe'; -import { lift } from '../util/lift'; -import { argsOrArgArray } from "../util/argsOrArgArray"; +import { argsOrArgArray } from '../util/argsOrArgArray'; +import { OperatorSubscriber } from '../operators/OperatorSubscriber'; +import { Subscriber } from '../Subscriber'; export function race[]>(observables: A): Observable>; export function race[]>(...observables: A): Observable>; @@ -51,81 +51,41 @@ export function race[]>(...observables: A): Obser * @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. */ -export function race(...observables: (ObservableInput | ObservableInput[])[]): Observable { - // if the only argument is an array, it was most likely called with - // `race([obs1, obs2, ...])` - observables = argsOrArgArray(observables); - - return observables.length === 1 ? from(observables[0]) : lift(from(observables), function (this: Subscriber, source: Observable) { - return source.subscribe(new RaceSubscriber(this)); - }); +export function race(...sources: (ObservableInput | ObservableInput[])[]): Observable { + sources = argsOrArgArray(sources); + // If only one source was passed, just return it. Otherwise return the race. + return sources.length === 1 ? from(sources[0]) : new Observable(raceInit(sources as ObservableInput[])); } /** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} + * An observable initializer function for both the static version and the + * operator version of race. + * @param sources The sources to race */ -export class RaceSubscriber extends ComplexOuterSubscriber { - 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]; - const subscription = innerSubscribe(observable, new ComplexInnerSubscriber(this, null, i)); - - if (this.subscriptions) { - this.subscriptions.push(subscription!); - } - this.add(subscription); - } - this.observables = null!; +export function raceInit(sources: ObservableInput[]) { + return (subscriber: Subscriber) => { + let subscriptions: Subscription[] = []; + + // Subscribe to all of the sources. Note that we are checking `subscriptions` here + // Is is an array of all actively "racing" subscriptions, and it is `null` after the + // race has been won. So, if we have racer that synchronously "wins", this loop will + // stop before it subscribes to any more. + for (let i = 0; subscriptions && !subscriber.closed && i < sources.length; i++) { + subscriptions.push( + from(sources[i] as ObservableInput).subscribe( + new OperatorSubscriber(subscriber, (value) => { + if (subscriptions) { + // We're still racing, but we won! So unsubscribe + // all other subscriptions that we have, except this one. + for (let s = 0; s < subscriptions.length; s++) { + s !== i && subscriptions[s].unsubscribe(); + } + subscriptions = null!; + } + subscriber.next(value); + }) + ) + ); } - } - - notifyNext(_outerValue: T, innerValue: T, - outerIndex: number): 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); - } - - notifyComplete(innerSub: ComplexInnerSubscriber): void { - this.hasFirst = true; - super.notifyComplete(innerSub); - } - - notifyError(error: any): void { - this.hasFirst = true; - super.notifyError(error); - } + }; } diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts new file mode 100644 index 0000000000..ed28e1a0ab --- /dev/null +++ b/src/internal/operators/OperatorSubscriber.ts @@ -0,0 +1,75 @@ +/** @prettier */ +import { Subscriber } from '../Subscriber'; + +/** + * A generic helper for allowing operators to be created with a Subscriber and + * use closures to capture neceessary state from the operator function itself. + */ +export class OperatorSubscriber extends Subscriber { + /** + * Creates an instance of an `OperatorSubscriber`. + * @param destination The downstream subscriber. + * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any + * error that occurs in this function is caught and sent to the `error` method of this subscriber. + * @param onError Handles errors from the subscription, any errors that occur in this handler are caught + * and send to the `destination` error handler. + * @param onComplete Handles completion notification from the subscription. Any errors that occur in + * this handler are sent to the `destination` error handler. + * @param onUnsubscribe Additional teardown logic here. This will only be called on teardown if the + * subscriber itself is not already closed. Called before any additional teardown logic is called. + */ + constructor( + destination: Subscriber, + onNext?: (value: T) => void, + onError?: (err: any) => void, + onComplete?: () => void, + private onUnsubscribe?: () => void + ) { + super(destination); + if (onNext) { + this._next = function (value: T) { + try { + onNext(value); + } catch (err) { + // NOTE: At some point we may want to refactor this to send to + // `destination.error(err)`. Currently, this is the way it is *only* to + // accommodate `groupBy`, with minimal ill effects to other operators, but + // it does mean that additional logic is being fired at each step during + // an error call. Which since it is by definition an exceptional state, probably + // isn't a big deal. Just making a note of this here so context isn't lost. + this.error(err); + } + }; + } + if (onError) { + this._error = function (err) { + try { + onError(err); + } catch (err) { + // Send any errors that occur down stream. + this.destination.error(err); + } + // Ensure teardown. + this.unsubscribe(); + }; + } + if (onComplete) { + this._complete = function () { + try { + onComplete(); + } catch (err) { + // Send any errors that occur down stream. + this.destination.error(err); + } + // Ensure teardown. + this.unsubscribe(); + }; + } + } + + unsubscribe() { + // Execute additional teardown if we have any and we didn't already do so. + !this.closed && this.onUnsubscribe?.(); + super.unsubscribe(); + } +} diff --git a/src/internal/operators/audit.ts b/src/internal/operators/audit.ts index f48d97634b..1c626106e3 100644 --- a/src/internal/operators/audit.ts +++ b/src/internal/operators/audit.ts @@ -1,11 +1,11 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; -import { MonoTypeOperatorFunction, SubscribableOrPromise, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction, SubscribableOrPromise } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '../innerSubscribe'; +import { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Ignores source values for a duration determined by another Observable, then @@ -53,75 +53,34 @@ import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '.. * @name audit */ export function audit(durationSelector: (value: T) => SubscribableOrPromise): MonoTypeOperatorFunction { - return function auditOperatorFunction(source: Observable) { - return lift(source, 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 SimpleOuterSubscriber { - - private value: T | null = null; - private hasValue: boolean = false; - private throttled: Subscription | null = null; - - constructor(destination: Subscriber, - private durationSelector: (value: T) => SubscribableOrPromise) { - super(destination); - } - - protected _next(value: T): void { - this.value = value; - this.hasValue = true; - if (!this.throttled) { - let duration; - try { - const { durationSelector } = this; - duration = durationSelector(value); - } catch (err) { - return this.destination.error(err); - } - const innerSubscription = innerSubscribe(duration, new SimpleInnerSubscriber(this)); - if (!innerSubscription || 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); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let lastValue: T | null = null; + let durationSubscriber: Subscriber | null = null; - notifyNext(): void { - this.clearThrottle(); - } + const endDuration = () => { + durationSubscriber?.unsubscribe(); + durationSubscriber = null; + if (hasValue) { + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + } + }; - notifyComplete(): void { - this.clearThrottle(); - } + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + hasValue = true; + lastValue = value; + if (!durationSubscriber) { + from(durationSelector(value)).subscribe( + (durationSubscriber = new OperatorSubscriber(subscriber, endDuration, undefined, endDuration)) + ); + } + }) + ); + }); } diff --git a/src/internal/operators/buffer.ts b/src/internal/operators/buffer.ts index 19fe5dfa7e..d0793c1ef1 100644 --- a/src/internal/operators/buffer.ts +++ b/src/internal/operators/buffer.ts @@ -1,9 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleInnerSubscriber, SimpleOuterSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Buffers the source Observable values until `closingNotifier` emits. @@ -45,43 +45,27 @@ import { SimpleInnerSubscriber, SimpleOuterSubscriber, innerSubscribe } from '.. * @name buffer */ export function buffer(closingNotifier: Observable): OperatorFunction { - return function bufferOperatorFunction(source: Observable) { - return lift(source, new BufferOperator(closingNotifier)); - }; -} - -class BufferOperator implements Operator { - - constructor(private closingNotifier: Observable) { - } - - call(subscriber: Subscriber, source: any): any { - const bufferSubscriber = new BufferSubscriber(subscriber); - subscriber.add(source.subscribe(bufferSubscriber)); - subscriber.add(innerSubscribe(this.closingNotifier, new SimpleInnerSubscriber(bufferSubscriber))); - return subscriber; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferSubscriber extends SimpleOuterSubscriber { - private buffer: T[] = []; + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let buffer: T[] = []; - constructor(destination: Subscriber) { - super(destination); - } + // Subscribe to our source. + source.subscribe(new OperatorSubscriber(subscriber, (value) => buffer.push(value))); - protected _next(value: T) { - this.buffer.push(value); - } + // Subscribe to the closing notifier. + closingNotifier.subscribe( + new OperatorSubscriber(subscriber, () => { + // Start a new buffer and emit the previous one. + const b = buffer; + buffer = []; + subscriber.next(b); + }) + ); - notifyNext(): void { - const buffer = this.buffer; - this.buffer = []; - this.destination.next(buffer); - } + return () => { + // Ensure buffered values are released on teardown. + buffer = null!; + }; + }); } diff --git a/src/internal/operators/bufferCount.ts b/src/internal/operators/bufferCount.ts index c39d8e3ecf..081702cc87 100644 --- a/src/internal/operators/bufferCount.ts +++ b/src/internal/operators/bufferCount.ts @@ -1,8 +1,10 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { OperatorFunction, TeardownLogic } from '../types'; +import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { arrRemove } from '../util/arrRemove'; /** * Buffers the source Observable values until the size hits the maximum @@ -59,100 +61,67 @@ import { lift } from '../util/lift'; * @name bufferCount */ export function bufferCount(bufferSize: number, startBufferEvery: number | null = null): OperatorFunction { - return function bufferCountOperatorFunction(source: Observable) { - return lift(source, new BufferCountOperator(bufferSize, startBufferEvery)); - }; -} - -class BufferCountOperator implements Operator { - private subscriberClass: any; - - constructor(private bufferSize: number, private startBufferEvery: number | null) { - 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(); - } - + // If no `startBufferEvery` value was supplied, then we're + // opening and closing on the bufferSize itself. + startBufferEvery = startBufferEvery ?? bufferSize; + + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let buffers: T[][] = []; + let count = 0; + + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + let toEmit: T[][] | null = null; + + // Check to see if we need to start a buffer. + // This will start one at the first value, and then + // a new one every N after that. + if (count++ % startBufferEvery! === 0) { + buffers.push([]); + } + + // Push our value into our active buffers. + for (const buffer of buffers) { + buffer.push(value); + // Check to see if we're over the bufferSize + // if we are, record it so we can emit it later. + // If we emitted it now and removed it, it would + // mutate the `buffers` array while we're looping + // over it. + if (bufferSize <= buffer.length) { + toEmit = toEmit ?? []; + toEmit.push(buffer); + } + } + + if (toEmit) { + // We have found some buffers that are over the + // `bufferSize`. Emit them, and remove them from our + // buffers list. + for (const buffer of toEmit) { + arrRemove(buffers, buffer); + subscriber.next(buffer); + } + } + }, + undefined, + () => { + // When the source completes, emit all of our + // active buffers. + for (const buffer of buffers) { + subscriber.next(buffer); + } + subscriber.complete(); + }, + () => { + // Clean up our memory when we teardown + buffers = null!; + } + ) + ); + }); } diff --git a/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index 5efef01b39..715a1d8e2e 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -1,12 +1,13 @@ /** @prettier */ -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, SchedulerAction, SchedulerLike } from '../types'; +import { OperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { arrRemove } from '../util/arrRemove'; +import { asyncScheduler } from '../scheduler/async'; /* tslint:disable:max-line-length */ export function bufferTime(bufferTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction; @@ -80,20 +81,15 @@ export function bufferTime( * @name bufferTime */ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): OperatorFunction { - let scheduler: SchedulerLike = async; - - if (isScheduler(otherArgs[otherArgs.length - 1])) { - scheduler = otherArgs.pop() as SchedulerLike; - } - + const scheduler = isScheduler(otherArgs[otherArgs.length - 1]) ? (otherArgs.pop() as SchedulerLike) : asyncScheduler; const bufferCreationInterval = (otherArgs[0] as number) ?? null; const maxBufferSize = (otherArgs[1] as number) || Infinity; - return function bufferTimeOperatorFunction(source: Observable) { - return lift(source, function (this: Subscriber, source: Observable) { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; // The active buffers, their related subscriptions, and removal functions. - let bufferRecords: { buffer: T[]; subs: Subscription; remove: () => void }[] | null = []; + let bufferRecords: { buffer: T[]; subs: Subscription }[] | null = []; // If true, it means that every time we emit a buffer, we want to start a new buffer // this is only really used for when *just* the buffer time span is passed. let restartOnEmit = false; @@ -104,12 +100,12 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper * does not alter the buffer. Also checks to see if a new buffer needs to be started * after the emit. */ - const emit = (record: { buffer: T[]; subs: Subscription; remove: () => void }) => { - record.remove(); - subscriber.next(record.buffer); - if (restartOnEmit) { - startBuffer(); - } + const emit = (record: { buffer: T[]; subs: Subscription }) => { + const { buffer, subs } = record; + subs.unsubscribe(); + arrRemove(bufferRecords, record); + subscriber.next(buffer); + restartOnEmit && startBuffer(); }; /** @@ -125,87 +121,57 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper const record = { buffer, subs, - remove() { - this.subs.unsubscribe(); - if (bufferRecords) { - const index = bufferRecords.indexOf(this); - if (0 <= index) { - bufferRecords.splice(index, 1); - } - } - }, }; bufferRecords.push(record); - subs.add( - scheduler.schedule(() => { - emit(record); - }, bufferTimeSpan) - ); + subs.add(scheduler.schedule(() => emit(record), bufferTimeSpan)); } }; - if (bufferCreationInterval !== null && bufferCreationInterval >= 0) { - // The user passed both a bufferTimeSpan (required), and a creation interval - // That means we need to start new buffers on the interval, and those buffers need - // to wait the required time span before emitting. - subscriber.add( - scheduler.schedule(function () { - startBuffer(); - if (!this.closed) { - subscriber.add(this.schedule(null, bufferCreationInterval)); - } - }, bufferCreationInterval) - ); - startBuffer(); - } else { - restartOnEmit = true; - startBuffer(); - } + bufferCreationInterval !== null && bufferCreationInterval >= 0 + ? // The user passed both a bufferTimeSpan (required), and a creation interval + // That means we need to start new buffers on the interval, and those buffers need + // to wait the required time span before emitting. + subscriber.add( + scheduler.schedule(function () { + startBuffer(); + !this.closed && subscriber.add(this.schedule(null, bufferCreationInterval)); + }, bufferCreationInterval) + ) + : (restartOnEmit = true); - const bufferTimeSubscriber = new BufferTimeSubscriber( + startBuffer(); + + const bufferTimeSubscriber = new OperatorSubscriber( subscriber, - (value) => { + (value: T) => { // Copy the records, so if we need to remove one we // don't mutate the array. It's hard, but not impossible to // set up a buffer time that could mutate the array and // cause issues here. const recordsCopy = bufferRecords!.slice(); - for (let i = 0; i < recordsCopy.length; i++) { + for (const record of recordsCopy) { // Loop over all buffers and - const record = recordsCopy[i]; const { buffer } = record; buffer.push(value); // If the buffer is over the max size, we need to emit it. - if (maxBufferSize <= buffer.length) { - emit(record); - } + maxBufferSize <= buffer.length && emit(record); } }, + undefined, () => { // The source completed, emit all of the active // buffers we have before we complete. - for (const record of bufferRecords!) { - record.remove(); - subscriber.next(record.buffer); + while (bufferRecords?.length) { + subscriber.next(bufferRecords.shift()!.buffer); } - // Free up memory. - bufferRecords = null; bufferTimeSubscriber?.unsubscribe(); - } + subscriber.complete(); + subscriber.unsubscribe(); + }, + // Clean up + () => (bufferRecords = null) ); source.subscribe(bufferTimeSubscriber); }); - }; -} - -class BufferTimeSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void, protected onBeforeComplete: () => void) { - super(destination); - } - - _complete() { - this.onBeforeComplete(); - super._complete(); - } } diff --git a/src/internal/operators/bufferToggle.ts b/src/internal/operators/bufferToggle.ts index 2666f353db..c2abf5df93 100644 --- a/src/internal/operators/bufferToggle.ts +++ b/src/internal/operators/bufferToggle.ts @@ -1,10 +1,12 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscription } from '../Subscription'; -import { ComplexOuterSubscriber, ComplexInnerSubscriber, innerSubscribe } from '../innerSubscribe'; import { OperatorFunction, SubscribableOrPromise } from '../types'; -import { lift } from '../util/lift'; +import { wrappedLift } from '../util/lift'; +import { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { noop } from '../util/noop'; +import { arrRemove } from '../util/arrRemove'; /** * Buffers the source Observable values starting from an emission from @@ -56,123 +58,56 @@ export function bufferToggle( closingSelector: (value: O) => SubscribableOrPromise ): OperatorFunction { return function bufferToggleOperatorFunction(source: Observable) { - return lift(source, new BufferToggleOperator(openings, closingSelector)); + return wrappedLift(source, (subscriber, liftedSource) => { + const buffers: T[][] = []; + + // Subscribe to the openings notifier first + from(openings).subscribe( + new OperatorSubscriber( + subscriber, + (openValue) => { + const buffer: T[] = []; + buffers.push(buffer); + // We use this composite subscription, so that + // when the closing notifier emits, we can tear it down. + const closingSubscription = new Subscription(); + + // This is captured here, because we emit on both next or + // if the closing notifier completes without value. + // TODO: We probably want to not have closing notifiers emit!! + const emit = () => { + arrRemove(buffers, buffer); + subscriber.next(buffer); + closingSubscription.unsubscribe(); + }; + + // The line below will add the subscription to the parent subscriber *and* the closing subscription. + closingSubscription.add(from(closingSelector(openValue)).subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit))); + }, + undefined, + noop + ) + ); + + liftedSource.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + // Value from our source. Add it to all pending buffers. + for (const buffer of buffers) { + buffer.push(value); + } + }, + undefined, + () => { + // Source complete. Emit all pending buffers. + while (buffers.length > 0) { + subscriber.next(buffers.shift()!); + } + subscriber.complete(); + } + ) + ); + }); }; } - -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 ComplexOuterSubscriber { - private contexts: Array> = []; - - constructor(destination: Subscriber, - openings: SubscribableOrPromise, - private closingSelector: (value: O) => SubscribableOrPromise | void) { - super(destination); - this.add(innerSubscribe(openings, new ComplexInnerSubscriber(this, undefined, 0))) - } - - 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): void { - outerValue ? this.closeBuffer(outerValue) : this.openBuffer(innerValue); - } - - notifyComplete(innerSub: ComplexInnerSubscriber): 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 = innerSubscribe(closingNotifier, new ComplexInnerSubscriber(this, context, 0)); - - if (!innerSubscription || innerSubscription.closed) { - this.closeBuffer(context); - } else { - ( innerSubscription).context = context; - - this.add(innerSubscription); - subscription.add(innerSubscription); - } - } -} diff --git a/src/internal/operators/bufferWhen.ts b/src/internal/operators/bufferWhen.ts index d724ac8744..6571654124 100644 --- a/src/internal/operators/bufferWhen.ts +++ b/src/internal/operators/bufferWhen.ts @@ -1,11 +1,10 @@ /** @prettier */ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; -import { OperatorFunction } from '../types'; +import { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; /** * Buffers the source Observable values, using a factory function of closing @@ -48,89 +47,44 @@ import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '.. * @return {Observable} An observable of arrays of buffered values. * @name bufferWhen */ -export function bufferWhen(closingSelector: () => Observable): OperatorFunction { - return function (source: Observable) { - return lift(source, 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)); - } -} - -class BufferWhenSubscriber extends SimpleOuterSubscriber { - private buffer: T[] | undefined; - private subscribing: boolean = false; - private closingSubscription: Subscription | undefined; - - 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(); - } - - unsubscribe() { - if (!this.closed) { - this.buffer = null!; - this.subscribing = false; - super.unsubscribe(); - } - } - - notifyNext(): void { - this.openBuffer(); - } +export function bufferWhen(closingSelector: () => ObservableInput): OperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let buffer: T[] | null = null; + let closingSubscriber: Subscriber | null = null; - notifyComplete(): void { - if (this.subscribing) { - this.complete(); - } else { - this.openBuffer(); - } - } + const openBuffer = () => { + closingSubscriber?.unsubscribe(); - openBuffer() { - let { closingSubscription } = this; + const b = buffer; + buffer = []; + b && subscriber.next(b); - if (closingSubscription) { - this.remove(closingSubscription); - closingSubscription.unsubscribe(); - } + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector()); + } catch (err) { + subscriber.error(err); + return; + } - const buffer = this.buffer; - if (this.buffer) { - this.destination.next(buffer); - } + closingNotifier.subscribe((closingSubscriber = new OperatorSubscriber(subscriber, openBuffer, undefined, () => openBuffer()))); + }; - this.buffer = []; + openBuffer(); - let closingNotifier; - try { - const { closingSelector } = this; - closingNotifier = closingSelector(); - } catch (err) { - return this.error(err); - } - closingSubscription = new Subscription(); - this.closingSubscription = closingSubscription; - this.add(closingSubscription); - this.subscribing = true; - closingSubscription.add(innerSubscribe(closingNotifier, new SimpleInnerSubscriber(this))); - this.subscribing = false; - } + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => buffer?.push(value), + undefined, + () => { + buffer && subscriber.next(buffer); + subscriber.complete(); + }, + () => (buffer = closingSubscriber = null!) + ) + ); + }); } diff --git a/src/internal/operators/catchError.ts b/src/internal/operators/catchError.ts index f793d1b3de..77f3ece3e0 100644 --- a/src/internal/operators/catchError.ts +++ b/src/internal/operators/catchError.ts @@ -6,6 +6,7 @@ import { ObservableInput, OperatorFunction, ObservedValueOf } from '../types'; import { lift } from '../util/lift'; import { Subscription } from '../Subscription'; import { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ export function catchError>( @@ -116,54 +117,36 @@ export function catchError>( let syncUnsub = false; let handledResult: Observable>; - const handleError = (err: any) => { - try { - handledResult = from(selector(err, catchError(selector)(source))); - } catch (err) { - subscriber.error(err); - return; - } - }; - innerSub = source.subscribe( - new CatchErrorSubscriber(subscriber, (err) => { - handleError(err); - if (handledResult) { - if (innerSub) { - innerSub.unsubscribe(); - innerSub = null; - subscription.add(handledResult.subscribe(subscriber)); - } else { - syncUnsub = true; - } + new OperatorSubscriber(subscriber, undefined, (err) => { + handledResult = from(selector(err, catchError(selector)(source))); + if (innerSub) { + innerSub.unsubscribe(); + innerSub = null; + subscription.add(handledResult.subscribe(subscriber)); + } else { + // We don't have an innerSub yet, that means the error was synchronous + // because the subscribe call hasn't returned yet. + syncUnsub = true; } }) ); if (syncUnsub) { + // We have a synchronous error, we need to make sure to + // teardown right away. This ensures that `finalize` is called + // at the right time, and that teardown occurs at the expected + // time between the source error and the subscription to the + // next observable. innerSub.unsubscribe(); innerSub = null; subscription.add(handledResult!.subscribe(subscriber)); } else { + // Everything was fine after subscription, add it to our + // parent subscription. subscription.add(innerSub); } return subscription; }); } - -/** - * This must exist to ensure that the `closed` state of the inner subscriber is set at - * the proper time to ensure operators like `take` can stop the inner subscription if - * it is a synchronous firehose. - */ -class CatchErrorSubscriber extends Subscriber { - constructor(destination: Subscriber, private onError: (err: any) => void) { - super(destination); - } - - _error(err: any) { - this.onError(err); - this.unsubscribe(); - } -} diff --git a/src/internal/operators/concat.ts b/src/internal/operators/concat.ts deleted file mode 100644 index acf58b9576..0000000000 --- a/src/internal/operators/concat.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { concat as concatStatic } from '../observable/concat'; -import { Observable } from '../Observable'; -import { ObservableInput, OperatorFunction, MonoTypeOperatorFunction, SchedulerLike } from '../types'; -import { stankyLift } from '../util/lift'; - -/* tslint:disable:max-line-length */ -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(scheduler?: SchedulerLike): MonoTypeOperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(v2: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(v2: ObservableInput, v3: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(...observables: Array | SchedulerLike>): MonoTypeOperatorFunction; -/** @deprecated remove in v8. Use {@link concatWith} */ -export function concat(...observables: Array | SchedulerLike>): OperatorFunction; -/* tslint:enable:max-line-length */ - -/** - * @deprecated remove in v8. Use {@link concatWith} - */ -export function concat(...observables: Array | SchedulerLike | undefined>): OperatorFunction { - return (source: Observable) => stankyLift( - source, - concatStatic(source, ...(observables as any[])), - ); -} diff --git a/src/internal/operators/concatWith.ts b/src/internal/operators/concatWith.ts index 414203a09c..197b24a2d3 100644 --- a/src/internal/operators/concatWith.ts +++ b/src/internal/operators/concatWith.ts @@ -1,7 +1,11 @@ import { concat as concatStatic } from '../observable/concat'; import { Observable } from '../Observable'; -import { ObservableInput, OperatorFunction, ObservedValueUnionFromArray } from '../types'; -import { stankyLift } from '../util/lift'; +import { ObservableInput, OperatorFunction, ObservedValueUnionFromArray, MonoTypeOperatorFunction, SchedulerLike } from '../types'; +import { lift } from '../util/lift'; +import { Subscriber } from '../Subscriber'; +import { concatAll } from './concatAll'; +import { fromArray } from '../observable/fromArray'; +import { isScheduler } from '../util/isScheduler'; export function concatWith(): OperatorFunction; export function concatWith[]>(...otherSources: A): OperatorFunction | T>; @@ -45,8 +49,41 @@ export function concatWith[]>(...otherSources: * @param otherSources Other observable sources to subscribe to, in sequence, after the original source is complete. */ export function concatWith[]>(...otherSources: A): OperatorFunction | T> { - return (source: Observable) => stankyLift( + return concat(...otherSources); +} + +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(scheduler?: SchedulerLike): MonoTypeOperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(v2: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(v2: ObservableInput, v3: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler?: SchedulerLike): OperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(...observables: Array | SchedulerLike>): MonoTypeOperatorFunction; +/** @deprecated remove in v8. Use {@link concatWith} */ +export function concat(...observables: Array | SchedulerLike>): OperatorFunction; + + +/** + * @deprecated remove in v8. Use {@link concatWith} + */ +export function concat(...args: any[]): OperatorFunction { + let scheduler: SchedulerLike | undefined; + + if (isScheduler(args[args.length - 1])) { + scheduler = args.pop() as SchedulerLike; + } + + return (source: Observable) => lift( source, - concatStatic(source, ...otherSources) + function (this: Subscriber, source: Observable) { + concatAll()(fromArray([source, ...args], scheduler)).subscribe(this); + } ); -} +} \ No newline at end of file diff --git a/src/internal/operators/count.ts b/src/internal/operators/count.ts index b9b4a8587d..2dae32db12 100644 --- a/src/internal/operators/count.ts +++ b/src/internal/operators/count.ts @@ -1,8 +1,9 @@ +/** @pretter */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; -import { Observer, OperatorFunction } from '../types'; +import { OperatorFunction } from '../types'; import { Subscriber } from '../Subscriber'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Counts the number of emissions on the source and emits that number when the * source completes. @@ -63,59 +64,15 @@ import { lift } from '../util/lift'; */ export function count(predicate?: (value: T, index: number, source: Observable) => boolean): OperatorFunction { - return (source: Observable) => lift(source, new CountOperator(predicate, source)); -} - -class CountOperator implements Operator { - constructor(private predicate: ((value: T, index: number, source: Observable) => boolean) | undefined, - 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) | undefined, - 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 (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let index = 0; + let count = 0; + return source.subscribe(new OperatorSubscriber(subscriber, (value) => + (!predicate || predicate(value, index++, source)) && count++ + , undefined, () => { + subscriber.next(count); + subscriber.complete(); + })) + }); } diff --git a/src/internal/operators/debounce.ts b/src/internal/operators/debounce.ts index d0f4d9465f..2d8fb4e8bb 100644 --- a/src/internal/operators/debounce.ts +++ b/src/internal/operators/debounce.ts @@ -1,11 +1,11 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; -import { MonoTypeOperatorFunction, SubscribableOrPromise, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction, SubscribableOrPromise } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; /** * Emits a notification from the source Observable only after a particular time span @@ -66,90 +66,57 @@ import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '.. * @name debounce */ export function debounce(durationSelector: (value: T) => SubscribableOrPromise): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, 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 SimpleOuterSubscriber { - private value: T | null = null; - private hasValue: boolean = false; - private durationSubscription: Subscription | null | undefined = 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 = innerSubscribe(duration, new SimpleInnerSubscriber(this)); - if (subscription && !subscription.closed) { - this.add(this.durationSubscription = subscription); - } - } - - notifyNext(): void { - this.emitValue(); - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let lastValue: T | null = null; + // The subscriber/subscription for the current debounce, if there is one. + let durationSubscriber: Subscriber | null = null; - notifyComplete(): void { - this.emitValue(); - } + const emit = () => { + // Unsubscribe any current debounce subscription we have, + // we only cared about the first notification from it, and we + // want to clean that subscription up as soon as possible. + durationSubscriber?.unsubscribe(); + durationSubscriber = null; + if (hasValue) { + // We have a value! Free up memory first, then emit the value. + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + } + }; - emitValue(): void { - if (this.hasValue) { - const value = this.value; - const subscription = this.durationSubscription; - if (subscription) { - this.durationSubscription = null; - subscription.unsubscribe(); - this.remove(subscription); - } - // This must be done *before* passing the value - // along to the destination because it's possible for - // the value to synchronously re-enter this operator - // recursively if the duration selector Observable - // emits synchronously - this.value = null; - this.hasValue = false; - super._next(value!); - } - } + source.subscribe( + new OperatorSubscriber( + subscriber, + (value: T) => { + // Cancel any pending debounce duration. We don't + // need to null it out here yet tho, because we're just going + // to create another one in a few lines. + durationSubscriber?.unsubscribe(); + hasValue = true; + lastValue = value; + // Capture our duration subscriber, so we can unsubscribe it when we're notified + // and we're going to emit the value. + durationSubscriber = new OperatorSubscriber(subscriber, emit, undefined, emit); + // Subscribe to the duration. + from(durationSelector(value)).subscribe(durationSubscriber); + }, + undefined, + () => { + // Source completed. + // Emit any pending debounced values then complete + emit(); + subscriber.complete(); + }, + () => { + // Teardown. + lastValue = durationSubscriber = null; + } + ) + ); + }); } diff --git a/src/internal/operators/debounceTime.ts b/src/internal/operators/debounceTime.ts index 4e8b073aa1..7df0b93c40 100644 --- a/src/internal/operators/debounceTime.ts +++ b/src/internal/operators/debounceTime.ts @@ -1,10 +1,11 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; -import { async } from '../scheduler/async'; -import { MonoTypeOperatorFunction, SchedulerLike, TeardownLogic } from '../types'; +import { asyncScheduler } from '../scheduler/async'; +import { MonoTypeOperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits a notification from the source Observable only after a particular time span @@ -64,74 +65,60 @@ import { lift } from '../util/lift'; * too frequently. * @name debounceTime */ -export function debounceTime(dueTime: number, scheduler: SchedulerLike = async): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new DebounceTimeOperator(dueTime, scheduler)); -} - -class DebounceTimeOperator implements Operator { - constructor(private dueTime: number, private scheduler: SchedulerLike) { - } - - 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 = null; - private lastValue: T | null = null; - private hasValue: boolean = false; +export function debounceTime(dueTime: number, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // Used to note that we have a value. This is mostly for the + // completion phase. There we have to check to see if we have a value + // waiting, and emit it if we do. + let hasValue = false; + // The last value that has arrived via `next`. + let lastValue: T | null = null; + // The subscription for our debounce period. + let debounceSubscription: Subscription | null = null; - constructor(destination: Subscriber, - private dueTime: number, - private scheduler: SchedulerLike) { - super(destination); - } - - protected _next(value: T) { - this.clearDebounce(); - this.lastValue = value; - this.hasValue = true; - this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext as any, this.dueTime, this)); - } - - protected _complete() { - this.debouncedNext(); - this.destination.complete(); - } - - debouncedNext(): void { - this.clearDebounce(); - - if (this.hasValue) { - const { lastValue } = this; - // This must be done *before* passing the value - // along to the destination because it's possible for - // the value to synchronously re-enter this operator - // recursively when scheduled with things like - // VirtualScheduler/TestScheduler. - this.lastValue = null; - this.hasValue = false; - this.destination.next(lastValue); - } - } - - private clearDebounce(): void { - const debouncedSubscription = this.debouncedSubscription; - - if (debouncedSubscription !== null) { - this.remove(debouncedSubscription); - debouncedSubscription.unsubscribe(); - this.debouncedSubscription = null; - } - } -} + /** + * Emits the last value seen and clears it. + */ + const emitLastValue = () => { + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + }; -function dispatchNext(subscriber: DebounceTimeSubscriber) { - subscriber.debouncedNext(); + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + // Cancel the previous debounce period, because + // we are going to start a new one. + debounceSubscription?.unsubscribe(); + // Record the value + hasValue = true; + lastValue = value; + // Start a new debounce period. Notice that we are capturing + // the subscription for it here so we can cancel it if we have to. + subscriber.add( + (debounceSubscription = scheduler.schedule(() => { + // Release the subscription for the debounce. + debounceSubscription = null; + // We don't need to check to see if we have a value here, + // we can just emit it, because we can't possibly get + // here if we didn't already get a value. + emitLastValue(); + }, dueTime)) + ); + }, + // Let errors pass through + undefined, + () => { + // If we have a value waiting, emit it. + hasValue && emitLastValue(); + subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/defaultIfEmpty.ts b/src/internal/operators/defaultIfEmpty.ts index e2dcf8dcff..9e4339a0d8 100644 --- a/src/internal/operators/defaultIfEmpty.ts +++ b/src/internal/operators/defaultIfEmpty.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ export function defaultIfEmpty(defaultValue?: R): OperatorFunction; @@ -44,40 +45,25 @@ export function defaultIfEmpty(defaultValue?: R): OperatorFunction(defaultValue: R | null = null): OperatorFunction { - return (source: Observable) => lift(source, new DefaultIfEmptyOperator(defaultValue)) as Observable; -} - -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 (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + hasValue = true; + subscriber.next(value); + }, + undefined, + () => { + if (!hasValue) { + subscriber.next(defaultValue!); + } + subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/delay.ts b/src/internal/operators/delay.ts index e1e620d865..526e0e861a 100644 --- a/src/internal/operators/delay.ts +++ b/src/internal/operators/delay.ts @@ -1,15 +1,11 @@ -import { async } from '../scheduler/async'; +/** @prettier */ +import { asyncScheduler } from '../scheduler/async'; import { isValidDate } from '../util/isDate'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { - MonoTypeOperatorFunction, - SchedulerAction, - SchedulerLike, - TeardownLogic -} from '../types'; +import { MonoTypeOperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Delays the emission of items from the source Observable by a given timeout or @@ -59,96 +55,83 @@ import { lift } from '../util/lift'; * @return {Observable} An Observable that delays the emissions of the source * Observable by the specified timeout or Date. */ -export function delay(delay: number | Date, scheduler: SchedulerLike = async): MonoTypeOperatorFunction { - const delayFor = isValidDate(delay) ? +delay - scheduler.now() : Math.abs(delay); - return (source: Observable) => lift(source, new DelayOperator(delayFor, scheduler)); -} - -class DelayOperator implements Operator { - constructor(private delay: number, private scheduler: SchedulerLike) {} - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler)); - } -} +export function delay(delay: number | Date, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction { + // TODO: Properly handle negative delays and dates in the past. + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + const isAbsoluteDelay = isValidDate(delay); + // If the source is complete + let isComplete = false; + // The number of active delays in progress. + let active = 0; + // For absolute time delay, we collect the values in this array and emit + // them when the delay fires. + let absoluteTimeValues: T[] | null = isAbsoluteDelay ? [] : null; -interface DelayState { - source: DelaySubscriber; - destination: Subscriber; - scheduler: SchedulerLike; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class DelaySubscriber extends Subscriber { - private queue: Array> = []; - private active: boolean = false; + /** + * Used to check to see if we should complete the resulting + * subscription after delays finish or when the source completes. + * We don't want to complete when the source completes if we + * have delays in flight. + */ + const checkComplete = () => isComplete && !active && !absoluteTimeValues?.length && subscriber.complete(); - private static dispatch(this: SchedulerAction>, state: DelayState): void { - const source = state.source; - const queue = source.queue; - const scheduler = state.scheduler; - const destination = state.destination; + if (isAbsoluteDelay) { + // A date was passed. We only do one delay, so let's get it + // scheduled right away. + active++; + subscriber.add( + scheduler.schedule(() => { + active--; + if (absoluteTimeValues) { + const values = absoluteTimeValues; + absoluteTimeValues = null; + for (const value of values) { + subscriber.next(value); + } + } + checkComplete(); + }, +delay - scheduler.now()) + ); + } - while (queue.length > 0 && queue[0].time - scheduler.now() <= 0) { - destination.next(queue.shift()!.value); - } - - if (queue.length > 0) { - const delay = Math.max(0, queue[0].time - scheduler.now()); - this.schedule(state, delay); - } else if (source.isStopped) { - source.destination.complete(); - source.active = false; - } else { - this.unsubscribe(); - source.active = false; - } - } - - constructor(protected destination: Subscriber, private delay: number, private scheduler: SchedulerLike) { - super(destination); - } - - private _schedule(scheduler: SchedulerLike): void { - this.active = true; - const { destination } = this; - // TODO: The cast below seems like an issue with typings for SchedulerLike to me. - destination.add( - scheduler.schedule>(DelaySubscriber.dispatch as any, this.delay, { - source: this, - destination, - scheduler, - } as DelayState) - ); - } - - protected _next(value: T) { - const scheduler = this.scheduler; - const message = new DelayMessage(scheduler.now() + this.delay, value); - this.queue.push(message); - if (this.active === false) { - this._schedule(scheduler); - } - } - - protected _error(err: any) { - this.queue.length = 0; - this.destination.error(err); - this.unsubscribe(); - } - - protected _complete() { - if (this.queue.length === 0) { - this.destination.complete(); - } - this.unsubscribe(); - } -} + // Subscribe to the source + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + if (isAbsoluteDelay) { + // If we're dealing with an absolute time (via Date) delay, then before + // the delay fires, the `absoluteTimeValues` array will be present, and + // we want to add them to that. Otherwise, if it's `null`, that is because + // the delay has already fired. + absoluteTimeValues ? absoluteTimeValues.push(value) : subscriber.next(value); + } else { + active++; + subscriber.add( + scheduler.schedule(() => { + active--; + subscriber.next(value); + checkComplete(); + }, delay as number) + ); + } + }, + // Allow errors to pass through. + undefined, + () => { + isComplete = true; + checkComplete(); + } + ) + ); -class DelayMessage { - constructor(public readonly time: number, public readonly value: T) {} + // Additional teardown. The other teardown is set up + // implicitly by subscribing with Subscribers. + return () => { + // Release the buffered values. + absoluteTimeValues = null!; + }; + }); } diff --git a/src/internal/operators/delayWhen.ts b/src/internal/operators/delayWhen.ts index 48628f89cc..067461bc03 100644 --- a/src/internal/operators/delayWhen.ts +++ b/src/internal/operators/delayWhen.ts @@ -1,15 +1,24 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; -import { ComplexOuterSubscriber, ComplexInnerSubscriber, innerSubscribe } from '../innerSubscribe'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { concat } from '../observable/concat'; +import { take } from './take'; +import { ignoreElements } from './ignoreElements'; /* tslint:disable:max-line-length */ /** @deprecated In future versions, empty notifiers will no longer re-emit the source value on the output observable. */ -export function delayWhen(delayDurationSelector: (value: T, index: number) => Observable, subscriptionDelay?: Observable): MonoTypeOperatorFunction; -export function delayWhen(delayDurationSelector: (value: T, index: number) => Observable, subscriptionDelay?: Observable): MonoTypeOperatorFunction; +export function delayWhen( + delayDurationSelector: (value: T, index: number) => Observable, + subscriptionDelay?: Observable +): MonoTypeOperatorFunction; +/** @deprecated In future versions, `subscriptionDelay` will no longer be supported. */ +export function delayWhen( + delayDurationSelector: (value: T, index: number) => Observable, + subscriptionDelay?: Observable +): MonoTypeOperatorFunction; /* tslint:disable:max-line-length */ /** @@ -71,151 +80,80 @@ export function delayWhen(delayDurationSelector: (value: T, index: number) => * `delayDurationSelector`. * @name delayWhen */ -export function delayWhen(delayDurationSelector: (value: T, index: number) => Observable, - subscriptionDelay?: Observable): MonoTypeOperatorFunction { +export function delayWhen( + delayDurationSelector: (value: T, index: number) => Observable, + subscriptionDelay?: Observable +): MonoTypeOperatorFunction { if (subscriptionDelay) { + // DEPRECATED PATH return (source: Observable) => - lift(new SubscriptionDelayObservable(source, subscriptionDelay), new DelayWhenOperator(delayDurationSelector)); - } - return (source: Observable) => lift(source, new DelayWhenOperator(delayDurationSelector)); -} - -class DelayWhenOperator implements Operator { - constructor(private delayDurationSelector: (value: T, index: number) => 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 ComplexOuterSubscriber { - private completed: boolean = false; - private delayNotifierSubscriptions: Array = []; - private index: number = 0; - - constructor(destination: Subscriber, - private delayDurationSelector: (value: T, index: number) => Observable) { - super(destination); - } - - notifyNext(outerValue: T, _innerValue: any, - _outerIndex: number, innerSub: ComplexInnerSubscriber): void { - this.destination.next(outerValue); - this.removeSubscription(innerSub); - this.tryComplete(); - } - - notifyError(error: any): void { - this._error(error); - } - - notifyComplete(innerSub: ComplexInnerSubscriber): void { - const value = this.removeSubscription(innerSub); - if (value) { - this.destination.next(value); - } - this.tryComplete(); - } - - protected _next(value: T): void { - const index = this.index++; - try { - const delayNotifier = this.delayDurationSelector(value, index); - if (delayNotifier) { - this.tryDelay(delayNotifier, value); - } - } catch (err) { - this.destination.error(err); - } - } - - protected _complete(): void { - this.completed = true; - this.tryComplete(); - this.unsubscribe(); - } - - private removeSubscription(subscription: ComplexInnerSubscriber): T { - subscription.unsubscribe(); - - const subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription); - if (subscriptionIdx !== -1) { - this.delayNotifierSubscriptions.splice(subscriptionIdx, 1); - } - - return subscription.outerValue; - } - - private tryDelay(delayNotifier: Observable, value: T): void { - const notifierSubscription = innerSubscribe(delayNotifier, new ComplexInnerSubscriber(this, value, 0)); - - if (notifierSubscription && !notifierSubscription.closed) { - const destination = this.destination as Subscription; - destination.add(notifierSubscription); - this.delayNotifierSubscriptions.push(notifierSubscription); - } - } - - 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(public source: Observable, private subscriptionDelay: Observable) { - super(); - } - - /** @deprecated This is an internal implementation detail, do not use. */ - _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.unsubscribe(); - this.subscribeToSource(); - } - - private subscribeToSource(): void { - if (!this.sourceSubscribed) { - this.sourceSubscribed = true; - this.unsubscribe(); - this.source.subscribe(this.parent); - } - } + concat(subscriptionDelay.pipe(take(1), ignoreElements()), source.pipe(delayWhen(delayDurationSelector))); + } + + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // An index to give to the projection function. + let index = 0; + // Whether or not the source has completed. + let isComplete = false; + // Tracks the number of actively delayed values we have. + let active = 0; + + /** + * Checks to see if we can complete the result and completes it, if so. + */ + const checkComplete = () => isComplete && !active && subscriber.complete(); + + source.subscribe( + new OperatorSubscriber( + subscriber, + (value: T) => { + // Closed bit to guard reentrancy and + // synchronous next/complete (which both make the same calls right now) + let closed = false; + + /** + * Notifies the consumer of the value. + */ + const notify = () => { + // Notify the consumer. + subscriber.next(value); + + // Ensure our inner subscription is cleaned up + // as soon as possible. Once the first `next` fires, + // we have no more use for this subscription. + durationSubscriber?.unsubscribe(); + + if (!closed) { + active--; + closed = true; + checkComplete(); + } + }; + + // We have to capture our duration subscriber so we can unsubscribe from + // it on the first next notification it gives us. + const durationSubscriber = new OperatorSubscriber( + subscriber, + notify, + // Errors are sent to consumer. + undefined, + // TODO(benlesh): I'm inclined to say this is _incorrect_ behavior. + // A completion should not be a notification. Note the deprecation above + notify + ); + + active++; + delayDurationSelector(value, index++).subscribe(durationSubscriber); + }, + // Errors are passed through to consumer. + undefined, + () => { + isComplete = true; + checkComplete(); + } + ) + ); + }); } diff --git a/src/internal/operators/dematerialize.ts b/src/internal/operators/dematerialize.ts index 93660eaad4..f3894e9cef 100644 --- a/src/internal/operators/dematerialize.ts +++ b/src/internal/operators/dematerialize.ts @@ -1,9 +1,9 @@ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { observeNotification } from '../Notification'; import { OperatorFunction, ObservableNotification, ValueFromNotification } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Converts an Observable of {@link ObservableNotification} objects into the emissions @@ -53,28 +53,10 @@ import { lift } from '../util/lift'; * embedded in Notification objects emitted by the source Observable. */ export function dematerialize>(): OperatorFunction> { - return function dematerializeOperatorFunction(source: Observable) { - return lift(source, new DeMaterializeOperator()); + return (source: Observable) => { + return lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + return source.subscribe(new OperatorSubscriber(subscriber, (notification) => observeNotification(notification, subscriber))) + }); }; } - -class DeMaterializeOperator> 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(notification: N) { - observeNotification(notification, this.destination); - } -} diff --git a/src/internal/operators/distinct.ts b/src/internal/operators/distinct.ts index e0cba04ec9..0e2a27bb06 100644 --- a/src/internal/operators/distinct.ts +++ b/src/internal/operators/distinct.ts @@ -1,9 +1,10 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { noop } from '../util/noop'; /** * Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from previous items. @@ -72,70 +73,21 @@ import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '.. * @return {Observable} An Observable that emits items from the source Observable with distinct values. * @name distinct */ -export function distinct(keySelector?: (value: T) => K, - flushes?: Observable): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new DistinctOperator(keySelector, flushes)); -} - -class DistinctOperator implements Operator { - constructor(private keySelector?: (value: T) => K, private flushes?: Observable) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new DistinctSubscriber(subscriber, this.keySelector, this.flushes)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class DistinctSubscriber extends SimpleOuterSubscriber { - private values = new Set(); - - constructor(destination: Subscriber, private keySelector?: (value: T) => K, flushes?: Observable) { - super(destination); - - if (flushes) { - this.add(innerSubscribe(flushes, new SimpleInnerSubscriber(this))); - } - } - - notifyNext(): void { - this.values.clear(); - } - - notifyError(error: any): void { - this._error(error); - } - - protected _next(value: T): void { - if (this.keySelector) { - this._useKeySelector(value); - } else { - this._finalizeNext(value, value); - } - } - - private _useKeySelector(value: T): void { - let key: K; - const { destination } = this; - try { - key = this.keySelector!(value); - } catch (err) { - destination.error(err); - return; - } - this._finalizeNext(key, value); - } - - private _finalizeNext(key: K|T, value: T) { - const { values } = this; - if (!values.has(key)) { - values.add(key); - this.destination.next(value); - } - } +export function distinct(keySelector?: (value: T) => K, flushes?: Observable): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + const distinctKeys = new Set(); + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + const key = keySelector ? keySelector(value) : value; + if (!distinctKeys.has(key)) { + distinctKeys.add(key); + subscriber.next(value); + } + }) + ); + flushes?.subscribe(new OperatorSubscriber(subscriber, () => distinctKeys.clear(), undefined, noop)); + }); } diff --git a/src/internal/operators/distinctUntilChanged.ts b/src/internal/operators/distinctUntilChanged.ts index d07a7f517c..3514550f32 100644 --- a/src/internal/operators/distinctUntilChanged.ts +++ b/src/internal/operators/distinctUntilChanged.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ export function distinctUntilChanged(compare?: (x: T, y: T) => boolean): MonoTypeOperatorFunction; @@ -63,64 +64,28 @@ export function distinctUntilChanged(compare: (x: K, y: K) => boolean, key * @return {Observable} An Observable that emits items from the source Observable with distinct values. * @name distinctUntilChanged */ -export function distinctUntilChanged(compare?: (x: K, y: K) => boolean, keySelector?: (x: T) => K): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new DistinctUntilChangedOperator(compare, keySelector)); +export function distinctUntilChanged(compare?: (a: K, b: K) => boolean, keySelector?: (x: T) => K): MonoTypeOperatorFunction { + compare = compare ?? defaultCompare; + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let prev: any; + let first = true; + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + // WARNING: Intentionally terse code for library size. + // If this is the first value, set the previous value state, the `1` is to allow it to move to the next + // part of the terse conditional. Then we capture `prev` to pass to `compare`, but set `prev` to the result of + // either the `keySelector` -- if provided -- or the `value`, *then* it will execute the `compare`. + // If `compare` returns truthy, it will move on to call `subscriber.next()`. + ((first && ((prev = value), 1)) || !compare!(prev, (prev = keySelector ? keySelector(value) : (value as any)))) && + subscriber.next(value); + first = false; + }) + ); + }); } -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 | undefined; - 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 { - let key: any; - try { - const { keySelector } = this; - key = keySelector ? keySelector(value) : value; - } catch (err) { - return this.destination.error(err); - } - let result = false; - if (this.hasKey) { - try { - const { compare } = this; - result = compare(this.key, key); - } catch (err) { - return this.destination.error(err); - } - } else { - this.hasKey = true; - } - if (!result) { - this.key = key; - this.destination.next(value); - } - } +function defaultCompare(a: any, b: any) { + return a === b; } diff --git a/src/internal/operators/every.ts b/src/internal/operators/every.ts index 43d349460e..852b5e0bd3 100644 --- a/src/internal/operators/every.ts +++ b/src/internal/operators/every.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { Observer, OperatorFunction } from '../types'; +import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that emits whether or not every item of the source satisfies the condition specified. @@ -29,58 +30,29 @@ import { lift } from '../util/lift'; * @return {Observable} An Observable of booleans that determines if all items of the source Observable meet the condition specified. * @name every */ -export function every(predicate: (value: T, index: number, source: Observable) => boolean, - thisArg?: any): OperatorFunction { - return (source: Observable) => lift(source, 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); - } +export function every( + predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any +): OperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let index = 0; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + if (!predicate.call(thisArg, value, index, source)) { + subscriber.next(false); + subscriber.complete(); + } + }, + undefined, + () => { + subscriber.next(true); + subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/exhaust.ts b/src/internal/operators/exhaust.ts index 31bed5f145..086460578f 100644 --- a/src/internal/operators/exhaust.ts +++ b/src/internal/operators/exhaust.ts @@ -1,10 +1,10 @@ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; -import { ObservableInput, OperatorFunction, TeardownLogic } from '../types'; +import { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; export function exhaust(): OperatorFunction, T>; export function exhaust(): OperatorFunction; @@ -53,45 +53,20 @@ export function exhaust(): OperatorFunction; * @name exhaust */ export function exhaust(): OperatorFunction { - return (source: Observable) => lift(source, 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 SimpleOuterSubscriber { - private hasCompleted = false; - private innerSubscription?: Subscription; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(value: T): void { - if (!this.innerSubscription) { - this.add(this.innerSubscription = innerSubscribe(value, new SimpleInnerSubscriber(this))); - } - } - - protected _complete(): void { - this.hasCompleted = true; - if (!this.innerSubscription) { - this.destination.complete(); - } - } - - notifyComplete(): void { - this.innerSubscription = undefined; - if (this.hasCompleted) { - this.destination.complete(); - } - } -} + return (source: Observable>) => lift(source, function (this: Subscriber, source: Observable>) { + const subscriber = this; + let isComplete = false; + let innerSub: Subscription | null = null; + source.subscribe(new OperatorSubscriber(subscriber, inner => { + if (!innerSub) { + innerSub = from(inner).subscribe(new OperatorSubscriber(subscriber, undefined, undefined, () => { + innerSub = null; + isComplete && subscriber.complete(); + })) + } + }, undefined, () => { + isComplete = true; + !innerSub && subscriber.complete(); + })) + }); +} \ No newline at end of file diff --git a/src/internal/operators/exhaustMap.ts b/src/internal/operators/exhaustMap.ts index 931d7c8a19..5c09e9e760 100644 --- a/src/internal/operators/exhaustMap.ts +++ b/src/internal/operators/exhaustMap.ts @@ -1,19 +1,26 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; import { ObservableInput, OperatorFunction, ObservedValueOf } from '../types'; import { map } from './map'; import { from } from '../observable/from'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ -export function exhaustMap>(project: (value: T, index: number) => O): OperatorFunction>; +export function exhaustMap>( + project: (value: T, index: number) => O +): OperatorFunction>; /** @deprecated resultSelector is no longer supported. Use inner map instead. */ -export function exhaustMap>(project: (value: T, index: number) => O, resultSelector: undefined): OperatorFunction>; +export function exhaustMap>( + project: (value: T, index: number) => O, + resultSelector: undefined +): OperatorFunction>; /** @deprecated resultSelector is no longer supported. Use inner map instead. */ -export function exhaustMap(project: (value: T, index: number) => ObservableInput, resultSelector: (outerValue: T, innerValue: I, outerIndex: number, innerIndex: number) => R): 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 */ /** @@ -62,82 +69,37 @@ export function exhaustMap(project: (value: T, index: number) => Observ */ export function exhaustMap>( project: (value: T, index: number) => O, - resultSelector?: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R, -): OperatorFunction|R> { + resultSelector?: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R +): OperatorFunction | R> { if (resultSelector) { // DEPRECATED PATH - return (source: Observable) => source.pipe( - exhaustMap((a, i) => from(project(a, i)).pipe( - map((b: any, ii: any) => resultSelector(a, b, i, ii)), - )), - ); + return (source: Observable) => + source.pipe(exhaustMap((a, i) => from(project(a, i)).pipe(map((b: any, ii: any) => resultSelector(a, b, i, ii))))); } return (source: Observable) => - lift(source, new ExhaustMapOperator(project)); -} - -class ExhaustMapOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ExhaustMapSubscriber(subscriber, this.project)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ExhaustMapSubscriber extends SimpleOuterSubscriber { - private innerSubscription?: Subscription; - private hasCompleted = false; - private index = 0; - - constructor(protected destination: Subscriber, - private project: (value: T, index: number) => ObservableInput) { - super(destination); - } - - protected _next(value: T): void { - if (!this.innerSubscription) { - let result: ObservableInput; - const index = this.index++; - try { - result = this.project(value, index); - } catch (err) { - this.destination.error(err); - return; - } - const innerSubscriber = new SimpleInnerSubscriber(this); - const destination = this.destination; - destination.add(innerSubscriber); - this.innerSubscription = innerSubscriber; - innerSubscribe(result, innerSubscriber); - } - } - - protected _complete(): void { - this.hasCompleted = true; - if (!this.innerSubscription) { - this.destination.complete(); - } - this.unsubscribe(); - } - - notifyNext(innerValue: R): void { - this.destination.next(innerValue); - } - - notifyError(err: any): void { - this.destination.error(err); - } - - notifyComplete(): void { - this.innerSubscription = undefined; - if (this.hasCompleted) { - this.destination.complete(); - } - } + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let index = 0; + let innerSub: Subscriber | null = null; + let isComplete = false; + source.subscribe( + new OperatorSubscriber( + subscriber, + (outerValue) => { + if (!innerSub) { + innerSub = new OperatorSubscriber(subscriber, undefined, undefined, () => { + innerSub = null; + isComplete && subscriber.complete(); + }); + from(project(outerValue, index++)).subscribe(innerSub); + } + }, + undefined, + () => { + isComplete = true; + !innerSub && subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/expand.ts b/src/internal/operators/expand.ts index 0214fbf27c..83994db329 100644 --- a/src/internal/operators/expand.ts +++ b/src/internal/operators/expand.ts @@ -1,13 +1,22 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { MonoTypeOperatorFunction, OperatorFunction, ObservableInput, SchedulerLike } from '../types'; import { lift } from '../util/lift'; -import { SimpleInnerSubscriber, SimpleOuterSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; /* tslint:disable:max-line-length */ -export function expand(project: (value: T, index: number) => ObservableInput, concurrent?: number, scheduler?: SchedulerLike): OperatorFunction; -export function expand(project: (value: T, index: number) => ObservableInput, concurrent?: number, scheduler?: SchedulerLike): MonoTypeOperatorFunction; +export function expand( + project: (value: T, index: number) => ObservableInput, + concurrent?: number, + scheduler?: SchedulerLike +): OperatorFunction; +export function expand( + project: (value: T, index: number) => ObservableInput, + concurrent?: number, + scheduler?: SchedulerLike +): MonoTypeOperatorFunction; /* tslint:enable:max-line-length */ /** @@ -61,114 +70,85 @@ export function expand(project: (value: T, index: number) => ObservableInput< * from this transformation. * @name expand */ -export function expand(project: (value: T, index: number) => ObservableInput, - concurrent: number = Infinity, - scheduler?: SchedulerLike): OperatorFunction { +export function expand( + project: (value: T, index: number) => ObservableInput, + concurrent = Infinity, + scheduler?: SchedulerLike +): OperatorFunction { concurrent = (concurrent || 0) < 1 ? Infinity : concurrent; - return (source: Observable) => lift(source, new ExpandOperator(project, concurrent, scheduler)); -} - -export class ExpandOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput, - private concurrent: number, - private scheduler?: SchedulerLike) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new ExpandSubscriber(subscriber, this.project, this.concurrent, this.scheduler)); - } -} - -interface DispatchArg { - subscriber: ExpandSubscriber; - result: ObservableInput; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class ExpandSubscriber extends SimpleOuterSubscriber { - private index: number = 0; - private active: number = 0; - private hasCompleted: boolean = false; - private buffer: any[] | undefined; - - constructor(protected destination: Subscriber, - private project: (value: T, index: number) => ObservableInput, - private concurrent: number, - private scheduler?: SchedulerLike) { - super(destination); - if (concurrent < Infinity) { - this.buffer = []; - } - } - - private static dispatch(arg: DispatchArg): void { - const {subscriber, result} = arg; - subscriber.subscribeToProjection(result); - } - - 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); - try { - this.active++; - const { project } = this; - const result = project(value, index); - if (!this.scheduler) { - this.subscribeToProjection(result); - } else { - const state: DispatchArg = { subscriber: this, result }; - const destination = this.destination; - destination.add(this.scheduler.schedule>( - ExpandSubscriber.dispatch as any, - 0, - state - )); + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // The number of active subscriptions. + let active = 0; + // The buffered values that we will subscribe to. + let buffer: (T | R)[] = []; + // An index to pass to the projection function. + let index = 0; + // Whether or not the source has completed. + let isComplete = false; + + /** + * Emits the given value, then projects it into an inner observable which + * is then subscribed to for the expansion. + * @param value the value to emit and start the expansion with + */ + const emitAndExpand = (value: T | R) => { + subscriber.next(value); + // Doing the `from` and `project` here so that it is caught by the + // try/catch in our OperatorSubscriber. Otherwise, if we were to inline + // this in `doSub` below, if it is called with a scheduler, errors thrown + // would be out-of-band with the try/catch and we would have to do the + // try catching manually there. While this does mean we have to potentially + // keep a larger allocation (the observable) in memory, the tradeoff is it + // keeps the size down. + // TODO: Correct the types here. `project` could be R or T. + const inner = from(project(value as any, index++)); + active++; + const doSub = () => { + inner.subscribe( + new OperatorSubscriber(subscriber, next, undefined, () => { + --active === 0 && isComplete && !buffer.length ? subscriber.complete() : trySub(); + }) + ); + }; + + scheduler ? subscriber.add(scheduler.schedule(doSub)) : doSub(); + }; + + /** + * Tries to dequeue a value from the buffer, if there is one, and + * process it. + */ + const trySub = () => { + // It seems like here we could just make the assumption that we've arrived here because + // we need to start one more expansion because one has just completed. However, it's + // possible, due to scheduling, that multiple inner subscriptions could complete and we + // could need to start more than one inner subscription from our buffer. Hence the loop. + while (0 < buffer.length && active < concurrent) { + emitAndExpand(buffer.shift()!); } - } catch (e) { - destination.error(e); - } - } else { - this.buffer!.push(value); - } - } - - private subscribeToProjection(result: any): void { - this.destination.add(innerSubscribe(result, new SimpleInnerSubscriber(this))); - } - - protected _complete(): void { - this.hasCompleted = true; - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - this.unsubscribe(); - } - - notifyNext(innerValue: R): void { - this._next(innerValue); - } - - notifyComplete(): void { - const buffer = this.buffer; - this.active--; - if (buffer && buffer.length > 0) { - this._next(buffer.shift()); - } - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - } + }; + + /** + * Handle the next value. Captured here because this is called "recursively" by both incoming + * values from the source, and values emitted by the expanded inner subscriptions. + * @param value The value to process + */ + const next = (value: T | R) => (active < concurrent ? emitAndExpand(value) : buffer.push(value)); + + // subscribe to our source. + source.subscribe( + new OperatorSubscriber(subscriber, next, undefined, () => { + isComplete = true; + active === 0 && subscriber.complete(); + }) + ); + + return () => { + // Release buffered values. + buffer = null!; + }; + }); } diff --git a/src/internal/operators/filter.ts b/src/internal/operators/filter.ts index 55a4d92681..0b2a1b7670 100644 --- a/src/internal/operators/filter.ts +++ b/src/internal/operators/filter.ts @@ -1,16 +1,15 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { OperatorFunction, MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* 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) => value is S, thisArg?: any): OperatorFunction; // NOTE(benlesh): T|null|undefined solves the issue discussed here: https://github.com/ReactiveX/rxjs/issues/4959#issuecomment-520629091 -export function filter(predicate: BooleanConstructor): OperatorFunction>; -export function filter(predicate: (value: T, index: number) => boolean, - thisArg?: any): MonoTypeOperatorFunction; +export function filter(predicate: BooleanConstructor): OperatorFunction>; +export function filter(predicate: (value: T, index: number) => boolean, thisArg?: any): MonoTypeOperatorFunction; /* tslint:enable:max-line-length */ /** @@ -54,50 +53,24 @@ export function filter(predicate: (value: T, index: number) => boolean, * @param thisArg An optional argument to determine the value of `this` * in the `predicate` function. */ -export function filter(predicate: (value: T, index: number) => boolean, - thisArg?: any): MonoTypeOperatorFunction { - return function filterOperatorFunction(source: Observable): Observable { - return lift(source, 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); - } +export function filter(predicate: (value: T, index: number) => boolean, thisArg?: any): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // An index passed to our predicate function on each call. + let index = 0; - // 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); - } - } + // Subscribe to the source, all errors and completions are + // forwarded to the consumer. + return source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + // Call the predicate with the appropriate `this` context, + // if the predicate returns `true`, then send the value + // to the consumer. + if (predicate.call(thisArg, value, index++)) { + subscriber.next(value); + } + }) + ); + }); } diff --git a/src/internal/operators/finalize.ts b/src/internal/operators/finalize.ts index ec8d978f66..7a75c52f9e 100644 --- a/src/internal/operators/finalize.ts +++ b/src/internal/operators/finalize.ts @@ -1,7 +1,6 @@ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; /** @@ -60,16 +59,8 @@ import { lift } from '../util/lift'; * @name finally */ export function finalize(callback: () => void): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new FinallyOperator(callback)); -} - -class FinallyOperator implements Operator { - constructor(private callback: () => void) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - const subscription = source.subscribe(subscriber); - subscription.add(this.callback); - return subscription; - } + return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { + source.subscribe(this); + this.add(callback); + }); } diff --git a/src/internal/operators/find.ts b/src/internal/operators/find.ts index e8724336de..3446ded2b3 100644 --- a/src/internal/operators/find.ts +++ b/src/internal/operators/find.ts @@ -1,13 +1,18 @@ -import {Observable} from '../Observable'; -import {Operator} from '../Operator'; -import {Subscriber} from '../Subscriber'; -import {OperatorFunction} from '../types'; +/** @prettier */ +import { Observable } from '../Observable'; +import { Subscriber } from '../Subscriber'; +import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; -export function find(predicate: (value: T, index: number, source: Observable) => value is S, - thisArg?: any): OperatorFunction; -export function find(predicate: (value: T, index: number, source: Observable) => boolean, - thisArg?: any): OperatorFunction; +export function find( + predicate: (value: T, index: number, source: Observable) => value is S, + thisArg?: any +): OperatorFunction; +export function find( + predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any +): OperatorFunction; /** * Emits only the first value emitted by the source Observable that meets some * condition. @@ -46,64 +51,38 @@ export function find(predicate: (value: T, index: number, source: Observable< * condition. * @name find */ -export function find(predicate: (value: T, index: number, source: Observable) => boolean, - thisArg?: any): OperatorFunction { - if (typeof predicate !== 'function') { - throw new TypeError('predicate is not a function'); - } - return (source: Observable) => lift(source, new FindValueOperator(predicate, source, false, thisArg)) as Observable; +export function find( + predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any +): OperatorFunction { + return (source: Observable) => lift(source, createFind(predicate, thisArg, 'value')); } -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(); - this.unsubscribe(); - } - - 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); - } +export function createFind( + predicate: (value: T, index: number, source: Observable) => boolean, + thisArg: any, + emit: 'value' | 'index' +) { + const findIndex = emit === 'index'; + return function (this: Subscriber, source: Observable) { + const subscriber = this; + let index = 0; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + const i = index++; + if (predicate.call(thisArg, value, i, source)) { + subscriber.next(findIndex ? i : value); + subscriber.complete(); + } + }, + undefined, + () => { + subscriber.next(findIndex ? -1 : undefined); + subscriber.complete(); + } + ) + ); + }; } diff --git a/src/internal/operators/findIndex.ts b/src/internal/operators/findIndex.ts index d42d1e2129..d3d942dfa2 100644 --- a/src/internal/operators/findIndex.ts +++ b/src/internal/operators/findIndex.ts @@ -1,7 +1,8 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { FindValueOperator } from '../operators/find'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { createFind } from './find'; /** * Emits only the index of the first value emitted by the source Observable that * meets some condition. @@ -41,7 +42,9 @@ import { lift } from '../util/lift'; * matches the condition. * @name find */ -export function findIndex(predicate: (value: T, index: number, source: Observable) => boolean, - thisArg?: any): OperatorFunction { - return (source: Observable) => lift(source, new FindValueOperator(predicate, source, true, thisArg)) as Observable; +export function findIndex( + predicate: (value: T, index: number, source: Observable) => boolean, + thisArg?: any +): OperatorFunction { + return (source: Observable) => lift(source, createFind(predicate, thisArg, 'index')); } diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 24c72f8d23..ce97c96e82 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -1,13 +1,11 @@ /** @prettier */ import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subject } from '../Subject'; -import { OperatorFunction } from '../types'; +import { Observer, OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; -/* tslint:disable:max-line-length */ export function groupBy( keySelector: (value: T) => value is K ): OperatorFunction | GroupedObservable>>; @@ -28,7 +26,6 @@ export function groupBy( durationSelector?: (grouped: GroupedObservable) => Observable, subjectSelector?: () => Subject ): OperatorFunction>; -/* tslint:enable:max-line-length */ /** * Groups the items emitted by an Observable according to a specified criterion, @@ -127,214 +124,139 @@ export function groupBy( durationSelector?: (grouped: GroupedObservable) => Observable, subjectSelector?: () => Subject ): OperatorFunction> { - return (source: Observable) => lift(source, new GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector)); -} + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + // A lookup for the groups that we have so far. + const groups = new Map>(); -export interface RefCountSubscription { - count: number; - unsubscribe: () => void; - closed: boolean; - attemptedToUnsubscribe: boolean; -} + // Used for notifying all groups and the subscriber in the same way. + const notify = (cb: (group: Observer) => void) => { + groups.forEach(cb); + cb(subscriber); + }; -class GroupByOperator implements Operator> { - constructor( - private keySelector: (value: T) => K, - private elementSelector?: ((value: T) => R) | void, - private durationSelector?: (grouped: GroupedObservable) => Observable, - private subjectSelector?: () => Subject - ) {} + // Capturing a reference to this, because we need a handle to it + // in `createGroupedObservable` below. This is what we use to + // subscribe to our source observable. This sometimes needs to be unsubscribed + // out-of-band with our `subscriber` which is the downstream subscriber, or destination, + // in cases where a user unsubscribes from the main resulting subscription, but + // still has groups from this subscription subscribed and would expect values from it + // Consider: `source.pipe(groupBy(fn), take(2))`. + const groupBySourceSubscriber = new GroupBySubscriber( + subscriber, + (value: T) => { + const key = keySelector(value); - call(subscriber: Subscriber>, source: any): any { - return source.subscribe( - new GroupBySubscriber(subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector) - ); - } -} + let group = groups.get(key); + if (!group) { + // Create our group subject + groups.set(key, (group = subjectSelector ? subjectSelector() : new Subject())); -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class GroupBySubscriber extends Subscriber implements RefCountSubscription { - private groups: Map> | null = 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); - } + // Emit the grouped observable. Note that we can't do a simple `asObservable()` here, + // because the grouped observable has special semantics around reference counting + // to ensure we don't sever our connection to the source prematurely. + const grouped = createGroupedObservable(key, group); + subscriber.next(grouped); - private _group(value: T, key: K) { - let groups = this.groups; + if (durationSelector) { + const durationSubscriber = new OperatorSubscriber( + // Providing the group here ensures that it is disposed of -- via `unsubscribe` -- + // wnen the duration subscription is torn down. That is important, because then + // if someone holds a handle to the grouped observable and tries to subscribe to it + // after the connection to the source has been severed, they will get an + // `ObjectUnsubscribedError` and know they can't possibly get any notifications. + group as any, + () => { + // Our duration notified! We can complete the group. + // The group will be removed from the map in the teardown phase. + group!.complete(); + durationSubscriber?.unsubscribe(); + }, + undefined, + undefined, + // Teardown: Remove this group from our map. + () => groups.delete(key) + ); - if (!groups) { - groups = this.groups = new Map>(); - } + // Start our duration notifier. + groupBySourceSubscriber.add(durationSelector(grouped).subscribe(durationSubscriber)); + } + } - let group = groups.get(key); + // Send the value to our group. + group.next(elementSelector ? elementSelector(value) : value); + }, + // Error from the source. + (err) => notify((consumer) => consumer.error(err)), + // Source completes. + () => notify((consumer) => consumer.complete()), + // Free up memory. + // When the source subscription is _finally_ torn down, release the subjects and keys + // in our groups Map, they may be quite large and we don't want to keep them around if we + // don't have to. + () => groups.clear() + ); - let element: R; - if (this.elementSelector) { - try { - element = this.elementSelector(value); - } catch (err) { - this.error(err); - } - } else { - element = value as any; - } + // Subscribe to the source + source.subscribe(groupBySourceSubscriber); - if (!group) { - group = (this.subjectSelector ? this.subjectSelector() : new Subject()) as 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))); + /** + * Creates the actual grouped observable returned. + * @param key The key of the group + * @param groupSubject The subject that fuels the group + */ + function createGroupedObservable(key: K, groupSubject: Subject) { + const result: any = new Observable((groupSubscriber) => { + groupBySourceSubscriber.activeGroups++; + const innerSub = groupSubject.subscribe(groupSubscriber); + return () => { + innerSub.unsubscribe(); + // We can kill the subscription to our source if we now have no more + // active groups subscribed, and a teardown was already attempted on + // the source. + --groupBySourceSubscriber.activeGroups === 0 && + groupBySourceSubscriber.teardownAttempted && + groupBySourceSubscriber.unsubscribe(); + }; + }); + result.key = key; + return result; } - } - - 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} + * This was created because groupBy is a bit unique, in that emitted groups that have + * subscriptions have to keep the subscription to the source alive until they + * are torn down. */ -class GroupDurationSubscriber extends Subscriber { - constructor(private key: K, group: Subject, private parent: GroupBySubscriber) { - super(group); - } - - protected _next(): void { - this.complete(); - } +class GroupBySubscriber extends OperatorSubscriber { + /** + * The number of actively subscribed groups + */ + activeGroups = 0; + /** + * Whether or not teardown was attempted on this subscription. + */ + teardownAttempted = false; unsubscribe() { - if (!this.closed) { - const { parent, key } = this; - this.key = this.parent = null!; - if (parent) { - parent.removeGroup(key); - } - super.unsubscribe(); - } + this.teardownAttempted = true; + // We only kill our subscription to the source if we have + // no active groups. As stated above, consider this scenario: + // source$.pipe(groupBy(fn), take(2)). + this.activeGroups === 0 && super.unsubscribe(); } } /** - * 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 + * An observable of values that is the emitted by the result of a {@link groupBy} operator, + * contains a `key` property for the grouping. */ -export class GroupedObservable extends Observable { - /** @deprecated Do not construct this type. Internal use only */ - constructor(public key: K, private groupSubject: Subject, private refCountSubscription?: RefCountSubscription) { - super(); - } - - /** @deprecated This is an internal implementation detail, do not use. */ - _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(); - } - } - } +export interface GroupedObservable extends Observable { + /** + * The key value for the grouped notifications. + */ + readonly key: K; } diff --git a/src/internal/operators/ignoreElements.ts b/src/internal/operators/ignoreElements.ts index 16b06167c5..80397367ae 100644 --- a/src/internal/operators/ignoreElements.ts +++ b/src/internal/operators/ignoreElements.ts @@ -1,8 +1,10 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { noop } from '../util/noop'; /** * Ignores all items emitted by the source Observable and only passes calls of `complete` or `error`. @@ -37,24 +39,9 @@ import { lift } from '../util/lift'; * @name ignoreElements */ export function ignoreElements(): OperatorFunction { - return function ignoreElementsOperatorFunction(source: Observable) { - return lift(source, 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 { - // Do nothing - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + source.subscribe(new OperatorSubscriber(subscriber, noop)); + }); } diff --git a/src/internal/operators/index.ts b/src/internal/operators/index.ts deleted file mode 100644 index 8d3b66be45..0000000000 --- a/src/internal/operators/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -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 { combineAll } from './combineAll'; -export { combineLatest, combineLatestWith } from './combineLatestWith'; -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 { distinct } from './distinct'; -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 { isEmpty } from './isEmpty'; -export { last } from './last'; -export { map } from './map'; -export { mapTo } from './mapTo'; -export { materialize } from './materialize'; -export { max } from './max'; -export { mergeWith, merge } from './mergeWith'; -export { mergeAll } from './mergeAll'; -export { mergeMap } from './mergeMap'; -export { mergeMap as flatMap } from './mergeMap'; -export { mergeMapTo } from './mergeMapTo'; -export { mergeScan } from './mergeScan'; -export { min } from './min'; -export { multicast } from './multicast'; -export { observeOn } from './observeOn'; -export { onErrorResumeNext } from './onErrorResumeNext'; -export { pairwise } from './pairwise'; -export { partition } from './partition'; -export { pluck } from './pluck'; -export { publish } from './publish'; -export { publishBehavior } from './publishBehavior'; -export { publishLast } from './publishLast'; -export { publishReplay } from './publishReplay'; -export { race, raceWith } from './raceWith'; -export { reduce } from './reduce'; -export { repeat } from './repeat'; -export { repeatWhen } from './repeatWhen'; -export { retry } from './retry'; -export { retryWhen } from './retryWhen'; -export { refCount } from './refCount'; -export { sample } from './sample'; -export { sampleTime } from './sampleTime'; -export { scan } from './scan'; -export { sequenceEqual } from './sequenceEqual'; -export { share } from './share'; -export { shareReplay } from './shareReplay'; -export { single } from './single'; -export { skip } from './skip'; -export { skipLast } from './skipLast'; -export { skipUntil } from './skipUntil'; -export { skipWhile } from './skipWhile'; -export { startWith } from './startWith'; -export { subscribeOn } from './subscribeOn'; -export { switchAll } from './switchAll'; -export { switchMap } from './switchMap'; -export { switchMapTo } from './switchMapTo'; -export { take } from './take'; -export { takeLast } from './takeLast'; -export { takeUntil } from './takeUntil'; -export { takeWhile } from './takeWhile'; -export { tap } from './tap'; -export { throttle } from './throttle'; -export { throttleTime } from './throttleTime'; -export { timeInterval } from './timeInterval'; -export { timeout } from './timeout'; -export { timeoutWith } from './timeoutWith'; -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 { withLatestFrom } from './withLatestFrom'; -export { zip, zipWith } from './zipWith'; -export { zipAll } from './zipAll'; diff --git a/src/internal/operators/isEmpty.ts b/src/internal/operators/isEmpty.ts index 0f995c7128..bf79ba7385 100644 --- a/src/internal/operators/isEmpty.ts +++ b/src/internal/operators/isEmpty.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits `false` if the input Observable emits any values, or emits `true` if the @@ -69,37 +70,22 @@ import { lift } from '../util/lift'; */ export function isEmpty(): OperatorFunction { - return (source: Observable) => lift(source, new IsEmptyOperator()); -} - -class IsEmptyOperator implements Operator { - call (observer: Subscriber, source: any): any { - return source.subscribe(new IsEmptySubscriber(observer)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class IsEmptySubscriber extends Subscriber { - constructor(destination: Subscriber) { - super(destination); - } - - private notifyComplete(isEmpty: boolean): void { - const destination = this.destination; - - destination.next(isEmpty); - destination.complete(); - } - - protected _next(value: boolean) { - this.notifyComplete(false); - } - - protected _complete() { - this.notifyComplete(true); - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + source.subscribe( + new OperatorSubscriber( + subscriber, + () => { + subscriber.next(false); + subscriber.complete(); + }, + undefined, + () => { + subscriber.next(true); + subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/map.ts b/src/internal/operators/map.ts index 82d72c20ea..5f93c0bad3 100644 --- a/src/internal/operators/map.ts +++ b/src/internal/operators/map.ts @@ -1,7 +1,9 @@ +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Applies a given `project` function to each value emitted by the source @@ -42,39 +44,19 @@ import { lift } from '../util/lift'; * @name map */ 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 lift(source, function (this: Subscriber, source: Observable) { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; // The index of the value from the source. Used with projection. let index = 0; - source.subscribe(new MapSubscriber(subscriber, (value: T) => { - // Try the projection, and catch any errors so we can send them to the consumer - // as an error notification. - let result: R; - try { - // Call with the `thisArg`. At some point we want to get rid of this, - // as `fn.bind()` is more explicit and easier to read, however... as a - // note, if no `thisArg` is passed, the `this` context will be `undefined`, - // as no other default makes sense. - result = project.call(thisArg, value, index++) - } catch (err) { - // Notify the consumer of the error. - subscriber.error(err); - return; - } - // Success! Send the projected result to the consumer - subscriber.next(result); - })) + // Subscribe to the source, all errors and completions are sent along + // to the consumer. + source.subscribe( + new OperatorSubscriber(subscriber, (value: T) => { + // Call the projection function with the appropriate this context, + // and send the resulting value to the consumer. + subscriber.next(project.call(thisArg, value, index++)); + }) + ); }); - }; } - -class MapSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void) { - super(destination); - } -} \ No newline at end of file diff --git a/src/internal/operators/mapTo.ts b/src/internal/operators/mapTo.ts index 5bc5edd880..3aba80d8a3 100644 --- a/src/internal/operators/mapTo.ts +++ b/src/internal/operators/mapTo.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; export function mapTo(value: R): OperatorFunction; /** @deprecated remove in v8. Use mapTo(value: R): OperatorFunction signature instead **/ @@ -40,37 +41,16 @@ export function mapTo(value: R): OperatorFunction; * @name mapTo */ export function mapTo(value: R): OperatorFunction { - return (source: Observable) => lift(source, new MapToOperator(value)); -} - -class MapToOperator implements Operator { - - value: R; - - constructor(value: R) { - this.value = value; - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new MapToSubscriber(subscriber, this.value)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class MapToSubscriber extends Subscriber { - - value: R; - - constructor(destination: Subscriber, value: R) { - super(destination); - this.value = value; - } - - protected _next(x: T) { - this.destination.next(this.value); - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // Subscribe to the source. All errors and completions are forwarded to the consumer + source.subscribe( + new OperatorSubscriber( + subscriber, + // On every value from the source, send the `mapTo` value to the consumer. + () => subscriber.next(value) + ) + ); + }); } diff --git a/src/internal/operators/materialize.ts b/src/internal/operators/materialize.ts index 2d50edea3d..af32536a72 100644 --- a/src/internal/operators/materialize.ts +++ b/src/internal/operators/materialize.ts @@ -1,9 +1,10 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Notification } from '../Notification'; import { OperatorFunction, ObservableNotification } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Represents all of the notifications from the source Observable as `next` @@ -60,40 +61,24 @@ import { lift } from '../util/lift'; * will not be available on the emitted values at that time. */ export function materialize(): OperatorFunction & ObservableNotification> { - return function materializeOperatorFunction(source: Observable) { - return lift(source, new MaterializeOperator()); - }; -} - -class MaterializeOperator implements Operator & ObservableNotification> { - call(subscriber: Subscriber & ObservableNotification>, 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 (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + subscriber.next(Notification.createNext(value)); + }, + (err) => { + subscriber.next(Notification.createError(err)); + subscriber.complete(); + }, + () => { + subscriber.next(Notification.createComplete()); + subscriber.complete(); + } + ) + ); + }); } diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index f0990572d7..28479c08a0 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -1,13 +1,12 @@ /** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; import { ObservableInput, OperatorFunction, ObservedValueOf } from '../types'; import { map } from './map'; import { from } from '../observable/from'; import { lift } from '../util/lift'; -import { innerSubscribe, SimpleOuterSubscriber, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ export function mergeMap>( @@ -106,102 +105,90 @@ export function mergeMap>( // The buffered values from the source (used for concurrency) let buffer: T[] = []; + /** + * Called to check to see if we can complete, and completes the result if + * nothing is active. + */ + const checkComplete = () => isComplete && !active && subscriber.complete(); + /** * Attempts to start an inner subscription from a buffered value, * so long as we don't have more active inner subscriptions than * the concurrency limit allows. */ - const doInnerSub = () => { + const tryInnerSub = () => { while (active < concurrent && buffer.length > 0) { - const value = buffer.shift()!; - - // Get the inner source from the projection function - let innerSource: Observable>; - try { - innerSource = from(project(value, index++)); - } catch (err) { - subscriber.error(err); - return; - } - - // Subscribe to the inner source - active++; - let innerSubs: Subscription; - subscriber.add( - (innerSubs = innerSource.subscribe( - new MergeMapSubscriber( - subscriber, - (innerValue) => { - // INNER SOURCE NEXT - // We got a value from the inner source, emit it from the result. - subscriber.next(innerValue); - }, - () => { - // INNER SOURCE COMPLETE - // Decrement the active count to ensure that the next time - // we try to call `doInnerSub`, the number is accurate. - active--; - if (buffer.length > 0) { - // If we have more values in the buffer, try to process those - // Note that this call will increment `active` ahead of the - // next conditional, if there were any more inner subscriptions - // to start. - doInnerSub(); - } - if (isComplete && active === 0) { - // If the outer is complete, and there are no more active, - // then we can complete the resulting observable subscription - subscriber.complete(); - } - // Make sure to teardown the inner subscription ASAP. - innerSubs?.unsubscribe(); - } - ) - )) - ); + doInnerSub(buffer.shift()!); } }; + /** + * Creates an inner observable and subscribes to it with the + * given outer value. + * @param value the value to process + */ + const doInnerSub = (value: T) => { + // Subscribe to the inner source + active++; + subscriber.add( + from(project(value, index++)).subscribe( + new OperatorSubscriber( + subscriber, + // INNER SOURCE NEXT + // We got a value from the inner source, emit it from the result. + (innerValue) => subscriber.next(innerValue), + // Errors are sent to the consumer. + undefined, + () => { + // INNER SOURCE COMPLETE + // Decrement the active count to ensure that the next time + // we try to call `doInnerSub`, the number is accurate. + active--; + // If we have more values in the buffer, try to process those + // Note that this call will increment `active` ahead of the + // next conditional, if there were any more inner subscriptions + // to start. + buffer.length && tryInnerSub(); + // Check to see if we can complete, and complete if so. + checkComplete(); + } + ) + ) + ); + }; + let outerSubs: Subscription; outerSubs = source.subscribe( - new MergeMapSubscriber( + new OperatorSubscriber( subscriber, - (value) => { - // OUTER SOURCE NEXT - // Push the value onto the buffer. We have no idea what the concurrency limit - // is and we don't care. Just buffer it and then call `doInnerSub()` to try to - // process what is in the buffer. - buffer.push(value); - doInnerSub(); - }, + // OUTER SOURCE NEXT + // If we are under our concurrency limit, start the inner subscription with the value + // right away. Otherwise, push it onto the buffer and wait. + (value) => (active < concurrent ? doInnerSub(value) : buffer.push(value)), + // Let errors pass through. + undefined, () => { // OUTER SOURCE COMPLETE // We don't necessarily stop here. If have any pending inner subscriptions // we need to wait for those to be done first. That includes buffered inners // that we haven't even subscribed to yet. isComplete = true; - if (active === 0 && buffer.length === 0) { - // Nothing is active, and nothing in the buffer, with no hope of getting any more - // we can complete the result - subscriber.complete(); - } + // If nothing is active, and nothing in the buffer, with no hope of getting any more + // we can complete the result + checkComplete(); // Be sure to teardown the outer subscription ASAP, in any case. outerSubs?.unsubscribe(); } ) ); - }); -} -// TODO(benlesh): This may end up being so common that we can centralize on one Subscriber for a few operators. - -/** - * A simple overridden Subscriber, used in both inner and outer subscriptions - */ -class MergeMapSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void, protected _complete: () => void) { - super(destination); - } + // Additional teardown. Called when the destination is torn down. + // Other teardown is registered implicitly above during subscription. + return () => { + // Release buffered values + buffer = null!; + }; + }); } /** diff --git a/src/internal/operators/mergeScan.ts b/src/internal/operators/mergeScan.ts index f7163279c6..ead8063c41 100644 --- a/src/internal/operators/mergeScan.ts +++ b/src/internal/operators/mergeScan.ts @@ -1,9 +1,10 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleInnerSubscriber, SimpleOuterSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; /** * Applies an accumulator function over the source Observable where the @@ -43,96 +44,102 @@ import { SimpleInnerSubscriber, SimpleOuterSubscriber, innerSubscribe } from '.. * @return {Observable} An observable of the accumulated values. * @name mergeScan */ -export function mergeScan(accumulator: (acc: R, value: T, index: number) => ObservableInput, - seed: R, - concurrent: number = Infinity): OperatorFunction { - return (source: Observable) => lift(source, new MergeScanOperator(accumulator, seed, concurrent)); -} - -export class MergeScanOperator implements Operator { - constructor(private accumulator: (acc: R, value: T, index: number) => ObservableInput, - private seed: R, - private concurrent: number) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new MergeScanSubscriber( - subscriber, this.accumulator, this.seed, this.concurrent - )); - } -} +export function mergeScan( + accumulator: (acc: R, value: T, index: number) => ObservableInput, + seed: R, + concurrent = Infinity +): OperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // Buffered values, in the event of going over our concurrency limit + let buffer: T[] = []; + // The number of active inner subscriptions. + let active = 0; + // Whether or not we have gotten any accumulated state. This is used to + // decide whether or not to emit in the event of an empty result. + let hasState = false; + // The accumulated state. + let state = seed; + // An index to pass to our accumulator function + let index = 0; + // Whether or not the outer source has completed. + let isComplete = false; -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -export class MergeScanSubscriber extends SimpleOuterSubscriber { - private hasValue: boolean = false; - private hasCompleted: boolean = false; - private buffer: Observable[] = []; - private active: number = 0; - protected index: number = 0; + /** + * Checks to see if we can complete our result or not. + */ + const checkComplete = () => { + // If the outer has completed, and nothing is left in the buffer, + // and we don't have any active inner subscriptions, then we can + // Emit the state and complete. + if (isComplete && !buffer.length && !active) { + // TODO: This seems like it might result in a double emission, perhaps bad behavior? + // maybe we should change this in an upcoming major? + !hasState && subscriber.next(state); + subscriber.complete(); + } + }; - constructor(protected destination: Subscriber, - private accumulator: (acc: R, value: T, index: number) => ObservableInput, - private acc: R, - private concurrent: number) { - super(destination); - } + const doInnerSub = (value: T) => { + active++; + from(accumulator(state!, value, index++)).subscribe( + new OperatorSubscriber( + subscriber, + (innerValue) => { + hasState = true; + // Intentially terse. Set the state, then emit it. + subscriber.next((state = innerValue)); + }, + // Errors are passed to the destination. + undefined, - protected _next(value: any): void { - if (this.active < this.concurrent) { - const index = this.index++; - const destination = this.destination; - let ish; - try { - const { accumulator } = this; - ish = accumulator(this.acc, value, index); - } catch (e) { - return destination.error(e); - } - this.active++; - this._innerSub(ish); - } else { - this.buffer.push(value); - } - } + // TODO: Much of this code is duplicated from mergeMap. Perhaps + // look into a way to unify this. - private _innerSub(ish: any): void { - const innerSubscriber = new SimpleInnerSubscriber(this); - this.destination.add(innerSubscriber); - innerSubscribe(ish, innerSubscriber); - } + () => { + // INNER SOURCE COMPLETE + // Decrement the active count to ensure that the next time + // we try to call `doInnerSub`, the number is accurate. + active--; + // If we have more values in the buffer, try to process those + // Note that this call will increment `active` ahead of the + // next conditional, if there were any more inner subscriptions + // to start. + buffer.length && tryInnerSub(); + // Check to see if we can complete, and complete if so. + checkComplete(); + } + ) + ); + }; - protected _complete(): void { - this.hasCompleted = true; - if (this.active === 0 && this.buffer.length === 0) { - if (this.hasValue === false) { - this.destination.next(this.acc); - } - this.destination.complete(); - } - this.unsubscribe(); - } + const tryInnerSub = () => { + while (buffer.length && active < concurrent) { + doInnerSub(buffer.shift()!); + } + }; - notifyNext(innerValue: R): void { - const { destination } = this; - this.acc = innerValue; - this.hasValue = true; - destination.next(innerValue); - } + source.subscribe( + new OperatorSubscriber( + subscriber, + // If we're under our concurrency limit, just start the inner subscription, otherwise buffer and wait. + (value) => (active < concurrent ? doInnerSub(value) : buffer.push(value)), + // Errors are passed through + undefined, + () => { + // Outer completed, make a note of it, and check to see if we can complete everything. + isComplete = true; + checkComplete(); + } + ) + ); - notifyComplete(): void { - const buffer = this.buffer; - this.active--; - if (buffer.length > 0) { - this._next(buffer.shift()); - } else if (this.active === 0 && this.hasCompleted) { - if (this.hasValue === false) { - this.destination.next(this.acc); - } - this.destination.complete(); - } - } + // Additional teardown (for when the destination is torn down). + // Other teardown is added implicitly via subscription above. + return () => { + // Ensure buffered values are released. + buffer = null!; + }; + }); } diff --git a/src/internal/operators/mergeWith.ts b/src/internal/operators/mergeWith.ts index 5383c3b7a4..e9b26c6aae 100644 --- a/src/internal/operators/mergeWith.ts +++ b/src/internal/operators/mergeWith.ts @@ -1,22 +1,40 @@ -import { merge as mergeStatic } from '../observable/merge'; +/** @prettier */ import { Observable } from '../Observable'; import { ObservableInput, OperatorFunction, MonoTypeOperatorFunction, SchedulerLike, ObservedValueUnionFromArray } from '../types'; -import { stankyLift } from '../util/lift'; - -/* tslint:disable:max-line-length */ +import { lift } from '../util/lift'; +import { Subscriber } from '../Subscriber'; +import { isScheduler } from '../util/isScheduler'; +import { argsOrArgArray } from '../util/argsOrArgArray'; +import { fromArray } from '../observable/fromArray'; +import { mergeAll } from './mergeAll'; /** @deprecated use {@link mergeWith} */ export function merge(): MonoTypeOperatorFunction; /** @deprecated use {@link mergeWith} */ -export function merge(v2: ObservableInput, ): OperatorFunction; +export function merge(v2: ObservableInput): OperatorFunction; /** @deprecated use {@link mergeWith} */ -export function merge(v2: ObservableInput, v3: ObservableInput, ): OperatorFunction; +export function merge(v2: ObservableInput, v3: ObservableInput): OperatorFunction; /** @deprecated use {@link mergeWith} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, ): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput +): OperatorFunction; /** @deprecated use {@link mergeWith} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, ): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput +): OperatorFunction; /** @deprecated use {@link mergeWith} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, ): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput +): OperatorFunction; // Below are signatures we no longer wish to support in this format. // They include either a concurrency argument or a scheduler argument. @@ -33,39 +51,99 @@ export function merge(v2: ObservableInput, scheduler: SchedulerLike): /** @deprecated use static {@link merge} */ export function merge(v2: ObservableInput, concurrent: number, scheduler?: SchedulerLike): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, scheduler: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + scheduler: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, concurrent: number, scheduler?: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + concurrent: number, + scheduler?: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, scheduler: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + scheduler: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, concurrent: number, scheduler?: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + concurrent: number, + scheduler?: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, scheduler: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + scheduler: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, concurrent: number, scheduler?: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + concurrent: number, + scheduler?: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, scheduler: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput, + scheduler: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ -export function merge(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, concurrent: number, scheduler?: SchedulerLike): OperatorFunction; +export function merge( + v2: ObservableInput, + v3: ObservableInput, + v4: ObservableInput, + v5: ObservableInput, + v6: ObservableInput, + concurrent: number, + scheduler?: SchedulerLike +): OperatorFunction; /** @deprecated use static {@link merge} */ export function merge(...observables: Array | SchedulerLike | number>): MonoTypeOperatorFunction; /** @deprecated use static {@link merge} */ export function merge(...observables: Array | SchedulerLike | number>): OperatorFunction; -/* tslint:enable:max-line-length */ /** * @deprecated use {@link mergeWith} or static {@link merge} */ -export function merge(...observables: Array | SchedulerLike | number | undefined>): OperatorFunction { - return (source: Observable) => stankyLift( - source, - mergeStatic(source, ...(observables as any[])) - ); +export function merge(...args: Array | SchedulerLike | number | undefined>): OperatorFunction { + let concurrent = Infinity; + let scheduler: SchedulerLike | undefined = undefined; + + if (isScheduler(args[args.length - 1])) { + scheduler = args.pop() as SchedulerLike; + } + + if (typeof args[args.length - 1] === 'number') { + concurrent = args.pop() as number; + } + + args = argsOrArgArray(args); + + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + mergeAll(concurrent)(fromArray([source, ...(args as ObservableInput[])], scheduler)).subscribe(this); + }); } export function mergeWith(): OperatorFunction; -export function mergeWith[]>(...otherSources: A): OperatorFunction)>; +export function mergeWith[]>(...otherSources: A): OperatorFunction>; /** * Merge the values from all observables to an single observable result. @@ -106,6 +184,8 @@ export function mergeWith[]>(...otherSources: * ``` * @param otherSources the sources to combine the current source with. */ -export function mergeWith[]>(...otherSources: A): OperatorFunction)> { +export function mergeWith[]>( + ...otherSources: A +): OperatorFunction> { return merge(...otherSources); -} \ No newline at end of file +} diff --git a/src/internal/operators/multicast.ts b/src/internal/operators/multicast.ts index 5134d425ef..45de8576d1 100644 --- a/src/internal/operators/multicast.ts +++ b/src/internal/operators/multicast.ts @@ -1,16 +1,23 @@ +/** @prettier */ 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 { OperatorFunction, UnaryFunction, ObservedValueOf, ObservableInput } from '../types'; -import { lift } from '../util/lift'; +import { hasLift, lift } from '../util/lift'; /* tslint:disable:max-line-length */ export function multicast(subject: Subject): UnaryFunction, ConnectableObservable>; -export function multicast>(subject: Subject, selector: (shared: Observable) => O): UnaryFunction, ConnectableObservable>>; +export function multicast>( + subject: Subject, + selector: (shared: Observable) => O +): UnaryFunction, ConnectableObservable>>; export function multicast(subjectFactory: (this: Observable) => Subject): UnaryFunction, ConnectableObservable>; -export function multicast>(SubjectFactory: (this: Observable) => Subject, selector: (shared: Observable) => O): OperatorFunction>; +export function multicast>( + SubjectFactory: (this: Observable) => Subject, + selector: (shared: Observable) => O +): OperatorFunction>; /* tslint:enable:max-line-length */ /** @@ -31,39 +38,34 @@ export function multicast>(SubjectFactory: (th * the underlying stream. * @name multicast */ -export function multicast(subjectOrSubjectFactory: Subject | (() => Subject), - selector?: (source: Observable) => Observable): OperatorFunction { +export function multicast( + subjectOrSubjectFactory: Subject | (() => Subject), + selector?: (source: Observable) => Observable +): OperatorFunction { return function multicastOperatorFunction(source: Observable): Observable { - let subjectFactory: () => Subject; - if (typeof subjectOrSubjectFactory === 'function') { - subjectFactory = <() => Subject>subjectOrSubjectFactory; - } else { - subjectFactory = function subjectFactory() { - return >subjectOrSubjectFactory; - }; - } + const subjectFactory = typeof subjectOrSubjectFactory === 'function' ? subjectOrSubjectFactory : () => subjectOrSubjectFactory; if (typeof selector === 'function') { - return lift(source, new MulticastOperator(subjectFactory, selector)); + return lift(source, function (this: Subscriber, source: Observable) { + const subject = subjectFactory(); + // Intentionally terse code: Subscribe to the result of the selector, + // then immediately connect the source through the subject, adding + // that to the resulting subscription. The act of subscribing with `this`, + // the primary destination subscriber, will automatically add the subcription + // to the result. + selector(subject).subscribe(this).add(source.subscribe(subject)); + }); } - const connectable: any = Object.create(source, connectableObservableDescriptor); + const connectable: any = new ConnectableObservable(source, subjectFactory); + // If we have lift, monkey patch that here. This is done so custom observable + // types will compose through multicast. Otherwise the resulting observable would + // simply be an instance of `ConnectableObservable`. + if (hasLift(source)) { + connectable.lift = source.lift; + } connectable.source = source; connectable.subjectFactory = subjectFactory; - - return > connectable; + 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/internal/operators/observeOn.ts b/src/internal/operators/observeOn.ts index 683f9590b9..1cf2304e58 100644 --- a/src/internal/operators/observeOn.ts +++ b/src/internal/operators/observeOn.ts @@ -1,15 +1,9 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; -import { observeNotification, COMPLETE_NOTIFICATION, nextNotification, errorNotification } from '../Notification'; -import { - MonoTypeOperatorFunction, - SchedulerAction, - SchedulerLike, - TeardownLogic, - ObservableNotification, -} from '../types'; +import { MonoTypeOperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * @@ -64,57 +58,16 @@ import { lift } from '../util/lift'; * but with provided scheduler. */ export function observeOn(scheduler: SchedulerLike, delay: number = 0): MonoTypeOperatorFunction { - return function observeOnOperatorFunction(source: Observable): Observable { - return lift(source, new ObserveOnOperator(scheduler, delay)); - }; -} - -class ObserveOnOperator implements Operator { - constructor(private scheduler: SchedulerLike, private delay: number = 0) {} - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay)); - } -} - -class ObserveOnSubscriber extends Subscriber { - /** @nocollapse */ - static dispatch(this: SchedulerAction, arg: ObserveOnMessage) { - const { notification, destination } = arg; - observeNotification(notification, destination); - this.unsubscribe(); - } - - constructor(destination: Subscriber, private scheduler: SchedulerLike, private delay: number = 0) { - super(destination); - } - - private scheduleMessage(notification: ObservableNotification): void { - const destination = this.destination as Subscriber; - destination.add( - this.scheduler.schedule(ObserveOnSubscriber.dispatch as any, this.delay, { - notification, - destination, - }) - ); - } - - protected _next(value: T): void { - this.scheduleMessage(nextNotification(value)); - } - - protected _error(error: any): void { - this.scheduleMessage(errorNotification(error)); - this.unsubscribe(); - } - - protected _complete(): void { - this.scheduleMessage(COMPLETE_NOTIFICATION); - this.unsubscribe(); - } -} - -interface ObserveOnMessage { - notification: ObservableNotification; - destination: Subscriber; + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => subscriber.add(scheduler.schedule(() => subscriber.next(value), delay)), + (err) => subscriber.add(scheduler.schedule(() => subscriber.error(err), delay)), + () => subscriber.add(scheduler.schedule(() => subscriber.complete(), delay)) + ) + ); + }); } diff --git a/src/internal/operators/pairwise.ts b/src/internal/operators/pairwise.ts index 1f0abeb1a2..6d5f1ecefd 100644 --- a/src/internal/operators/pairwise.ts +++ b/src/internal/operators/pairwise.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Groups pairs of consecutive emissions together and emits them as an array of @@ -47,41 +48,18 @@ import { lift } from '../util/lift'; * @name pairwise */ export function pairwise(): OperatorFunction { - return (source: Observable) => lift(source, new PairwiseOperator()); -} - -class PairwiseOperator implements Operator { - call(subscriber: Subscriber<[T, T]>, source: any): any { - return source.subscribe(new PairwiseSubscriber(subscriber)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class PairwiseSubscriber extends Subscriber { - private prev: T | undefined; - private hasPrev: boolean = false; - - constructor(destination: Subscriber<[T, T]>) { - super(destination); - } - - _next(value: T): void { - let pair: [T, T] | undefined; - - if (this.hasPrev) { - pair = [this.prev!, value]; - } else { - this.hasPrev = true; - } - - this.prev = value; - - if (pair) { - this.destination.next(pair); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber<[T, T]>, source: Observable) { + const subscriber = this; + let prev: T; + let hasPrev = false; + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + const p = prev; + prev = value; + hasPrev && subscriber.next([p, value]); + hasPrev = true; + }) + ); + }); } diff --git a/src/internal/operators/partition.ts b/src/internal/operators/partition.ts index 71c677c68f..7727a231fe 100644 --- a/src/internal/operators/partition.ts +++ b/src/internal/operators/partition.ts @@ -54,6 +54,6 @@ export function partition(predicate: (value: T, index: number) => boolean, thisArg?: any): UnaryFunction, [Observable, Observable]> { return (source: Observable) => [ filter(predicate, thisArg)(source), - filter(not(predicate, thisArg) as any)(source) + filter(not(predicate, thisArg))(source) ] as [Observable, Observable]; } diff --git a/src/internal/operators/raceWith.ts b/src/internal/operators/raceWith.ts index a5b48c9f72..b8f4c1b7ec 100644 --- a/src/internal/operators/raceWith.ts +++ b/src/internal/operators/raceWith.ts @@ -1,8 +1,9 @@ import { Observable } from '../Observable'; import { MonoTypeOperatorFunction, OperatorFunction, ObservableInput, ObservedValueUnionFromArray } from '../types'; -import { race as raceStatic } from '../observable/race'; -import { stankyLift } from '../util/lift'; +import { raceInit } from '../observable/race'; +import { lift } from '../util/lift'; import { argsOrArgArray } from "../util/argsOrArgArray"; +import { Subscriber } from '../Subscriber'; /* tslint:disable:max-line-length */ /** @deprecated Deprecated use {@link raceWith} */ @@ -22,7 +23,7 @@ export function race(...observables: Array | Array(...args: any[]): OperatorFunction { +export function race(...args: any[]): OperatorFunction { return raceWith(...argsOrArgArray(args)); } @@ -57,14 +58,7 @@ export function race(...args: any[]): OperatorFunction { export function raceWith[]>( ...otherSources: A ): OperatorFunction> { - return function raceWithOperatorFunction(source: Observable) { - if (otherSources.length === 0) { - return source; - } - - return stankyLift( - source, - raceStatic(source, ...otherSources) - ); - }; + return (source: Observable) => (!otherSources.length) ? source : lift(source, function(this: Subscriber, source: Observable) { + return raceInit([source, ...otherSources])(this); + }); } diff --git a/src/internal/operators/reduce.ts b/src/internal/operators/reduce.ts index 3d584fc99b..808f5abbf7 100644 --- a/src/internal/operators/reduce.ts +++ b/src/internal/operators/reduce.ts @@ -78,7 +78,7 @@ export function reduce(accumulator: (acc: V | A, value: V, index: number) } return function reduceOperatorFunction(source: Observable): Observable { return pipe( - scan((acc, value, index) => accumulator(acc, value, index + 1)), + scan((acc, value, index) => accumulator(acc, value, index)), takeLast(1), )(source); }; diff --git a/src/internal/operators/refCount.ts b/src/internal/operators/refCount.ts index 2d0795333b..843a064f21 100644 --- a/src/internal/operators/refCount.ts +++ b/src/internal/operators/refCount.ts @@ -1,11 +1,10 @@ /** @prettier */ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { ConnectableObservable } from '../observable/ConnectableObservable'; -import { Observable } from '../Observable'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Make a {@link ConnectableObservable} behave like a ordinary observable and automates the way @@ -61,87 +60,61 @@ import { lift } from '../util/lift'; * @see {@link publish} */ export function refCount(): MonoTypeOperatorFunction { - return function refCountOperatorFunction(source: ConnectableObservable): Observable { - return lift(source, new RefCountOperator()); - } as MonoTypeOperatorFunction; -} - -class RefCountOperator implements Operator { - call(subscriber: Subscriber, connectable: ConnectableObservable): TeardownLogic { - (connectable)._refCount++; - - const refCounter = new RefCountSubscriber(subscriber, connectable); - const subscription = connectable.subscribe(refCounter); - - if (!refCounter.closed) { - (refCounter).connection = connectable.connect(); - } + return ((source: ConnectableObservable) => + lift(source, function (this: Subscriber, source: ConnectableObservable) { + const subscriber = this; + let connection: Subscription | null = null; - return subscription; - } -} + (source as any)._refCount++; -class RefCountSubscriber extends Subscriber { - private connection: Subscription | null = null; + const refCounter = new OperatorSubscriber(subscriber, undefined, undefined, undefined, () => { + if (!source || (source as any)._refCount <= 0 || 0 < --(source as any)._refCount) { + connection = null; + return; + } - constructor(destination: Subscriber, private connectable: ConnectableObservable) { - super(destination); - } + /// + // 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: + // ``` + // range(0, 10).pipe( + // 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 + /// - unsubscribe() { - if (!this.closed) { - const { connectable } = this; - if (!connectable) { - this.connection = null; - return; - } + const sharedConnection = (source)._connection; + const conn = connection; + connection = null; - this.connectable = null!; - const refCount = (connectable as any)._refCount; - if (refCount <= 0) { - this.connection = null; - return; - } + if (sharedConnection && (!conn || sharedConnection === conn)) { + sharedConnection.unsubscribe(); + } - (connectable as any)._refCount = refCount - 1; - if (refCount > 1) { - this.connection = null; - return; - } + subscriber.unsubscribe(); + }); - /// - // 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: - // ``` - // range(0, 10).pipe( - // 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; + const subscription = source.subscribe(refCounter); - if (sharedConnection && (!connection || sharedConnection === connection)) { - sharedConnection.unsubscribe(); + if (!refCounter.closed) { + connection = source.connect(); } - super.unsubscribe(); - } - } + return subscription; + })) as MonoTypeOperatorFunction; } diff --git a/src/internal/operators/repeat.ts b/src/internal/operators/repeat.ts index 56592aa86c..79a1facd60 100644 --- a/src/internal/operators/repeat.ts +++ b/src/internal/operators/repeat.ts @@ -2,7 +2,6 @@ import { Observable } from '../Observable'; import { Subscription } from '../Subscription'; import { EMPTY } from '../observable/empty'; -import { SimpleOuterSubscriber } from '../innerSubscribe'; import { lift } from '../util/lift'; import { Subscriber } from '../Subscriber'; import { MonoTypeOperatorFunction } from '../types'; diff --git a/src/internal/operators/repeatWhen.ts b/src/internal/operators/repeatWhen.ts index 2820cc88f3..49ba25a83e 100644 --- a/src/internal/operators/repeatWhen.ts +++ b/src/internal/operators/repeatWhen.ts @@ -1,12 +1,12 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; import { Subscription } from '../Subscription'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that mirrors the source Observable with the exception of a `complete`. If the source @@ -38,96 +38,87 @@ import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '.. * @name repeatWhen */ export function repeatWhen(notifier: (notifications: Observable) => Observable): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { - const subscriber = this; - const subscription = new Subscription(); - let innerSub: Subscription | null; - let syncResub = false; - let completions$: Subject; - let isNotifierComplete = false; - let isMainComplete = false; + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let innerSub: Subscription | null; + let syncResub = false; + let completions$: Subject; + let isNotifierComplete = false; + let isMainComplete = false; - /** - * Gets the subject to send errors through. If it doesn't exist, - * we know we need to setup the notifier. - */ - const getCompletionSubject = () => { - if (!completions$) { - completions$ = new Subject(); - let notifier$: Observable; - // The notifier is a user-provided function, so we need to do - // some error handling. - try { - notifier$ = notifier(completions$); - } catch (err) { - subscriber.error(err); - // Returning null here will cause the code below to - // notice there's been a problem and skip error notification. - return null; - } - subscription.add( - notifier$.subscribe({ - next: () => { - if (innerSub) { - subscribeForRepeatWhen(); - } else { - // If we don't have an innerSub yet, that's because the inner subscription - // call hasn't even returned yet. We've arrived here synchronously. - // So we flag that we want to resub, such that we can ensure teardown - // happens before we resubscribe. - syncResub = true; - } - }, - error: (err) => subscriber.error(err), - complete: () => { - isNotifierComplete = true; - if (isMainComplete) { - subscriber.complete(); + /** + * Checks to see if we can complete the result, completes it, and returns `true` if it was completed. + */ + const checkComplete = () => isMainComplete && isNotifierComplete && (subscriber.complete(), true); + /** + * Gets the subject to send errors through. If it doesn't exist, + * we know we need to setup the notifier. + */ + const getCompletionSubject = () => { + if (!completions$) { + completions$ = new Subject(); + + // If the call to `notifier` throws, it will be caught by the OperatorSubscriber + // In the main subscription -- in `subscribeForRepeatWhen`. + notifier(completions$).subscribe( + new OperatorSubscriber( + subscriber, + () => { + if (innerSub) { + subscribeForRepeatWhen(); + } else { + // If we don't have an innerSub yet, that's because the inner subscription + // call hasn't even returned yet. We've arrived here synchronously. + // So we flag that we want to resub, such that we can ensure teardown + // happens before we resubscribe. + syncResub = true; + } + }, + undefined, + () => { + isNotifierComplete = true; + checkComplete(); } - }, + ) + ); + } + return completions$; + }; + + const subscribeForRepeatWhen = () => { + isMainComplete = false; + + innerSub = source.subscribe( + new OperatorSubscriber(subscriber, undefined, undefined, () => { + isMainComplete = true; + // Check to see if we are complete, and complete if so. + // If we are not complete. Get the subject. This calls the `notifier` function. + // If that function fails, it will throw and `.next()` will not be reached on this + // line. The thrown error is caught by the _complete handler in this + // `OperatorSubscriber` and handled appropriately. + !checkComplete() && getCompletionSubject().next(); }) ); - } - return completions$; - }; - const subscribeForRepeatWhen = () => { - isMainComplete = false; - innerSub = source.subscribe({ - next: (value) => subscriber.next(value), - error: (err) => subscriber.error(err), - complete: () => { - isMainComplete = true; - if (isNotifierComplete) { - subscriber.complete(); - } else { - const completions$ = getCompletionSubject(); - if (completions$) { - // We have set up the notifier without error. - completions$.next(); - } - } - }, - }); - if (syncResub) { - // Ensure that the inner subscription is torn down before - // moving on to the next subscription in the synchronous case. - // If we don't do this here, all inner subscriptions will not be - // torn down until the entire observable is done. - innerSub.unsubscribe(); - innerSub = null; - // We may need to do this multiple times, so reset the flags. - syncResub = false; - // Resubscribe - subscribeForRepeatWhen(); - } else { - subscription.add(innerSub); - } - }; - - // Start the subscription - subscribeForRepeatWhen(); + if (syncResub) { + // Ensure that the inner subscription is torn down before + // moving on to the next subscription in the synchronous case. + // If we don't do this here, all inner subscriptions will not be + // torn down until the entire observable is done. + innerSub.unsubscribe(); + // It is important to null this out. Not only to free up memory, but + // to make sure code above knows we are in a subscribing state to + // handle synchronous resubscription. + innerSub = null; + // We may need to do this multiple times, so reset the flags. + syncResub = false; + // Resubscribe + subscribeForRepeatWhen(); + } + }; - return subscription; - }); + // Start the subscription + subscribeForRepeatWhen(); + }); } diff --git a/src/internal/operators/retryWhen.ts b/src/internal/operators/retryWhen.ts index 7695398a5d..77916eb691 100644 --- a/src/internal/operators/retryWhen.ts +++ b/src/internal/operators/retryWhen.ts @@ -5,7 +5,8 @@ import { Subject } from '../Subject'; import { Subscription } from '../Subscription'; import { MonoTypeOperatorFunction } from '../types'; -import { lift } from '../util/lift'; +import { lift, wrappedLift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that mirrors the source Observable with the exception of an `error`. If the source Observable @@ -61,64 +62,34 @@ import { lift } from '../util/lift'; */ export function retryWhen(notifier: (errors: Observable) => Observable): MonoTypeOperatorFunction { return (source: Observable) => - lift(source, function (this: Subscriber, source: Observable) { - const subscriber = this; - const subscription = new Subscription(); + wrappedLift(source, (subscriber: Subscriber, source: Observable) => { let innerSub: Subscription | null; let syncResub = false; let errors$: Subject; - /** - * Gets the subject to send errors through. If it doesn't exist, - * we know we need to setup the notifier. - */ - const getErrorSubject = () => { - if (!errors$) { - errors$ = new Subject(); - let notifier$: Observable; - // The notifier is a user-provided function, so we need to do - // some error handling. - try { - notifier$ = notifier(errors$); - } catch (err) { - subscriber.error(err); - // Returning null here will cause the code below to - // notice there's been a problem and skip error notification. - return null; - } - subscription.add( - notifier$.subscribe({ - next: () => { - if (innerSub) { - subscribeForRetryWhen(); - } else { - // If we don't have an innerSub yet, that's because the inner subscription + const subscribeForRetryWhen = () => { + innerSub = source.subscribe( + new OperatorSubscriber(subscriber, undefined, (err) => { + if (!errors$) { + errors$ = new Subject(); + notifier(errors$).subscribe( + new OperatorSubscriber(subscriber, () => + // If we have an innerSub, this was an asynchronous call, kick off the retry. + // Otherwise, if we don't have an innerSub yet, that's because the inner subscription // call hasn't even returned yet. We've arrived here synchronously. // So we flag that we want to resub, such that we can ensure teardown // happens before we resubscribe. - syncResub = true; - } - }, - error: (err) => subscriber.error(err), - complete: () => subscriber.complete(), - }) - ); - } - return errors$; - }; - - const subscribeForRetryWhen = () => { - innerSub = source.subscribe({ - next: (value) => subscriber.next(value), - error: (err) => { - const errors$ = getErrorSubject(); + innerSub ? subscribeForRetryWhen() : (syncResub = true) + ) + ); + } if (errors$) { // We have set up the notifier without error. errors$.next(err); } - }, - complete: () => subscriber.complete(), - }); + }) + ); + if (syncResub) { // Ensure that the inner subscription is torn down before // moving on to the next subscription in the synchronous case. @@ -130,14 +101,10 @@ export function retryWhen(notifier: (errors: Observable) => Observable(notifier: Observable): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SampleOperator(notifier)); -} - -class SampleOperator implements Operator { - constructor(private notifier: Observable) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - const sampleSubscriber = new SampleSubscriber(subscriber); - const subscription = source.subscribe(sampleSubscriber); - subscription.add(innerSubscribe(this.notifier, new SimpleInnerSubscriber(sampleSubscriber))); - return subscription; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SampleSubscriber extends SimpleOuterSubscriber { - private value: T | undefined; - private hasValue: boolean = false; - - protected _next(value: T) { - this.value = value; - this.hasValue = true; - } - - notifyNext(): void { - this.emitValue(); - } - - notifyComplete(): void { - this.emitValue(); - } - - emitValue() { - if (this.hasValue) { - this.hasValue = false; - this.destination.next(this.value); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let lastValue: T | null = null; + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + hasValue = true; + lastValue = value; + }) + ); + const emit = () => { + if (hasValue) { + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + } + }; + notifier.subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit)); + }); } diff --git a/src/internal/operators/sampleTime.ts b/src/internal/operators/sampleTime.ts index b805d1a1b1..d6ea72e6fb 100644 --- a/src/internal/operators/sampleTime.ts +++ b/src/internal/operators/sampleTime.ts @@ -1,9 +1,11 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; -import { async } from '../scheduler/async'; -import { MonoTypeOperatorFunction, SchedulerAction, SchedulerLike, TeardownLogic } from '../types'; +import { asyncScheduler } from '../scheduler/async'; +import { MonoTypeOperatorFunction, SchedulerAction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { sample } from './sample'; +import { interval } from '../observable/interval'; /** * Emits the most recently emitted value from the source Observable within @@ -45,52 +47,8 @@ import { lift } from '../util/lift'; * @return {Observable} An Observable that emits the results of sampling the * values emitted by the source Observable at the specified time interval. * @name sampleTime + * @deprecated To be removed in v8. Use `sample(interval(period, scheduler?))`, it's the same thing. */ -export function sampleTime(period: number, scheduler: SchedulerLike = async): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SampleTimeOperator(period, scheduler)); -} - -class SampleTimeOperator implements Operator { - constructor(private period: number, - private scheduler: SchedulerLike) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SampleTimeSubscriber(subscriber, this.period, this.scheduler)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SampleTimeSubscriber extends Subscriber { - lastValue: T | undefined; - hasValue: boolean = false; - - constructor(destination: Subscriber, - private period: number, - private scheduler: SchedulerLike) { - super(destination); - this.add(scheduler.schedule(dispatchNotification, period, { subscriber: this, period })); - } - - protected _next(value: T) { - this.lastValue = value; - this.hasValue = true; - } - - notifyNext() { - if (this.hasValue) { - this.hasValue = false; - this.destination.next(this.lastValue); - } - } -} - -function dispatchNotification(this: SchedulerAction, state: any) { - let { subscriber, period } = state; - subscriber.notifyNext(); - this.schedule(state, period); +export function sampleTime(period: number, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction { + return sample(interval(period, scheduler)); } diff --git a/src/internal/operators/scan.ts b/src/internal/operators/scan.ts index 2916be207e..2ff64a015d 100644 --- a/src/internal/operators/scan.ts +++ b/src/internal/operators/scan.ts @@ -1,111 +1,131 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { OperatorFunction, TeardownLogic } from '../types'; +import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; -/* tslint:disable:max-line-length */ -export function scan(accumulator: (acc: A|V, value: V, index: number) => A): OperatorFunction; +export function scan(accumulator: (acc: A | V, value: V, index: number) => A): OperatorFunction; export function scan(accumulator: (acc: A, value: V, index: number) => A, seed: A): OperatorFunction; -export function scan(accumulator: (acc: A|S, value: V, index: number) => A, seed: S): OperatorFunction; -/* tslint:enable:max-line-length */ +export function scan(accumulator: (acc: A | S, value: V, index: number) => A, seed: S): OperatorFunction; + +// TODO: link to a "redux pattern" section in the guide (location TBD) /** - * Applies an accumulator function over the source Observable, and returns each - * intermediate result, with an optional seed value. + * Useful for encapsulating and managing state. Applies an accumulator (or "reducer function") + * to each value from the source after an initial state is established -- either via + * a `seed` value (second argument), or from the first value from the source. * * It's like {@link reduce}, but emits the current - * accumulation whenever the source emits a value. + * accumulation state after each update * * ![](scan.png) * - * 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. + * This operator maintains an internal state and emits it after processing each value as follows: + * + * 1. First value arrives + * - If a `seed` value was supplied (as the second argument to `scan`), let `state = seed` and `value = firstValue`. + * - If NO `seed` value was supplied (no second argument), let `state = firstValue` and go to 3. + * 2. Let `state = accumulator(state, value)`. + * - If an error is thrown by `accumulator`, notify the consumer of an error. The process ends. + * 3. Emit `state`. + * 4. Next value arrives, let `value = nextValue`, go to 2. + * + * ## Example + * + * An average of previous numbers. This example shows how + * not providing a `seed` can prime the stream with the + * first value from the source. + * + * ```ts + * import { interval } from 'rxjs'; + * import { scan, map } from 'rxjs/operators'; * - * 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. + * numbers$ + * .pipe( + * // Get the sum of the numbers coming in. + * scan((total, n) => total + n), + * // Get the average by dividing the sum by the total number + * // received so var (which is 1 more than the zero-based index). + * map((sum, index) => sum / (index + 1)) + * ) + * .subscribe(console.log); + * ``` * * ## Example - * Count the number of click events + * + * The Fibonacci sequence. This example shows how you can use + * a seed to prime accumulation process. Also... you know... Fibinacci. + * So important to like, computers and stuff that its whiteboarded + * in job interviews. Now you can show them the Rx version! (Please don't, haha) + * * ```ts - * import { fromEvent } from 'rxjs'; - * import { scan, mapTo } from 'rxjs/operators'; - * - * const clicks = fromEvent(document, 'click'); - * const ones = clicks.pipe(mapTo(1)); - * const seed = 0; - * const count = ones.pipe(scan((acc, one) => acc + one, seed)); - * count.subscribe(x => console.log(x)); + * import { interval } from 'rxjs'; + * import { scan, map, startWith } from 'rxjs/operators'; + * + * const firstTwoFibs = [0, 1]; + * // An endless stream of Fibonnaci numbers. + * const fibonnaci$ = interval(1000).pipe( + * // Scan to get the fibonnaci numbers (after 0, 1) + * scan(([a, b]) => [b, a + b], firstTwoFibs), + * // Get the second number in the tuple, it's the one you calculated + * map(([, n]) => n), + * // Start with our first two digits :) + * startWith(...firstTwoFibs) + * ); + * + * fibonnaci$.subscribe(console.log); * ``` * + * * @see {@link expand} * @see {@link mergeScan} * @see {@link reduce} * - * @param {function(acc: A, value: V, index: number): A} accumulator - * The accumulator function called on each source value. - * @param {V|A} [seed] The initial accumulation value. - * @return {Observable} An observable of the accumulated values. - * @name scan + * @param accumulator A "reducer function". This will be called for each value after an initial state is + * acquired. + * @param seed The initial state. If this is not provided, the first value from the source will + * be used as the initial state, and emitted without going through the accumulator. All subsequent values + * will be processed by the accumulator function. If this is provided, all values will go through + * the accumulator function. */ -export function scan(accumulator: (acc: V|A|S, value: V, index: number) => A, seed?: S): OperatorFunction { - let hasSeed = false; +export function scan(accumulator: (acc: V | A | S, value: V, index: number) => A, seed?: S): 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) { - hasSeed = true; - } - - return function scanOperatorFunction(source: Observable) { - return lift(source, new ScanOperator(accumulator, seed, hasSeed)); - }; -} - -class ScanOperator implements Operator { - constructor(private accumulator: (acc: V|A|S, value: V, index: number) => A, private seed?: S, private hasSeed: boolean = false) {} + const hasSeed = arguments.length >= 2; - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); - } -} + return (source: Observable) => { + return lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // Whether or not we have state yet. This will only be + // false before the first value arrives if we didn't get + // a seed value. + let hasState = hasSeed; + // The state that we're tracking, starting with the seed, + // if there is one, and then updated by the return value + // from the accumulator on each emission. + let state: any = seed; + // An index to pass to the accumulator function. + let index = 0; -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ScanSubscriber extends Subscriber { - private index: number = 0; - - constructor(destination: Subscriber, private accumulator: (acc: V|A, value: V, index: number) => A, private _state: any, - private _hasState: boolean) { - super(destination); - } - - protected _next(value: V): void { - const { destination } = this; - if (!this._hasState) { - this._state = value; - this._hasState = true; - destination.next(value); - } else { - const index = this.index++; - let result: A; - try { - result = this.accumulator(this._state, value, index); - } catch (err) { - destination.error(err); - return; - } - this._state = result; - destination.next(result); - } - } + // Subscribe to our source. All errors and completions are passed through. + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + const i = index++; + // Set the state and send it to the consumer. + subscriber.next( + (state = hasState + ? // We already have state, so we can get the new state from the accumulator + accumulator(state, value, i) + : // We didn't have state yet, a seed value was not provided, so + // we set the state to the first value, and mark that we have state now + ((hasState = true), value)) + ); + }) + ); + }); + }; } diff --git a/src/internal/operators/sequenceEqual.ts b/src/internal/operators/sequenceEqual.ts index 75427f93df..fed3521b9e 100644 --- a/src/internal/operators/sequenceEqual.ts +++ b/src/internal/operators/sequenceEqual.ts @@ -4,6 +4,7 @@ import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Compares all values of two observables in sequence using an optional comparator function @@ -85,55 +86,35 @@ export function sequenceEqual( * is used for both streams. */ const createSubscriber = (selfState: SequenceState, otherState: SequenceState) => { - const sequenceEqualSubscriber = new SequenceEqualSubscriber( + const sequenceEqualSubscriber = new OperatorSubscriber( subscriber, (a: T) => { const { buffer, complete } = otherState; if (buffer.length === 0) { - // If there's no values in the other buffer... - if (complete) { - // ... and the other stream is complete, we know - // this isn't a match, because we got one more value. - emit(false); - } else { - // Otherwise, we push onto our buffer, so when the other - // stream emits, it can pull this value off our buffer and check it - // at the appropriate time. - selfState.buffer.push(a); - } + // If there's no values in the other buffer + // and the other stream is complete, we know + // this isn't a match, because we got one more value. + // Otherwise, we push onto our buffer, so when the other + // stream emits, it can pull this value off our buffer and check it + // at the appropriate time. + complete ? emit(false) : selfState.buffer.push(a); } else { // If the other stream *does* have values in it's buffer, // pull the oldest one off so we can compare it to what we - // just got. - const b = buffer.shift()!; - - // Call the comparator. It's a user function, so we have to - // capture the error appropriately. - let result: boolean; - try { - result = comparator(a, b); - } catch (err) { - subscriber.error(err); - return; - } - - if (!result) { - // If it wasn't a match, emit `false` and complete. - emit(false); - } + // just got. If it wasn't a match, emit `false` and complete. + !comparator(a, buffer.shift()!) && emit(false); } }, + undefined, () => { // Or observable completed selfState.complete = true; const { complete, buffer } = otherState; - if (complete) { - // If the other observable is also complete, and there's - // still stuff left in their buffer, it doesn't match, if their - // buffer is empty, then it does match. This is because we can't - // possibly get more values here anymore. - emit(buffer.length === 0); - } + // If the other observable is also complete, and there's + // still stuff left in their buffer, it doesn't match, if their + // buffer is empty, then it does match. This is because we can't + // possibly get more values here anymore. + complete && emit(buffer.length === 0); // Be sure to clean up our stream as soon as possible if we can. sequenceEqualSubscriber?.unsubscribe(); } @@ -168,10 +149,3 @@ function createState(): SequenceState { complete: false, }; } - -// TODO: Combine with other implementations that are identical. -class SequenceEqualSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void, protected _complete: () => void) { - super(destination); - } -} diff --git a/src/internal/operators/single.ts b/src/internal/operators/single.ts index c918640fc6..c643903430 100644 --- a/src/internal/operators/single.ts +++ b/src/internal/operators/single.ts @@ -6,8 +6,7 @@ import { MonoTypeOperatorFunction } from '../types'; import { SequenceError } from '../util/SequenceError'; import { NotFoundError } from '../util/NotFoundError'; import { lift } from '../util/lift'; - -const defaultPredicate = () => true; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an observable that asserts that only one value is @@ -90,47 +89,28 @@ const defaultPredicate = () => true; * the predicate or `undefined` when no items match. */ export function single( - predicate: (value: T, index: number, source: Observable) => boolean = defaultPredicate + predicate?: (value: T, index: number, source: Observable) => boolean ): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, singleOperator(predicate)); -} - -function singleOperator(predicate: (value: T, index: number, source: Observable) => boolean) { - return function(this: Subscriber, source: Observable) { - let _hasValue = false; - let _seenValue = false; - let _value: T; - let _i = 0; - const _destination = this; - - return source.subscribe({ - next: value => { - _seenValue = true; - let match = false; - try { - match = predicate(value, _i++, source); - } catch (err) { - _destination.error(err); - return; - } - if (match) { - if (_hasValue) { - _destination.error(new SequenceError('Too many matching values')); - } else { - _hasValue = true; - _value = value; - } - } - }, - error: err => _destination.error(err), - complete: () => { - if (_hasValue) { - _destination.next(_value); - _destination.complete(); - } else { - _destination.error(_seenValue ? new NotFoundError('No matching values') : new EmptyError()); - } - }, - }); - }; + return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let singleValue: T; + let seenValue = false; + let index = 0; + source.subscribe(new OperatorSubscriber(subscriber, value => { + seenValue = true; + if (!predicate || predicate(value, index++, source)) { + hasValue && subscriber.error(new SequenceError('Too many matching values')); + hasValue = true; + singleValue = value; + } + }, undefined, () => { + if (hasValue) { + subscriber.next(singleValue); + subscriber.complete(); + } else { + subscriber.error(seenValue ? new NotFoundError('No matching values') : new EmptyError()) + } + })) + }); } diff --git a/src/internal/operators/skip.ts b/src/internal/operators/skip.ts index 96827f86fa..88c48011c7 100644 --- a/src/internal/operators/skip.ts +++ b/src/internal/operators/skip.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that skips the first `count` items emitted by the source Observable. @@ -14,33 +15,14 @@ import { lift } from '../util/lift'; * @name skip */ export function skip(count: number): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SkipOperator(count)); -} - -class SkipOperator implements Operator { - constructor(private total: number) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SkipSubscriber(subscriber, this.total)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SkipSubscriber extends Subscriber { - count: number = 0; - - constructor(destination: Subscriber, private total: number) { - super(destination); - } - - protected _next(x: T) { - if (++this.count > this.total) { - this.destination.next(x); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let seen = 0; + return source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + count === seen ? subscriber.next(value) : seen++; + }) + ); + }); } diff --git a/src/internal/operators/skipLast.ts b/src/internal/operators/skipLast.ts index 1c4e8dd1ff..f6a3e95b43 100644 --- a/src/internal/operators/skipLast.ts +++ b/src/internal/operators/skipLast.ts @@ -1,9 +1,8 @@ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; -import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Skip the last `count` values emitted by the source Observable. @@ -37,60 +36,37 @@ import { lift } from '../util/lift'; * @throws {ArgumentOutOfRangeError} When using `skipLast(i)`, it throws * ArgumentOutOrRangeError if `i < 0`. * - * @param {number} count Number of elements to skip from the end of the source Observable. + * @param {number} skipCount Number of elements to skip from the end of the source Observable. * @returns {Observable} An Observable that skips the last count values * emitted by the source Observable. * @name skipLast */ -export function skipLast(count: number): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SkipLastOperator(count)); -} - -class SkipLastOperator implements Operator { - constructor(private _skipCount: number) { - if (this._skipCount < 0) { - throw new ArgumentOutOfRangeError; - } - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - if (this._skipCount === 0) { - // If we don't want to skip any values then just subscribe - // to Subscriber without any further logic. - return source.subscribe(new Subscriber(subscriber)); - } else { - return source.subscribe(new SkipLastSubscriber(subscriber, this._skipCount)); - } - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SkipLastSubscriber extends Subscriber { - private _ring: T[]; - private _count: number = 0; - - constructor(destination: Subscriber, private _skipCount: number) { - super(destination); - this._ring = new Array(_skipCount); - } - - protected _next(value: T): void { - const skipCount = this._skipCount; - const count = this._count++; - - if (count < skipCount) { - this._ring[count] = value; - } else { - const currentIndex = count % skipCount; - const ring = this._ring; - const oldValue = ring[currentIndex]; - - ring[currentIndex] = value; - this.destination.next(oldValue); - } - } -} +export function skipLast(skipCount: number): MonoTypeOperatorFunction { + // For skipCounts less than or equal to zero, we are just mirroring the source. + return (source: Observable) => skipCount <= 0 ? source : lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + // A ring buffer to hold the values while we wait to see + // if we can emit it or it's part of the "skipped" last values. + // Note that it is the _same size_ as the skip count. + let ring: T[] = new Array(skipCount); + let count = 0; + source.subscribe(new OperatorSubscriber(subscriber, value => { + // Move us to the next slot in the ring buffer. + const currentCount = count++; + if (currentCount < skipCount) { + // Fill the ring first + ring[currentCount] = value; + } else { + const index = currentCount % skipCount; + // Pull the oldest value out and emit it, + // then stuff the new value in it's place. + const oldValue = ring[index]; + ring[index] = value; + subscriber.next(oldValue); + } + }, undefined, undefined, () => + // Free up memory + ring = null! + )) + }); +} \ No newline at end of file diff --git a/src/internal/operators/skipUntil.ts b/src/internal/operators/skipUntil.ts index a08c5814df..5ad08f0f99 100644 --- a/src/internal/operators/skipUntil.ts +++ b/src/internal/operators/skipUntil.ts @@ -1,10 +1,10 @@ -import { Operator } from '../Operator'; -import { Subscriber } from '../Subscriber'; +/** @prettier */ import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic, ObservableInput } from '../types'; -import { Subscription } from '../Subscription'; -import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { MonoTypeOperatorFunction } from '../types'; +import { wrappedLift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; +import { noop } from '../util/noop'; /** * Returns an Observable that skips items emitted by the source Observable until a second Observable emits an item. @@ -45,49 +45,22 @@ import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '.. * @name skipUntil */ export function skipUntil(notifier: Observable): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SkipUntilOperator(notifier)); -} - -class SkipUntilOperator implements Operator { - constructor(private notifier: Observable) { - } - - call(destination: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SkipUntilSubscriber(destination, this.notifier)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SkipUntilSubscriber extends SimpleOuterSubscriber { - private isTaking = false; - private innerSubscription: Subscription | undefined; - - constructor(destination: Subscriber, notifier: ObservableInput) { - super(destination); - const innerSubscriber = new SimpleInnerSubscriber(this); - this.add(innerSubscriber); - this.innerSubscription = innerSubscriber; - innerSubscribe(notifier, innerSubscriber); - } + return (source: Observable) => + wrappedLift(source, (subscriber, liftedSource) => { + let taking = false; - protected _next(value: T) { - if (this.isTaking) { - super._next(value); - } - } + const skipSubscriber = new OperatorSubscriber( + subscriber, + () => { + skipSubscriber?.unsubscribe(); + taking = true; + }, + undefined, + noop + ); - notifyNext(): void { - this.isTaking = true; - if (this.innerSubscription) { - this.innerSubscription.unsubscribe(); - } - } + from(notifier).subscribe(skipSubscriber); - notifyComplete() { - /* do nothing */ - } + liftedSource.subscribe(new OperatorSubscriber(subscriber, (value) => taking && subscriber.next(value))); + }); } diff --git a/src/internal/operators/skipWhile.ts b/src/internal/operators/skipWhile.ts index 2dcb4090bf..79abc8b77c 100644 --- a/src/internal/operators/skipWhile.ts +++ b/src/internal/operators/skipWhile.ts @@ -1,8 +1,9 @@ +/** @prettier */ import { Observable } from '../Observable'; -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that skips all items emitted by the source Observable as long as a specified condition holds @@ -16,49 +17,13 @@ import { lift } from '../util/lift'; * @name skipWhile */ export function skipWhile(predicate: (value: T, index: number) => boolean): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new SkipWhileOperator(predicate)); -} - -class SkipWhileOperator implements Operator { - constructor(private predicate: (value: T, index: number) => boolean) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new SkipWhileSubscriber(subscriber, this.predicate)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SkipWhileSubscriber extends Subscriber { - private skipping: boolean = true; - private index: number = 0; - - constructor(destination: Subscriber, - private predicate: (value: T, index: number) => boolean) { - super(destination); - } - - protected _next(value: T): void { - const destination = this.destination; - if (this.skipping) { - this.tryCallPredicate(value); - } - - if (!this.skipping) { - destination.next(value); - } - } - - private tryCallPredicate(value: T): void { - try { - const result = this.predicate(value, this.index++); - this.skipping = Boolean(result); - } catch (err) { - this.destination.error(err); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let taking = false; + let index = 0; + source.subscribe( + new OperatorSubscriber(subscriber, (value) => (taking || (taking = !predicate(value, index++))) && subscriber.next(value)) + ); + }); } diff --git a/src/internal/operators/subscribeOn.ts b/src/internal/operators/subscribeOn.ts index 730a0974b7..418688504d 100644 --- a/src/internal/operators/subscribeOn.ts +++ b/src/internal/operators/subscribeOn.ts @@ -1,51 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, SchedulerLike, TeardownLogic, SchedulerAction } from '../types'; -import { asap as asapScheduler } from '../scheduler/asap'; -import { Subscription } from '../Subscription'; -import { isScheduler } from '../util/isScheduler'; +import { MonoTypeOperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; -export interface DispatchArg { - source: Observable; - subscriber: Subscriber; -} - -class SubscribeOnObservable extends Observable { - /** @nocollapse */ - static dispatch(this: SchedulerAction, arg: DispatchArg) { - const { source, subscriber } = arg; - this.add(source.subscribe(subscriber)); - } - - constructor( - public source: Observable, - private delayTime: number = 0, - private scheduler: SchedulerLike = asapScheduler - ) { - super(); - if (delayTime < 0) { - this.delayTime = 0; - } - if (!isScheduler(scheduler)) { - this.scheduler = asapScheduler; - } - } - - /** @deprecated This is an internal implementation detail, do not use. */ - _subscribe(subscriber: Subscriber) { - const delay = this.delayTime; - const source = this.source; - const scheduler = this.scheduler; - - return scheduler.schedule>(SubscribeOnObservable.dispatch as any, delay, { - source, - subscriber, - }); - } -} - /** * Asynchronously subscribes Observers to this Observable on the specified {@link SchedulerLike}. * @@ -106,14 +64,9 @@ class SubscribeOnObservable extends Observable { * @return The source Observable modified so that its subscriptions happen on the specified {@link SchedulerLike}. */ export function subscribeOn(scheduler: SchedulerLike, delay: number = 0): MonoTypeOperatorFunction { - return function subscribeOnOperatorFunction(source: Observable): Observable { - return lift(source, new SubscribeOnOperator(scheduler, delay)); - }; -} - -class SubscribeOnOperator implements Operator { - constructor(private scheduler: SchedulerLike, private delay: number) {} - call(subscriber: Subscriber, source: any): TeardownLogic { - return new SubscribeOnObservable(source, this.delay, this.scheduler).subscribe(subscriber); - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + subscriber.add(scheduler.schedule(() => source.subscribe(subscriber), delay)); + }); } diff --git a/src/internal/operators/switchMap.ts b/src/internal/operators/switchMap.ts index 7beec5afa7..8730ceb12e 100644 --- a/src/internal/operators/switchMap.ts +++ b/src/internal/operators/switchMap.ts @@ -1,19 +1,25 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { Subscription } from '../Subscription'; import { ObservableInput, OperatorFunction, ObservedValueOf } from '../types'; -import { map } from './map'; import { from } from '../observable/from'; import { lift } from '../util/lift'; -import { SimpleInnerSubscriber, innerSubscribe, SimpleOuterSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ -export function switchMap>(project: (value: T, index: number) => O): OperatorFunction>; +export function switchMap>( + project: (value: T, index: number) => O +): OperatorFunction>; /** @deprecated resultSelector is no longer supported, use inner map instead */ -export function switchMap>(project: (value: T, index: number) => O, resultSelector: undefined): OperatorFunction>; +export function switchMap>( + project: (value: T, index: number) => O, + resultSelector: undefined +): OperatorFunction>; /** @deprecated resultSelector is no longer supported, use inner map instead */ -export function switchMap>(project: (value: T, index: number) => O, resultSelector: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R): OperatorFunction; +export function switchMap>( + project: (value: T, index: number) => O, + resultSelector: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R +): OperatorFunction; /* tslint:enable:max-line-length */ /** @@ -79,77 +85,53 @@ export function switchMap>(project: (value: */ export function switchMap>( project: (value: T, index: number) => O, - resultSelector?: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R, -): OperatorFunction|R> { - if (typeof resultSelector === 'function') { - return (source: Observable) => source.pipe( - switchMap((a, i) => from(project(a, i)).pipe( - map((b, ii) => resultSelector(a, b, i, ii)) - )) - ); - } - return (source: Observable) => lift(source, new SwitchMapOperator(project)); -} - -class SwitchMapOperator implements Operator { - constructor(private project: (value: T, index: number) => ObservableInput) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new SwitchMapSubscriber(subscriber, this.project)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class SwitchMapSubscriber extends SimpleOuterSubscriber { - private index: number = 0; - private innerSubscription?: Subscription; - - constructor(protected destination: Subscriber, - private project: (value: T, index: number) => ObservableInput) { - 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; - } - const innerSubscription = this.innerSubscription; - if (innerSubscription) { - innerSubscription.unsubscribe(); - } - const innerSubscriber = new SimpleInnerSubscriber(this); - this.destination.add(innerSubscriber); - this.innerSubscription = innerSubscriber; - innerSubscribe(result, innerSubscriber); - } - - protected _complete(): void { - const {innerSubscription} = this; - if (!innerSubscription || innerSubscription.closed) { - super._complete(); - } - this.innerSubscription = undefined; - this.unsubscribe(); - } + resultSelector?: (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R +): OperatorFunction | R> { + return (source: Observable) => + lift(source, function (this: Subscriber | R>, source: Observable) { + const subscriber = this; + let innerSubscriber: Subscriber> | null = null; + let index = 0; + // Whether or not the source subscription has completed + let isComplete = false; - notifyComplete(): void { - this.innerSubscription = undefined; - if (this.isStopped) { - super._complete(); - } - } + // We only complete the result if the source is complete AND we don't have an active inner subscription. + // This is called both when the source completes and when the inners complete. + const checkComplete = () => isComplete && !innerSubscriber && subscriber.complete(); - notifyNext(innerValue: R): void { - this.destination.next(innerValue); - } + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + // Cancel the previous inner subscription if there was one + innerSubscriber?.unsubscribe(); + let innerIndex = 0; + let outerIndex = index++; + // Start the next inner subscription + from(project(value, outerIndex)).subscribe( + (innerSubscriber = new OperatorSubscriber( + subscriber, + // When we get a new inner value, next it through. Note that this is + // handling the deprecate result selector here. This is because with this architecture + // it ends up being smaller than using the map operator. + (innerValue) => subscriber.next(resultSelector ? resultSelector(value, innerValue, outerIndex, innerIndex++) : innerValue), + undefined, + () => { + // The inner has completed. Null out the inner subcriber to + // free up memory and to signal that we have no inner subscription + // currently. + innerSubscriber = null!; + checkComplete(); + } + )) + ); + }, + undefined, + () => { + isComplete = true; + checkComplete(); + } + ) + ); + }); } diff --git a/src/internal/operators/take.ts b/src/internal/operators/take.ts index 27e250e578..5689cbd1ae 100644 --- a/src/internal/operators/take.ts +++ b/src/internal/operators/take.ts @@ -1,10 +1,10 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; -import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { EMPTY } from '../observable/empty'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits only the first `count` values emitted by the source Observable. @@ -51,41 +51,22 @@ import { lift } from '../util/lift'; * if the source emits fewer than `count` values. */ export function take(count: number): MonoTypeOperatorFunction { - if (isNaN(count)) { - throw new TypeError(`'count' is not a number`); - } - if (count < 0) { - throw new ArgumentOutOfRangeError; - } - - return (source: Observable) => (count === 0) ? EMPTY : lift(source, new TakeOperator(count)); -} - -class TakeOperator implements Operator { - constructor(private count: number) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new TakeSubscriber(subscriber, this.count)); - } -} - -class TakeSubscriber extends Subscriber { - private _valueCount: number = 0; - - constructor(destination: Subscriber, private count: number) { - super(destination); - } - - protected _next(value: T): void { - const total = this.count; - const count = ++this._valueCount; - if (count <= total) { - this.destination.next(value); - if (count === total) { - this.destination.complete(); - this.unsubscribe(); - } - } - } + return (source: Observable) => + count <= 0 + ? EMPTY + : lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let seen = 0; + return source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + if (++seen <= count) { + subscriber.next(value); + // We have to do <= here, because re-entrant code will increment `seen` twice. + if (count <= seen) { + subscriber.complete(); + } + } + }) + ); + }); } diff --git a/src/internal/operators/takeLast.ts b/src/internal/operators/takeLast.ts index 296bcdcb2e..d41ff2173f 100644 --- a/src/internal/operators/takeLast.ts +++ b/src/internal/operators/takeLast.ts @@ -1,3 +1,4 @@ +/** @prettier */ import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; @@ -5,6 +6,7 @@ import { EMPTY } from '../observable/empty'; import { Observable } from '../Observable'; import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits only the last `count` values emitted by the source Observable. @@ -48,66 +50,28 @@ import { lift } from '../util/lift'; * values emitted by the source Observable. */ export function takeLast(count: number): MonoTypeOperatorFunction { - if (isNaN(count)) { - throw new TypeError(`'count' is not a number`); - } - if (count < 0) { - throw new ArgumentOutOfRangeError; - } - - return function takeLastOperatorFunction(source: Observable): Observable { - if (count === 0) { - return EMPTY; - } else { - return lift(source, new TakeLastOperator(count)); - } - }; -} - -class TakeLastOperator implements Operator { - constructor(private total: number) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new TakeLastSubscriber(subscriber, this.total)); - } -} - -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 (source: Observable) => + count <= 0 + ? EMPTY + : lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let buffer: T[] = []; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + buffer.push(value); + count < buffer.length && buffer.shift(); + }, + undefined, + () => { + while (buffer.length) { + subscriber.next(buffer.shift()!); + } + subscriber.complete(); + buffer = null!; + } + ) + ); + }); } diff --git a/src/internal/operators/takeUntil.ts b/src/internal/operators/takeUntil.ts index 6bee857af4..6b620efc3e 100644 --- a/src/internal/operators/takeUntil.ts +++ b/src/internal/operators/takeUntil.ts @@ -1,11 +1,12 @@ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction, ObservableInput } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; +import { noop } from '../util/noop'; /** * Emits the values emitted by the source Observable until a `notifier` @@ -46,43 +47,10 @@ import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '.. * Observable until such time as `notifier` emits its first value. * @name takeUntil */ -export function takeUntil(notifier: Observable): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new TakeUntilOperator(notifier)); -} - -class TakeUntilOperator implements Operator { - constructor(private notifier: Observable) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - const takeUntilSubscriber = new TakeUntilSubscriber(subscriber); - const notifierSubscription = innerSubscribe(this.notifier, new SimpleInnerSubscriber(takeUntilSubscriber)); - if (notifierSubscription && !takeUntilSubscriber.notifierHasNotified) { - takeUntilSubscriber.add(notifierSubscription); - return source.subscribe(takeUntilSubscriber); - } - return takeUntilSubscriber; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class TakeUntilSubscriber extends SimpleOuterSubscriber { - notifierHasNotified = false; - - constructor(destination: Subscriber, ) { - super(destination); - } - - notifyNext(): void { - this.notifierHasNotified = true; - this.complete(); - } - - notifyComplete(): void { - // noop - } -} +export function takeUntil(notifier: ObservableInput): MonoTypeOperatorFunction { + return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + from(notifier).subscribe(new OperatorSubscriber(subscriber, () => subscriber.complete(), undefined, noop)); + !subscriber.closed && source.subscribe(subscriber); + }); +} \ No newline at end of file diff --git a/src/internal/operators/takeWhile.ts b/src/internal/operators/takeWhile.ts index 6e68916682..2de3b5f47f 100644 --- a/src/internal/operators/takeWhile.ts +++ b/src/internal/operators/takeWhile.ts @@ -1,8 +1,9 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { OperatorFunction, MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { OperatorFunction, MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; export function takeWhile(predicate: (value: T, index: number) => value is S): OperatorFunction; export function takeWhile(predicate: (value: T, index: number) => value is S, inclusive: false): OperatorFunction; @@ -51,60 +52,17 @@ export function takeWhile(predicate: (value: T, index: number) => boolean, in * `predicate`, then completes. * @name takeWhile */ -export function takeWhile( - predicate: (value: T, index: number) => boolean, - inclusive = false): MonoTypeOperatorFunction { +export function takeWhile(predicate: (value: T, index: number) => boolean, inclusive = false): MonoTypeOperatorFunction { return (source: Observable) => - lift(source, new TakeWhileOperator(predicate, inclusive)); -} - -class TakeWhileOperator implements Operator { - constructor( - private predicate: (value: T, index: number) => boolean, - private inclusive: boolean) {} - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe( - new TakeWhileSubscriber(subscriber, this.predicate, this.inclusive)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class TakeWhileSubscriber extends Subscriber { - private index: number = 0; - - constructor( - destination: Subscriber, - private predicate: (value: T, index: number) => boolean, - private inclusive: boolean) { - super(destination); - } - - protected _next(value: T): void { - const destination = this.destination; - let result: boolean; - try { - result = this.predicate(value, this.index++); - } catch (err) { - destination.error(err); - return; - } - this.nextOrComplete(value, result); - } - - private nextOrComplete(value: T, predicateResult: boolean): void { - const destination = this.destination; - if (Boolean(predicateResult)) { - destination.next(value); - } else { - if (this.inclusive) { - destination.next(value); - } - destination.complete(); - } - } + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let index = 0; + return source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + const result = predicate(value, index++); + (result || inclusive) && subscriber.next(value); + !result && subscriber.complete(); + }) + ); + }); } diff --git a/src/internal/operators/throttle.ts b/src/internal/operators/throttle.ts index 5bb43abf95..c656a0f61b 100644 --- a/src/internal/operators/throttle.ts +++ b/src/internal/operators/throttle.ts @@ -1,12 +1,12 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Subscription } from '../Subscription'; - -import { MonoTypeOperatorFunction, SubscribableOrPromise, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction, SubscribableOrPromise } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; export interface ThrottleConfig { leading?: boolean; @@ -15,7 +15,7 @@ export interface ThrottleConfig { export const defaultThrottleConfig: ThrottleConfig = { leading: true, - trailing: false + trailing: false, }; /** @@ -63,92 +63,43 @@ export const defaultThrottleConfig: ThrottleConfig = { * limit the rate of emissions from the source. * @name throttle */ -export function throttle(durationSelector: (value: T) => SubscribableOrPromise, - config: ThrottleConfig = defaultThrottleConfig): MonoTypeOperatorFunction { - return (source: Observable) => lift(source, new ThrottleOperator(durationSelector, !!config.leading, !!config.trailing)); -} - -class ThrottleOperator implements Operator { - constructor(private durationSelector: (value: T) => SubscribableOrPromise, - private leading: boolean, - private trailing: boolean) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe( - new ThrottleSubscriber(subscriber, this.durationSelector, this.leading, this.trailing) - ); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc - * @ignore - * @extends {Ignored} - */ -class ThrottleSubscriber extends SimpleOuterSubscriber { - private _throttled: Subscription | null | undefined; - private _sendValue: T | null = null; - private _hasValue = false; - - constructor(protected destination: Subscriber, - private durationSelector: (value: T) => SubscribableOrPromise, - private _leading: boolean, - private _trailing: boolean) { - super(destination); - } - - protected _next(value: T): void { - this._hasValue = true; - this._sendValue = value; - - if (!this._throttled) { - if (this._leading) { - this.send(); - } else { - this.throttle(value); - } - } - } - - private send() { - const { _hasValue, _sendValue } = this; - if (_hasValue) { - this.destination.next(_sendValue!); - this.throttle(_sendValue!); - } - this._hasValue = false; - this._sendValue = null; - } - - private throttle(value: T): void { - let result: SubscribableOrPromise; - try { - result = this.durationSelector(value); - } catch (err) { - this.destination.error(err); - return - } - this.add(this._throttled = innerSubscribe(result, new SimpleInnerSubscriber(this))); - } - - private throttlingDone() { - const { _throttled, _trailing } = this; - if (_throttled) { - _throttled.unsubscribe(); - } - this._throttled = null; - - if (_trailing) { - this.send(); - } - } - - notifyNext(): void { - this.throttlingDone(); - } - - notifyComplete(): void { - this.throttlingDone(); - } +export function throttle( + durationSelector: (value: T) => SubscribableOrPromise, + { leading, trailing }: ThrottleConfig = defaultThrottleConfig +): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let sendValue: T | null = null; + let throttled: Subscription | null = null; + + const throttlingDone = () => { + throttled?.unsubscribe(); + throttled = null; + trailing && send(); + }; + + const throttle = (value: T) => + (throttled = from(durationSelector(value)).subscribe( + new OperatorSubscriber(subscriber, throttlingDone, undefined, throttlingDone) + )); + + const send = () => { + if (hasValue) { + subscriber.next(sendValue!); + throttle(sendValue!); + } + hasValue = false; + sendValue = null; + }; + + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + hasValue = true; + sendValue = value; + !throttled && (leading ? send() : throttle(value)); + }) + ); + }); } diff --git a/src/internal/operators/throttleTime.ts b/src/internal/operators/throttleTime.ts index 1ad124d62f..1697cebbab 100644 --- a/src/internal/operators/throttleTime.ts +++ b/src/internal/operators/throttleTime.ts @@ -6,6 +6,7 @@ import { Observable } from '../Observable'; import { ThrottleConfig, defaultThrottleConfig } from './throttle'; import { MonoTypeOperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits a value from the source Observable, then ignores subsequent source @@ -143,13 +144,11 @@ export function throttleTime( */ const emit = (value: T) => { subscriber.next(value); - if (!isComplete) { - startThrottle(); - } + !isComplete && startThrottle(); }; source.subscribe( - new ThrottleTimeSubscriber( + new OperatorSubscriber( subscriber, (value) => { // We got a new value @@ -174,27 +173,16 @@ export function throttleTime( } } }, + undefined, () => { // The source completed isComplete = true; // If we're trailing, and we're in a throttle period and have a trailing value, // wait for the throttle period to end before we actually complete. // Otherwise, returning `true` here completes the result right away. - return !trailing || !throttleSubs || !hasTrailingValue; + (!trailing || !throttleSubs || !hasTrailingValue) && subscriber.complete(); } ) ); }); } - -class ThrottleTimeSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void, protected shouldComplete: () => boolean) { - super(destination); - } - - _complete() { - if (this.shouldComplete()) { - super._complete(); - } - } -} diff --git a/src/internal/operators/throwIfEmpty.ts b/src/internal/operators/throwIfEmpty.ts index db2ddb0a83..8e61630507 100644 --- a/src/internal/operators/throwIfEmpty.ts +++ b/src/internal/operators/throwIfEmpty.ts @@ -1,9 +1,11 @@ +/** @prettier */ import { EmptyError } from '../util/EmptyError'; import { Observable } from '../Observable'; import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { TeardownLogic, MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * If the source observable completes without emitting a value, it will emit @@ -35,46 +37,23 @@ import { lift } from '../util/lift'; * error to be thrown when the source observable completes without emitting a * value. */ -export function throwIfEmpty (errorFactory: (() => any) = defaultErrorFactory): MonoTypeOperatorFunction { - return (source: Observable) => { - return lift(source, new ThrowIfEmptyOperator(errorFactory)); - }; -} - -class ThrowIfEmptyOperator implements Operator { - constructor(private errorFactory: () => any) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ThrowIfEmptySubscriber(subscriber, this.errorFactory)); - } -} - -class ThrowIfEmptySubscriber extends Subscriber { - private hasValue: boolean = false; - - constructor(destination: Subscriber, private errorFactory: () => any) { - super(destination); - } - - protected _next(value: T): void { - this.hasValue = true; - this.destination.next(value); - } - - protected _complete() { - if (!this.hasValue) { - let err: any; - try { - err = this.errorFactory(); - } catch (e) { - err = e; - } - this.destination.error(err); - } else { - return this.destination.complete(); - } - } +export function throwIfEmpty(errorFactory: () => any = defaultErrorFactory): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + hasValue = true; + subscriber.next(value); + }, + undefined, + () => (hasValue ? subscriber.complete() : subscriber.error(errorFactory())) + ) + ); + }); } function defaultErrorFactory() { diff --git a/src/internal/operators/timeout.ts b/src/internal/operators/timeout.ts index 9563345d2c..c2a2ad297e 100644 --- a/src/internal/operators/timeout.ts +++ b/src/internal/operators/timeout.ts @@ -8,6 +8,7 @@ import { lift } from '../util/lift'; import { Observable } from '../Observable'; import { from } from '../observable/from'; import { createErrorClass } from '../util/createErrorClass'; +import { OperatorSubscriber } from './OperatorSubscriber'; export interface TimeoutConfig { /** @@ -300,30 +301,22 @@ export function timeout(each: number, scheduler?: SchedulerLike): MonoTypeOpe * * ![](timeout.png) */ -export function timeout( - dueOrConfig: number | Date | TimeoutConfig, - scheduler?: SchedulerLike -): OperatorFunction { +export function timeout(config: number | Date | TimeoutConfig, schedulerArg?: SchedulerLike): OperatorFunction { return (source: Observable) => { - let first: number | Date | undefined; - let each: number | undefined = undefined; - let _with: ((info: TimeoutInfo) => ObservableInput) | undefined = undefined; - let meta: any = null; - scheduler = scheduler ?? asyncScheduler; - - if (isValidDate(dueOrConfig)) { - first = dueOrConfig; - } else if (typeof dueOrConfig === 'number') { - each = dueOrConfig; - } else { - first = dueOrConfig.first; - each = dueOrConfig.each; - _with = dueOrConfig.with; - scheduler = dueOrConfig.scheduler || asyncScheduler; - meta = dueOrConfig.meta ?? null; - } - - _with = _with ?? timeoutErrorFactory; + // Intentionally terse code. + // If the first argument is a valid `Date`, then we use it as the `first` config. + // Otherwise, if the first argument is a `number`, then we use it as the `each` config. + // Otherwise, it can be assumed the first argument is the configuration object itself, and + // we destructure that into what we're going to use, setting important defaults as we do. + // NOTE: The default for `scheduler` will be the `scheduler` argument if it exists, or + // it will default to the `asyncScheduler`. + const { first, each, with: _with = timeoutErrorFactory, scheduler = schedulerArg ?? asyncScheduler, meta = null! } = (isValidDate( + config + ) + ? { first: config } + : typeof config === 'number' + ? { each: config } + : config) as TimeoutConfig; if (first == null && each == null) { // Ensure timeout was provided at runtime. @@ -332,62 +325,72 @@ export function timeout( return lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; - const subscription = new Subscription(); - let innerSub: Subscription; - let timerSubscription: Subscription | null = null; + // This subscription encapsulates our subscription to the + // source for this operator. We're capturing it separately, + // because if there is a `with` observable to fail over to, + // we want to unsubscribe from our original subscription, and + // hand of the subscription to that one. + let originalSourceSubscription: Subscription; + // The subscription for our timeout timer. This changes + // every time get get a new value. + let timerSubscription: Subscription; + // A bit of state we pass to our with and error factories to + // tell what the last value we saw was. let lastValue: T | null = null; + // A bit of state we pass to the with and error factories to + // tell how many values we have seen so far. let seen = 0; const startTimer = (delay: number) => { - subscription.add( + subscriber.add( (timerSubscription = scheduler!.schedule(() => { let withObservable: Observable; - const info: TimeoutInfo = { - meta, - lastValue, - seen, - }; try { - withObservable = from(_with!(info)); + withObservable = from( + _with!({ + meta, + lastValue, + seen, + }) + ); } catch (err) { subscriber.error(err); return; } - innerSub.unsubscribe(); - subscription.add(withObservable.subscribe(subscriber)); + originalSourceSubscription.unsubscribe(); + withObservable.subscribe(subscriber); }, delay)) ); }; - subscription.add( - (innerSub = source.subscribe({ - next: (value) => { - timerSubscription?.unsubscribe(); - timerSubscription = null; - seen++; - lastValue = value; - if (each != null && each > 0) { - startTimer(each); + subscriber.add( + (originalSourceSubscription = source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + // clear the timer so we can emit and start another one. + timerSubscription.unsubscribe(); + seen++; + // Emit + subscriber.next((lastValue = value)); + // null | undefined are both < 0. Thanks, JavaScript. + each! > 0 && startTimer(each!); + }, + undefined, + undefined, + () => { + // Be sure not to hold the last value in memory after unsubscription + // it could be quite large. + lastValue = null; } - subscriber.next(value); - }, - error: (err) => subscriber.error(err), - complete: () => subscriber.complete(), - })) + ) + )) ); - let firstTimer: number; - if (first != null) { - if (typeof first === 'number') { - firstTimer = first; - } else { - firstTimer = +first - scheduler!.now(); - } - } else { - firstTimer = each!; - } - startTimer(firstTimer); - - return subscription; + // Intentionally terse code. + // If `first` was provided, and it's a number, then use it. + // If `first` was provided and it's not a number, it's a Date, and we get the difference between it and "now". + // If `first` was not provided at all, then our first timer will be the value from `each`. + startTimer(first != null ? (typeof first === 'number' ? first : +first - scheduler!.now()) : each!); }); }; } diff --git a/src/internal/operators/window.ts b/src/internal/operators/window.ts index 507e3859fd..c6aae9780d 100644 --- a/src/internal/operators/window.ts +++ b/src/internal/operators/window.ts @@ -3,9 +3,8 @@ import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; import { Subject } from '../Subject'; import { Subscriber } from '../Subscriber'; -import { Operator } from '../Operator'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Branch out the source Observable values as a nested Observable whenever @@ -50,77 +49,49 @@ import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '.. * @name window */ export function window(windowBoundaries: Observable): OperatorFunction> { - return function windowOperatorFunction(source: Observable) { - return lift(source, 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(innerSubscribe(this.windowBoundaries, new SimpleInnerSubscriber(windowSubscriber))); - } - return sourceSubscription; - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WindowSubscriber extends SimpleOuterSubscriber { - private window: Subject = new Subject(); - - constructor(destination: Subscriber>) { - super(destination); - destination.next(this.window); - } - - notifyNext(): void { - this.openWindow(); - } - - notifyError(error: any): void { - this._error(error); - } - - notifyComplete(): void { - this._complete(); - } - - protected _next(value: T): void { - this.window.next(value); - } + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let windowSubject = new Subject(); - protected _error(err: any): void { - this.window.error(err); - this.destination.error(err); - } + subscriber.next(windowSubject.asObservable()); - protected _complete(): void { - this.window.complete(); - this.destination.complete(); - } + /** + * Subscribes to one of our two observables in this operator in the same way, + * only allowing for different behaviors with the next handler. + * @param source The observable to subscribe to. + * @param next The next handler to use with the subscription + */ + const windowSubscribe = (source: Observable, next: (value: any) => void) => + source.subscribe( + new OperatorSubscriber( + subscriber, + next, + (err: any) => { + windowSubject.error(err); + subscriber.error(err); + }, + () => { + windowSubject.complete(); + subscriber.complete(); + } + ) + ); - unsubscribe() { - if (!this.closed) { - this.window = null!; - super.unsubscribe(); - } - } + // Subscribe to our source + windowSubscribe(source, (value) => windowSubject.next(value)); + // Subscribe to the window boundaries. + windowSubscribe(windowBoundaries, () => { + windowSubject.complete(); + subscriber.next((windowSubject = new Subject())); + }); - private openWindow(): void { - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - const destination = this.destination; - const newWindow = (this.window = new Subject()); - destination.next(newWindow); - } + // Additional teardown. Note that other teardown and post-subscription logic + // is encapsulated in the act of a Subscriber subscribing to the observable + // during the subscribe call. We can return additional teardown here. + return () => { + windowSubject.unsubscribe(); + windowSubject = null!; + }; + }); } diff --git a/src/internal/operators/windowCount.ts b/src/internal/operators/windowCount.ts index f147d171f3..c65fba6f19 100644 --- a/src/internal/operators/windowCount.ts +++ b/src/internal/operators/windowCount.ts @@ -5,6 +5,7 @@ import { Observable } from '../Observable'; import { Subject } from '../Subject'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Branch out the source Observable values as a nested Observable with each @@ -69,79 +70,66 @@ import { lift } from '../util/lift'; * @name windowCount */ export function windowCount(windowSize: number, startWindowEvery: number = 0): OperatorFunction> { - return function windowCountOperatorFunction(source: Observable) { - return lift(source, 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; + const startEvery = startWindowEvery > 0 ? startWindowEvery : windowSize; - 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); - } - } + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let windows = [new Subject()]; + let starts: number[] = []; + let count = 0; - protected _error(err: any) { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift()!.error(err); - } - } - this.destination.error(err); - } + // Open the first window. + subscriber.next(windows[0].asObservable()); - protected _complete() { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift()!.complete(); - } - } - this.destination.complete(); - } + source.subscribe( + new OperatorSubscriber( + subscriber, + (value: T) => { + // Emit the value through all current windows. + // We don't need to create a new window yet, we + // do that as soon as we close one. + for (const window of windows) { + window.next(value); + } + // Here we're using the size of the window array to figure + // out if the oldest window has emitted enough values. We can do this + // because the size of the window array is a function of the values + // seen by the subscription. If it's time to close it, we complete + // it and remove it. + const c = count - windowSize + 1; + if (c >= 0 && c % startEvery === 0) { + windows.shift()!.complete(); + } - unsubscribe() { - if (!this.closed) { - this.count = 0; - this.windows = null!; - super.unsubscribe(); - } - } + // Look to see if the next count tells us it's time to open a new window. + // TODO: We need to figure out if this really makes sense. We're technically + // emitting windows *before* we have a value to emit them for. It's probably + // more expected that we should be emitting the window when the start + // count is reached -- not before. + if (++count % startEvery === 0) { + const window = new Subject(); + windows.push(window); + subscriber.next(window.asObservable()); + } + }, + (err) => { + while (windows.length > 0) { + windows.shift()!.error(err); + } + subscriber.error(err); + }, + () => { + while (windows.length > 0) { + windows.shift()!.complete(); + } + subscriber.complete(); + }, + () => { + starts = null!; + windows = null!; + } + ) + ); + }); } diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index 4fcb0f18c5..e7d2b64077 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -1,23 +1,27 @@ +/** @prettier */ import { Subject } from '../Subject'; -import { Operator } from '../Operator'; -import { async } from '../scheduler/async'; +import { asyncScheduler } 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, SchedulerLike, SchedulerAction } from '../types'; +import { Observer, OperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; - -export function windowTime(windowTimeSpan: number, - scheduler?: SchedulerLike): OperatorFunction>; -export function windowTime(windowTimeSpan: number, - windowCreationInterval: number, - scheduler?: SchedulerLike): OperatorFunction>; -export function windowTime(windowTimeSpan: number, - windowCreationInterval: number, - maxWindowSize: number, - scheduler?: SchedulerLike): OperatorFunction>; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { arrRemove } from '../util/arrRemove'; + +export function windowTime(windowTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction>; +export function windowTime( + windowTimeSpan: number, + windowCreationInterval: number, + scheduler?: SchedulerLike +): OperatorFunction>; +export function windowTime( + windowTimeSpan: number, + windowCreationInterval: number | null | void, + maxWindowSize: number, + scheduler?: SchedulerLike +): OperatorFunction>; /** * Branch out the source Observable values as a nested Observable periodically * in time. @@ -97,188 +101,110 @@ export function windowTime(windowTimeSpan: number, * intervals that determine window boundaries. * @returnAn observable of windows, which in turn are Observables. */ -export function windowTime(windowTimeSpan: number): OperatorFunction> { - let scheduler: SchedulerLike = async; - let windowCreationInterval: number | null = null; - let maxWindowSize: number = Infinity; - - if (isScheduler(arguments[3])) { - scheduler = arguments[3]; - } - - if (isScheduler(arguments[2])) { - scheduler = arguments[2]; - } else if (isNumeric(arguments[2])) { - maxWindowSize = Number(arguments[2]); - } - - if (isScheduler(arguments[1])) { - scheduler = arguments[1]; - } else if (isNumeric(arguments[1])) { - windowCreationInterval = Number(arguments[1]); - } - - return function windowTimeOperatorFunction(source: Observable) { - return lift(source, new WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)); - }; -} - -class WindowTimeOperator implements Operator> { - - constructor(private windowTimeSpan: number, - private windowCreationInterval: number | null, - private maxWindowSize: number, - private scheduler: SchedulerLike) { - } - - 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: SchedulerLike; -} - -interface TimeSpanOnlyState { - window: CountedSubject; - windowTimeSpan: number; - subscriber: WindowTimeSubscriber; - } - -interface CloseWindowContext { - action: SchedulerAction>; - 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>, - windowTimeSpan: number, - windowCreationInterval: number | null, - private maxWindowSize: number, - scheduler: SchedulerLike) { - 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 as any, windowTimeSpan, closeState)); - this.add(scheduler.schedule>(dispatchWindowCreation as any, windowCreationInterval, creationState)); - } else { - const timeSpanOnlyState: TimeSpanOnlyState = { subscriber: this, window, windowTimeSpan }; - this.add(scheduler.schedule>(dispatchWindowTimeSpanOnly as any, windowTimeSpan, timeSpanOnlyState)); - } - } - - protected _next(value: T): void { - // If we have a max window size, we might end up mutating the - // array while we're iterating over it. If that's the case, we'll - // copy it, otherwise, we don't just to save memory allocation. - const windows = this.maxWindowSize < Infinity ? this.windows.slice() : this.windows; - const len = windows.length; - for (let i = 0; i < len; i++) { - const window = windows[i]; - if (!window.closed) { - window.next(value); - if (this.maxWindowSize <= window.numberOfNextedValues) { - // mutation may occur here. - this.closeWindow(window); +export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): OperatorFunction> { + const scheduler = isScheduler(otherArgs[otherArgs.length - 1]) ? (otherArgs.pop() as SchedulerLike) : asyncScheduler; + const windowCreationInterval = (otherArgs[0] as number) ?? null; + const maxWindowSize = (otherArgs[1] as number) || Infinity; + + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + // The active windows, their related subscriptions, and removal functions. + let windowRecords: WindowRecord[] | null = []; + // If true, it means that every time we close a window, we want to start a new window. + // This is only really used for when *just* the time span is passed. + let restartOnClose = false; + + const closeWindow = (record: { window: Subject; subs: Subscription }) => { + const { window, subs } = record; + window.complete(); + subs.unsubscribe(); + arrRemove(windowRecords, record); + restartOnClose && startWindow(); + }; + + /** + * Called every time we start a new window. This also does + * the work of scheduling the job to close the window. + */ + const startWindow = () => { + if (windowRecords) { + const subs = new Subscription(); + subscriber.add(subs); + const window = new Subject(); + const record = { + window, + subs, + seen: 0, + }; + windowRecords.push(record); + subscriber.next(window.asObservable()); + subs.add(scheduler.schedule(() => closeWindow(record), windowTimeSpan)); } - } - } - } - - 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) { - windows.shift()!.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 { - const index = this.windows.indexOf(window); - // All closed windows should have been removed, - // we don't need to call complete unless they're found. - if (index >= 0) { - window.complete(); - this.windows.splice(index, 1); - } - } -} - -function dispatchWindowTimeSpanOnly(this: SchedulerAction>, state: TimeSpanOnlyState): void { - const { subscriber, windowTimeSpan, window } = state; - if (window) { - subscriber.closeWindow(window); - } - state.window = subscriber.openWindow(); - this.schedule(state, windowTimeSpan); -} - -function dispatchWindowCreation(this: SchedulerAction>, 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 as any, windowTimeSpan, timeSpanState); - action.add(context.subscription); - action.schedule(state, windowCreationInterval); + }; + + windowCreationInterval !== null && windowCreationInterval >= 0 + ? // The user passed both a windowTimeSpan (required), and a creation interval + // That means we need to start new window on the interval, and those windows need + // to wait the required time span before completing. + subscriber.add( + scheduler.schedule(function () { + startWindow(); + !this.closed && subscriber.add(this.schedule(null, windowCreationInterval)); + }, windowCreationInterval) + ) + : (restartOnClose = true); + + startWindow(); + + /** + * We need to loop over a copy of the window records several times in this operator. + * This is to save bytes over the wire more than anything. + * The reason we copy the array is that reentrant code could mutate the array while + * we are iterating over it. + */ + const loop = (cb: (record: WindowRecord) => void) => windowRecords!.slice().forEach(cb); + + /** + * Used to notify all of the windows and the subscriber in the same way + * in the error and complete handlers. + */ + const terminate = (cb: (consumer: Observer) => void) => { + loop(({ window }) => cb(window)); + cb(subscriber); + subscriber.unsubscribe(); + }; + + source.subscribe( + new OperatorSubscriber( + subscriber, + (value: T) => { + // Notify all windows of the value. + loop((record) => { + record.window.next(value); + // If the window is over the max size, we need to close it. + maxWindowSize <= ++record.seen && closeWindow(record); + }); + }, + // Notify the windows and the downstream subscriber of the error and clean up. + (err) => terminate((consumer) => consumer.error(err)), + // Complete the windows and the downstream subscriber and clean up. + () => terminate((consumer) => consumer.complete()) + ) + ); + + // Additional teardown. This will be called when the + // destination tears down. Other teardowns are registered implicitly + // above via subscription. + return () => { + // Ensure that the buffer is released. + windowRecords = null!; + }; + }); } -function dispatchWindowClose(this: SchedulerAction>, state: CloseState): void { - const { subscriber, window, context } = state; - if (context && context.action && context.subscription) { - context.action.remove(context.subscription); - } - subscriber.closeWindow(window); +interface WindowRecord { + seen: number; + window: Subject; + subs: Subscription; } diff --git a/src/internal/operators/windowToggle.ts b/src/internal/operators/windowToggle.ts index ea2376ad2f..d049547e00 100644 --- a/src/internal/operators/windowToggle.ts +++ b/src/internal/operators/windowToggle.ts @@ -1,12 +1,14 @@ /** @prettier */ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; import { Subscription } from '../Subscription'; -import { ComplexOuterSubscriber, ComplexInnerSubscriber, innerSubscribe } from '../innerSubscribe'; -import { OperatorFunction } from '../types'; +import { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { noop } from '../util/noop'; +import { arrRemove } from '../util/arrRemove'; /** * Branch out the source Observable values as a nested Observable starting from @@ -56,152 +58,89 @@ import { lift } from '../util/lift'; * @name windowToggle */ export function windowToggle( - openings: Observable, - closingSelector: (openValue: O) => Observable + openings: ObservableInput, + closingSelector: (openValue: O) => ObservableInput ): OperatorFunction> { - return (source: Observable) => lift(source, 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 ComplexOuterSubscriber { - private contexts: WindowContext[] = []; - private openSubscription: Subscription | undefined; - - constructor( - destination: Subscriber>, - private openings: Observable, - private closingSelector: (openValue: O) => Observable - ) { - super(destination); - this.add((this.openSubscription = innerSubscribe(openings, new ComplexInnerSubscriber(this, openings, 0)))); - } - - 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(); - } - - unsubscribe() { - if (!this.closed) { - 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(); + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + const windows: Subject[] = []; + + const handleError = (err: any) => { + while (0 < windows.length) { + windows.shift()!.error(err); } - } - super.unsubscribe(); - } - } + subscriber.error(err); + }; - notifyNext(outerValue: any, innerValue: any): void { - if (outerValue === this.openings) { - let closingNotifier; + let openNotifier: Observable; try { - const { closingSelector } = this; - closingNotifier = closingSelector(innerValue); - } catch (e) { - return this.error(e); - } - - const window = new Subject(); - const subscription = new Subscription(); - const context = { window, subscription }; - this.contexts.push(context); - const innerSubscription = innerSubscribe(closingNotifier, new ComplexInnerSubscriber(this, context, 0)); - - if (innerSubscription!.closed) { - this.closeWindow(this.contexts.length - 1); - } else { - (innerSubscription).context = context; - subscription.add(innerSubscription); + openNotifier = from(openings); + } catch (err) { + subscriber.error(err); + return; } - - 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(); - } + openNotifier.subscribe( + new OperatorSubscriber( + subscriber, + (openValue) => { + const window = new Subject(); + windows.push(window); + const closingSubscription = new Subscription(); + const closeWindow = () => { + arrRemove(windows, window); + window.complete(); + closingSubscription.unsubscribe(); + }; + + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector(openValue)); + } catch (err) { + handleError(err); + return; + } + + subscriber.next(window.asObservable()); + + closingSubscription.add(closingNotifier.subscribe(new OperatorSubscriber(subscriber, closeWindow, handleError, closeWindow))); + }, + undefined, + noop + ) + ); + + // Subcribe to the source to get things started. + source.subscribe( + new OperatorSubscriber( + subscriber, + (value: T) => { + // Copy the windows array before we emit to + // make sure we don't have issues with reentrant code. + const windowsCopy = windows.slice(); + for (const window of windowsCopy) { + window.next(value); + } + }, + handleError, + () => { + // Complete all of our windows before we complete. + while (0 < windows.length) { + windows.shift()!.complete(); + } + subscriber.complete(); + }, + () => { + // Add this teardown so that all window subjects are + // disposed of. This way, if a user tries to subscribe + // to a window *after* the outer subscription has been unsubscribed, + // they will get an error, instead of waiting forever to + // see if a value arrives. + while (0 < windows.length) { + windows.shift()!.unsubscribe(); + } + } + ) + ); + }); } diff --git a/src/internal/operators/windowWhen.ts b/src/internal/operators/windowWhen.ts index 5f55ab5259..4860fc0f9a 100644 --- a/src/internal/operators/windowWhen.ts +++ b/src/internal/operators/windowWhen.ts @@ -1,11 +1,12 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; -import { Subscription } from '../Subscription'; -import { ComplexOuterSubscriber, ComplexInnerSubscriber, innerSubscribe } from '../innerSubscribe'; -import { OperatorFunction } from '../types'; +import { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; + /** * Branch out the source Observable values as a nested Observable using a * factory function of closing Observables to determine when to start a new @@ -50,95 +51,77 @@ import { lift } from '../util/lift'; * are Observables. * @name windowWhen */ -export function windowWhen(closingSelector: () => Observable): OperatorFunction> { - return function windowWhenOperatorFunction(source: Observable) { - return lift(source, 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 ComplexOuterSubscriber { - private window: Subject | undefined; - private closingNotification: Subscription | undefined; - - constructor(protected destination: Subscriber>, - private closingSelector: () => Observable) { - super(destination); - this.openWindow(); - } - - notifyNext(_outerValue: T, _innerValue: any, - _outerIndex: number, - innerSub: ComplexInnerSubscriber): void { - this.openWindow(innerSub); - } - - notifyError(error: any): void { - this._error(error); - } - - notifyComplete(innerSub: ComplexInnerSubscriber): void { - this.openWindow(innerSub); - } +export function windowWhen(closingSelector: () => ObservableInput): OperatorFunction> { + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let window: Subject | null; + let closingSubscriber: Subscriber | undefined; - protected _next(value: T): void { - this.window!.next(value); - } + /** + * When we get an error, we have to notify both the + * destiation subscriber and the window. + */ + const handleError = (err: any) => { + window!.error(err); + subscriber.error(err); + }; - protected _error(err: any): void { - this.window!.error(err); - this.destination.error(err); - this.unsubscribeClosingNotification(); - } + /** + * Called every time we need to open a window. + * Recursive, as it will start the closing notifier, which + * inevitably *should* call openWindow -- but may not if + * it is a "never" observable. + */ + const openWindow = () => { + // We need to clean up our closing subscription, + // we only cared about the first next or complete notification. + closingSubscriber?.unsubscribe(); - protected _complete(): void { - this.window!.complete(); - this.destination.complete(); - this.unsubscribeClosingNotification(); - } + // Close our window before starting a new one. + window?.complete(); - private unsubscribeClosingNotification(): void { - if (this.closingNotification) { - this.closingNotification.unsubscribe(); - } - } + // Start the new window. + window = new Subject(); + subscriber.next(window.asObservable()); - private openWindow(innerSub: ComplexInnerSubscriber | null = null): void { - if (innerSub) { - this.remove(innerSub); - innerSub.unsubscribe(); - } + // Get our closing notifier. + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector()); + } catch (err) { + handleError(err); + return; + } - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } + // Subscribe to the closing notifier, be sure + // to capture the subscriber (aka Subscription) + // so we can clean it up when we close the window + // and open a new one. + closingNotifier.subscribe((closingSubscriber = new OperatorSubscriber(subscriber, openWindow, handleError, openWindow))); + }; - const window = this.window = new Subject(); - this.destination.next(window); + // Start the first window. + openWindow(); - let closingNotifier; - try { - const { closingSelector } = this; - closingNotifier = closingSelector(); - } catch (e) { - this.destination.error(e); - this.window.error(e); - return; - } - this.add(this.closingNotification = innerSubscribe(closingNotifier, new ComplexInnerSubscriber(this, undefined, 0))); - } + // Subscribe to the source + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => window!.next(value), + handleError, + () => { + // The source completed, close the window and complete. + window!.complete(); + subscriber.complete(); + }, + () => { + // Be sure to clean up our closing subscription + // when this tears down. + closingSubscriber?.unsubscribe(); + window = null!; + } + ) + ); + }); } diff --git a/src/internal/operators/withLatestFrom.ts b/src/internal/operators/withLatestFrom.ts index 6de8a5e972..e986133e1a 100644 --- a/src/internal/operators/withLatestFrom.ts +++ b/src/internal/operators/withLatestFrom.ts @@ -1,22 +1,103 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { ComplexOuterSubscriber, innerSubscribe, ComplexInnerSubscriber } from '../innerSubscribe'; import { ObservableInput, OperatorFunction, ObservedValueOf } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { from } from '../observable/from'; +import { identity } from '../util/identity'; +import { noop } from '../util/noop'; /* tslint:disable:max-line-length */ export function withLatestFrom(project: (v1: T) => R): OperatorFunction; -export function withLatestFrom, R>(source2: O2, project: (v1: T, v2: ObservedValueOf) => R): OperatorFunction; -export function withLatestFrom, O3 extends ObservableInput, R>(v2: O2, v3: O3, project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf) => R): OperatorFunction; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, R>(v2: O2, v3: O3, v4: O4, project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf, v4: ObservedValueOf) => R): OperatorFunction; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, O5 extends ObservableInput, R>(v2: O2, v3: O3, v4: O4, v5: O5, project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf, v4: ObservedValueOf, v5: ObservedValueOf) => R): OperatorFunction; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, O5 extends ObservableInput, O6 extends ObservableInput, R>(v2: O2, v3: O3, v4: O4, v5: O5, v6: O6, project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf, v4: ObservedValueOf, v5: ObservedValueOf, v6: ObservedValueOf) => R): OperatorFunction; +export function withLatestFrom, R>( + source2: O2, + project: (v1: T, v2: ObservedValueOf) => R +): OperatorFunction; +export function withLatestFrom, O3 extends ObservableInput, R>( + v2: O2, + v3: O3, + project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf) => R +): OperatorFunction; +export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, R>( + v2: O2, + v3: O3, + v4: O4, + project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf, v4: ObservedValueOf) => R +): OperatorFunction; +export function withLatestFrom< + T, + O2 extends ObservableInput, + O3 extends ObservableInput, + O4 extends ObservableInput, + O5 extends ObservableInput, + R +>( + v2: O2, + v3: O3, + v4: O4, + v5: O5, + project: (v1: T, v2: ObservedValueOf, v3: ObservedValueOf, v4: ObservedValueOf, v5: ObservedValueOf) => R +): OperatorFunction; +export function withLatestFrom< + T, + O2 extends ObservableInput, + O3 extends ObservableInput, + O4 extends ObservableInput, + O5 extends ObservableInput, + O6 extends ObservableInput, + R +>( + v2: O2, + v3: O3, + v4: O4, + v5: O5, + v6: O6, + project: ( + v1: T, + v2: ObservedValueOf, + v3: ObservedValueOf, + v4: ObservedValueOf, + v5: ObservedValueOf, + v6: ObservedValueOf + ) => R +): OperatorFunction; export function withLatestFrom>(source2: O2): OperatorFunction]>; -export function withLatestFrom, O3 extends ObservableInput>(v2: O2, v3: O3): OperatorFunction, ObservedValueOf]>; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput>(v2: O2, v3: O3, v4: O4): OperatorFunction, ObservedValueOf, ObservedValueOf]>; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, O5 extends ObservableInput>(v2: O2, v3: O3, v4: O4, v5: O5): OperatorFunction, ObservedValueOf, ObservedValueOf, ObservedValueOf]>; -export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput, O5 extends ObservableInput, O6 extends ObservableInput>(v2: O2, v3: O3, v4: O4, v5: O5, v6: O6): OperatorFunction, ObservedValueOf, ObservedValueOf, ObservedValueOf, ObservedValueOf]>; +export function withLatestFrom, O3 extends ObservableInput>( + v2: O2, + v3: O3 +): OperatorFunction, ObservedValueOf]>; +export function withLatestFrom, O3 extends ObservableInput, O4 extends ObservableInput>( + v2: O2, + v3: O3, + v4: O4 +): OperatorFunction, ObservedValueOf, ObservedValueOf]>; +export function withLatestFrom< + T, + O2 extends ObservableInput, + O3 extends ObservableInput, + O4 extends ObservableInput, + O5 extends ObservableInput +>( + v2: O2, + v3: O3, + v4: O4, + v5: O5 +): OperatorFunction, ObservedValueOf, ObservedValueOf, ObservedValueOf]>; +export function withLatestFrom< + T, + O2 extends ObservableInput, + O3 extends ObservableInput, + O4 extends ObservableInput, + O5 extends ObservableInput, + O6 extends ObservableInput +>( + v2: O2, + v3: O3, + v4: O4, + v5: O5, + v6: O6 +): OperatorFunction, ObservedValueOf, ObservedValueOf, ObservedValueOf, ObservedValueOf]>; export function withLatestFrom(...observables: Array | ((...values: Array) => R)>): OperatorFunction; export function withLatestFrom(array: ObservableInput[]): OperatorFunction; export function withLatestFrom(array: ObservableInput[], project: (...values: Array) => R): OperatorFunction; @@ -66,88 +147,68 @@ export function withLatestFrom(array: ObservableInput[], project: (.. * each input Observable. * @name withLatestFrom */ -export function withLatestFrom(...args: Array | ((...values: Array) => R)>): OperatorFunction { +export function withLatestFrom(...inputs: any[]): OperatorFunction { return (source: Observable) => { - let project: any; - if (typeof args[args.length - 1] === 'function') { - project = args.pop(); + let project: (...values: any[]) => R; + if (typeof inputs[inputs.length - 1] === 'function') { + project = inputs.pop(); } - const observables = []>args; - return lift(source, new WithLatestFromOperator(observables, project)); - }; -} - -class WithLatestFromOperator implements Operator { - constructor(private observables: Observable[], - private project?: (...values: any[]) => Observable) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new WithLatestFromSubscriber(subscriber, this.observables, this.project)); - } -} -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class WithLatestFromSubscriber extends ComplexOuterSubscriber { - private values: any[]; - private toRespond: number[] = []; + return lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + const len = inputs.length; + const otherValues = new Array(len); + // An array of whether or not the other sources have emitted. Matched with them by index. + // TODO: At somepoint, we should investigate the performance implications here, and look + // into using a `Set()` and checking the `size` to see if we're ready. + let hasValue = inputs.map(() => false); + // Flipped true when we have at least one value from all other sources and + // we are ready to start emitting values. + let ready = false; - constructor(destination: Subscriber, - observables: Observable[], - private project?: (...values: any[]) => Observable) { - super(destination); - const len = observables.length; - this.values = new Array(len); + // Source subscription + source.subscribe( + new OperatorSubscriber(subscriber, (value) => { + if (ready) { + // We have at least one value from the other sources. Go ahead and emit. + const values = [value, ...otherValues]; + subscriber.next(project ? project(...values) : values); + } + }) + ); - for (let i = 0; i < len; i++) { - this.toRespond.push(i); - } - - for (let i = 0; i < len; i++) { - let observable = observables[i]; - this.add(innerSubscribe(observable, new ComplexInnerSubscriber(this, undefined, i))); - } - } - - notifyNext(_outerValue: T, innerValue: R, - outerIndex: number): void { - this.values[outerIndex] = innerValue; - const toRespond = this.toRespond; - if (toRespond.length > 0) { - const found = toRespond.indexOf(outerIndex); - if (found !== -1) { - toRespond.splice(found, 1); - } - } - } - - notifyComplete() { - // noop - } - - protected _next(value: T) { - if (this.toRespond.length === 0) { - const args = [value, ...this.values]; - if (this.project) { - this._tryProject(args); - } else { - this.destination.next(args); + // Other sources + for (let i = 0; i < len; i++) { + const input = inputs[i]; + let otherSource: Observable; + try { + otherSource = from(input); + } catch (err) { + subscriber.error(err); + return; + } + otherSource.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + otherValues[i] = value; + if (!ready && !hasValue[i]) { + // If we're not ready yet, flag to show this observable has emitted. + hasValue[i] = true; + // Intentionally terse code. + // If all of our other observables have emitted, set `ready` to `true`, + // so we know we can start emitting values, then clean up the `hasValue` array, + // because we don't need it anymore. + (ready = hasValue.every(identity)) && (hasValue = null!); + } + }, + undefined, + // Completing one of the other sources has + // no bearing on the completion of our result. + noop + ) + ); } - } - } - - private _tryProject(args: any[]) { - let result: any; - try { - result = this.project!.apply(this, args); - } catch (err) { - this.destination.error(err); - return; - } - this.destination.next(result); - } + }); + }; } diff --git a/src/internal/scheduler/AsyncAction.ts b/src/internal/scheduler/AsyncAction.ts index 1e3d5ab0c4..988d136374 100644 --- a/src/internal/scheduler/AsyncAction.ts +++ b/src/internal/scheduler/AsyncAction.ts @@ -4,6 +4,7 @@ import { SchedulerAction } from '../types'; import { Subscription } from '../Subscription'; import { AsyncScheduler } from './AsyncScheduler'; import { intervalProvider } from './intervalProvider'; +import { arrRemove } from '../util/arrRemove'; /** * We need this JSDoc comment for affecting ESDoc. @@ -132,18 +133,12 @@ export class AsyncAction extends Action { unsubscribe() { if (!this.closed) { const { id, scheduler } = this; - const actions = scheduler.actions; - const index = actions.indexOf(this); + const { actions } = scheduler; - this.work = null!; - this.state = null!; + this.work = this.state = this.scheduler = null!; this.pending = false; - this.scheduler = null!; - - if (index !== -1) { - actions.splice(index, 1); - } + arrRemove(actions, this); if (id != null) { this.id = this.recycleAsyncId(scheduler, id, null); } diff --git a/src/internal/util/arrRemove.ts b/src/internal/util/arrRemove.ts new file mode 100644 index 0000000000..7763267b3c --- /dev/null +++ b/src/internal/util/arrRemove.ts @@ -0,0 +1,13 @@ +/** @prettier */ + +/** + * Removes an item from an array, mutating it. + * @param arr The array to remove the item from + * @param item The item to remove + */ +export function arrRemove(arr: T[] | undefined | null, item: T) { + if (arr) { + const index = arr.indexOf(item); + 0 <= index && arr.splice(index, 1); + } +} diff --git a/src/internal/util/lift.ts b/src/internal/util/lift.ts index e9fbf7c316..f250c665f7 100644 --- a/src/internal/util/lift.ts +++ b/src/internal/util/lift.ts @@ -1,6 +1,8 @@ /** @prettier */ import { Observable } from '../Observable'; import { Operator } from '../Operator'; +import { Subscriber } from '../Subscriber'; +import { TeardownLogic } from '../types'; /** * A utility to lift observables. Will also error if an observable is passed that does not @@ -23,32 +25,25 @@ export function lift(source: Observable, operator?: Operator): Ob throw new TypeError('Unable to lift unknown Observable type'); } -// TODO: Figure out proper typing for what we're doing below at some point. -// For right now it's not that important, as it's internal implementation and not -// public typings on a public API. - /** - * A utility used to lift observables in the case that we are trying to convert a static observable - * creation function to an operator that appropriately uses lift. Ultimately this is a smell - * related to `lift`, hence the name. - * - * We _must_ do this for version 7, because it is what allows subclassed observables to compose through - * the operators that use this. That will be going away in v8. - * - * See https://github.com/ReactiveX/rxjs/issues/5571 - * and https://github.com/ReactiveX/rxjs/issues/5431 - * - * @param source the original observable source for the operator - * @param liftedSource the actual composed source we want to lift - * @param operator the operator to lift it with (often undefined in this case) + * A lightweight wrapper to deal with sitations where there may be try/catching at the + * time of the subscription (and not just via notifications). + * @param source The source observable to lift + * @param wrappedOperator The lightweight operator function to wrap. */ -export function stankyLift(source: Observable, liftedSource: Observable, operator?: Operator): Observable { - if (hasLift(source)) { - return source.lift.call(liftedSource, operator); - } - throw new TypeError('Unable to lift unknown Observable type'); +export function wrappedLift( + source: Observable, + wrappedOperator: (subscriber: Subscriber, liftedSource: Observable) => TeardownLogic +): Observable { + return lift(source, function (this: Subscriber, liftedSource: Observable) { + try { + wrappedOperator(this, liftedSource); + } catch (err) { + this.error(err); + } + }); } -function hasLift(source: any): source is { lift: InstanceType['lift'] } { +export function hasLift(source: any): source is { lift: InstanceType['lift'] } { return source && typeof source.lift === 'function'; } diff --git a/src/internal/util/not.ts b/src/internal/util/not.ts index e5e6952297..5e5d7e2d8f 100644 --- a/src/internal/util/not.ts +++ b/src/internal/util/not.ts @@ -1,8 +1,3 @@ -export function not(pred: Function, thisArg: any): Function { - function notPred(): any { - return !(( notPred).pred.apply(( notPred).thisArg, arguments)); - } - ( notPred).pred = pred; - ( notPred).thisArg = thisArg; - return notPred; +export function not(pred: (value: T, index: number) => boolean, thisArg: any): (value: T, index: number) => boolean { + return (value: T, index: number) => !pred.call(thisArg, value, index); } \ No newline at end of file diff --git a/src/internal/util/subscribeTo.ts b/src/internal/util/subscribeTo.ts deleted file mode 100644 index 7e3247309d..0000000000 --- a/src/internal/util/subscribeTo.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ObservableInput } from '../types'; -import { subscribeToArray } from './subscribeToArray'; -import { subscribeToPromise } from './subscribeToPromise'; -import { subscribeToIterable } from './subscribeToIterable'; -import { subscribeToObservable } from './subscribeToObservable'; -import { isArrayLike } from './isArrayLike'; -import { isPromise } from './isPromise'; -import { isObject } from './isObject'; -import { iterator as Symbol_iterator } from '../symbol/iterator'; -import { observable as Symbol_observable } from '../symbol/observable'; -import { Subscription } from '../Subscription'; -import { Subscriber } from '../Subscriber'; -import { subscribeToAsyncIterable } from './subscribeToAsyncIterable'; - -export const subscribeTo = (result: ObservableInput): (subscriber: Subscriber) => Subscription | void => { - if (!!result && typeof (result as any)[Symbol_observable] === 'function') { - return subscribeToObservable(result as any); - } else if (isArrayLike(result)) { - return subscribeToArray(result); - } else if (isPromise(result)) { - return subscribeToPromise(result); - } else if (!!result && typeof (result as any)[Symbol_iterator] === 'function') { - return subscribeToIterable(result as any); - } else if ( - Symbol && Symbol.asyncIterator && - !!result && typeof (result as any)[Symbol.asyncIterator] === 'function' - ) { - return subscribeToAsyncIterable(result as any); - } else { - const value = isObject(result) ? 'an invalid object' : `'${result}'`; - const msg = `You provided ${value} where a stream was expected.` - + ' You can provide an Observable, Promise, Array, or Iterable.'; - throw new TypeError(msg); - } -}; diff --git a/src/operators/index.ts b/src/operators/index.ts index 13347cc81a..9ae9a424a3 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -10,11 +10,10 @@ export { bufferWhen } from '../internal/operators/bufferWhen'; export { catchError } from '../internal/operators/catchError'; export { combineAll } from '../internal/operators/combineAll'; export { combineLatest, combineLatestWith } from '../internal/operators/combineLatestWith'; -export { concat } from '../internal/operators/concat'; export { concatAll } from '../internal/operators/concatAll'; export { concatMap } from '../internal/operators/concatMap'; export { concatMapTo } from '../internal/operators/concatMapTo'; -export { concatWith } from '../internal/operators/concatWith'; +export { concat, concatWith } from '../internal/operators/concatWith'; export { count } from '../internal/operators/count'; export { debounce } from '../internal/operators/debounce'; export { debounceTime } from '../internal/operators/debounceTime';