From f93fb9c1fb7434c97e1d156370756159c5f2b077 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Fri, 11 Sep 2020 10:40:15 -0500 Subject: [PATCH 001/138] fix(scan): proper indexes when seed is not supplied - Smaller implementation - Improved docs - Adds comments - Adds a test - Fixes weird adjustment in `reduce`. Closes #4348 Closes #3879 --- spec/operators/scan-spec.ts | 10 ++ src/internal/operators/reduce.ts | 2 +- src/internal/operators/scan.ts | 178 ++++++++++++++++++------------- 3 files changed, 113 insertions(+), 77 deletions(-) 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/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/scan.ts b/src/internal/operators/scan.ts index 2916be207e..5c907458c3 100644 --- a/src/internal/operators/scan.ts +++ b/src/internal/operators/scan.ts @@ -1,111 +1,137 @@ -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'; -/* 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; - } + const hasSeed = arguments.length >= 2; - return function scanOperatorFunction(source: Observable) { - return lift(source, new ScanOperator(accumulator, seed, hasSeed)); + return (source: Observable) => { + return lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasState = hasSeed; + let state: any = hasSeed ? seed! : null!; + let index = 0; + source.subscribe( + new ScanSubscriber(subscriber, (value) => { + const i = index++; + if (!hasState) { + // If a seed was not passed, we use the first value from the source + // as the initial state. That means we also pass it through, and the + // accumulator (reducer) does not get executed. + hasState = true; + state = value; + } else { + // Otherwise, if we have a seed, or we already have state, we try + // to execute the accumulator, and we handle the error appropriately. + try { + state = accumulator(state, value, i); + } catch (err) { + // An error occurred in the user-provided function, forward it + // to the consumer via error notification. + subscriber.error(err); + return; + } + } + subscriber.next(state); + }) + ); + }); }; } -class ScanOperator implements Operator { - constructor(private accumulator: (acc: V|A|S, value: V, index: number) => A, private seed?: S, private hasSeed: boolean = false) {} - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class ScanSubscriber extends Subscriber { - private index: number = 0; - - constructor(destination: Subscriber, private accumulator: (acc: V|A, value: V, index: number) => A, private _state: any, - private _hasState: boolean) { +class ScanSubscriber extends Subscriber { + constructor(destination: Subscriber, protected _next: (value: T) => void) { 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); - } - } } From da733775a96d7e6d814edb5e81891f7c0c7b8b10 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sat, 12 Sep 2020 19:06:35 -0500 Subject: [PATCH 002/138] refactor(groupBy): reduce the size of the implementation - Refactor to make it smaller - Replaced a faulty test with a test that tests the same thing. Verified the behavior has not changed since 6.x. --- spec/operators/groupBy-spec.ts | 79 +++----- src/internal/operators/groupBy.ts | 298 +++++++++++------------------- 2 files changed, 142 insertions(+), 235 deletions(-) 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/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 24c72f8d23..10f56f9db2 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -1,13 +1,10 @@ /** @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 { OperatorFunction, Observer } from '../types'; import { lift } from '../util/lift'; -/* tslint:disable:max-line-length */ export function groupBy( keySelector: (value: T) => value is K ): OperatorFunction | GroupedObservable>>; @@ -28,7 +25,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,7 +123,100 @@ 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; + const groups = new Map>(); + let groupBySubscriber: GroupBySubscriber2; + + function createGroupedObservable(key: K, groupSubject: Subject) { + const result: any = new Observable((goSubscriber) => { + groupBySubscriber.count++; + const innerSub = groupSubject.subscribe(goSubscriber); + return () => { + innerSub.unsubscribe(); + if (--groupBySubscriber.count === 0 && groupBySubscriber.unsubAttempted) { + groupBySubscriber.unsubscribe(); + } + }; + }); + result.key = key; + return result; + } + + groupBySubscriber = new GroupBySubscriber2( + subscriber, + (value: T) => { + let key: K; + try { + key = keySelector(value); + } catch (err) { + groupBySubscriber.error(err); + return; + } + + let element: R; + if (elementSelector) { + try { + element = elementSelector(value); + } catch (err) { + groupBySubscriber.error(err); + return; + } + } else { + element = value as any; + } + + let group = groups.get(key); + if (!group) { + if (subjectSelector) { + try { + group = subjectSelector(); + } catch (err) { + groupBySubscriber.error(err); + return; + } + } else { + group = new Subject(); + } + + groups.set(key, group); + const grouped = createGroupedObservable(key, group); + subscriber.next(grouped); + if (durationSelector) { + let duration: any; + try { + duration = durationSelector(grouped); + } catch (err) { + groupBySubscriber.error(err); + return; + } + const durationSubscriber = new GroupDurationSubscriber( + group, + () => { + group!.complete(); + durationSubscriber?.unsubscribe(); + }, + () => groups.delete(key) + ); + groupBySubscriber.add(duration.subscribe(durationSubscriber)); + } + } + + group.next(element!); + }, + (err) => { + groups.forEach((group) => group.error(err)); + subscriber.error(err); + }, + () => { + groups.forEach((group) => group.complete()); + subscriber.complete(); + } + ); + + return source.subscribe(groupBySubscriber); + }); } export interface RefCountSubscription { @@ -137,204 +226,41 @@ export interface RefCountSubscription { attemptedToUnsubscribe: boolean; } -class GroupByOperator implements Operator> { - constructor( - private keySelector: (value: T) => K, - private elementSelector?: ((value: T) => R) | void, - private durationSelector?: (grouped: GroupedObservable) => Observable, - private subjectSelector?: () => Subject - ) {} - - call(subscriber: Subscriber>, source: any): any { - return source.subscribe( - new GroupBySubscriber(subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector) - ); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class GroupBySubscriber extends Subscriber implements RefCountSubscription { - private groups: Map> | null = null; - public attemptedToUnsubscribe: boolean = false; - public count: number = 0; +class GroupBySubscriber2 extends Subscriber { + count = 0; + unsubAttempted = false; constructor( - destination: Subscriber>, - private keySelector: (value: T) => K, - private elementSelector?: ((value: T) => R) | void, - private durationSelector?: (grouped: GroupedObservable) => Observable, - private subjectSelector?: () => Subject + destination: Subscriber, + protected _next: (value: T) => void, + protected _error: (err: any) => void, + protected _complete: () => void ) { super(destination); } - protected _next(value: T): void { - let key: K; - try { - key = this.keySelector(value); - } catch (err) { - this.error(err); - return; - } - - this._group(value, key); - } - - private _group(value: T, key: K) { - let groups = this.groups; - - if (!groups) { - groups = this.groups = new Map>(); - } - - let group = groups.get(key); - - let element: R; - if (this.elementSelector) { - try { - element = this.elementSelector(value); - } catch (err) { - this.error(err); - } - } else { - element = value as any; - } - - 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))); - } - } - - 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(); - } + this.unsubAttempted = true; + if (this.count === 0) { + super.unsubscribe(); } } } -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class GroupDurationSubscriber extends Subscriber { - constructor(private key: K, group: Subject, private parent: GroupBySubscriber) { - super(group); - } - - protected _next(): void { - this.complete(); +class GroupDurationSubscriber extends Subscriber { + constructor(destination: Observer, protected _next: (value: T) => void, private onUnsubscribe: () => void) { + super(destination); } unsubscribe() { if (!this.closed) { - const { parent, key } = this; - this.key = this.parent = null!; - if (parent) { - parent.removeGroup(key); - } + this.isStopped = true; + this.onUnsubscribe(); 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 - */ -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 { + readonly key: K; } From dc79c3af094f9eb1e5790924d5a249065671810d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sat, 12 Sep 2020 19:12:04 -0500 Subject: [PATCH 003/138] refactor: fix up the name, remove the 2 --- src/internal/operators/groupBy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 10f56f9db2..a4b14381b3 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -127,7 +127,7 @@ export function groupBy( lift(source, function (this: Subscriber>, source: Observable) { const subscriber = this; const groups = new Map>(); - let groupBySubscriber: GroupBySubscriber2; + let groupBySubscriber: GroupBySubscriber; function createGroupedObservable(key: K, groupSubject: Subject) { const result: any = new Observable((goSubscriber) => { @@ -144,7 +144,7 @@ export function groupBy( return result; } - groupBySubscriber = new GroupBySubscriber2( + groupBySubscriber = new GroupBySubscriber( subscriber, (value: T) => { let key: K; @@ -226,7 +226,7 @@ export interface RefCountSubscription { attemptedToUnsubscribe: boolean; } -class GroupBySubscriber2 extends Subscriber { +class GroupBySubscriber extends Subscriber { count = 0; unsubAttempted = false; From ed841b40927f4df522ddb50d1bf6999f9364d829 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sat, 12 Sep 2020 19:39:37 -0500 Subject: [PATCH 004/138] refactor: smaller still - Centralize all try/catching for next calls. --- src/internal/operators/groupBy.ts | 52 +++++++++---------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index a4b14381b3..4b043224a2 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -147,50 +147,17 @@ export function groupBy( groupBySubscriber = new GroupBySubscriber( subscriber, (value: T) => { - let key: K; - try { - key = keySelector(value); - } catch (err) { - groupBySubscriber.error(err); - return; - } - - let element: R; - if (elementSelector) { - try { - element = elementSelector(value); - } catch (err) { - groupBySubscriber.error(err); - return; - } - } else { - element = value as any; - } + const key = keySelector(value); + const element = elementSelector ? elementSelector(value) : value; let group = groups.get(key); if (!group) { - if (subjectSelector) { - try { - group = subjectSelector(); - } catch (err) { - groupBySubscriber.error(err); - return; - } - } else { - group = new Subject(); - } - + group = subjectSelector ? subjectSelector() : new Subject(); groups.set(key, group); const grouped = createGroupedObservable(key, group); subscriber.next(grouped); if (durationSelector) { - let duration: any; - try { - duration = durationSelector(grouped); - } catch (err) { - groupBySubscriber.error(err); - return; - } + const duration = durationSelector(grouped); const durationSubscriber = new GroupDurationSubscriber( group, () => { @@ -232,13 +199,22 @@ class GroupBySubscriber extends Subscriber { constructor( destination: Subscriber, - protected _next: (value: T) => void, + protected onNext: (value: T) => void, protected _error: (err: any) => void, protected _complete: () => void ) { super(destination); } + // TODO: Unify this pattern elsewhere to reduce try-catching. + protected _next(value: T) { + try { + this.onNext(value); + } catch (err) { + this._error(err); + } + } + unsubscribe() { this.unsubAttempted = true; if (this.count === 0) { From 32f961981bec9a32dfa49d2244ab3e4d6e0e4800 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:02:40 -0500 Subject: [PATCH 005/138] refactor: Add OperatorSubscriber, update map - map is smaller --- spec/operators/map-spec.ts | 9 +----- src/internal/Subscriber.ts | 2 +- src/internal/operators/OperatorSubscriber.ts | 17 +++++++++++ src/internal/operators/map.ts | 30 ++------------------ 4 files changed, 22 insertions(+), 36 deletions(-) create mode 100644 src/internal/operators/OperatorSubscriber.ts 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/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/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts new file mode 100644 index 0000000000..59a5dc5fa0 --- /dev/null +++ b/src/internal/operators/OperatorSubscriber.ts @@ -0,0 +1,17 @@ +/** @prettier */ +import { Subscriber } from '../Subscriber'; + +export class OperatorSubscriber extends Subscriber { + constructor(destination: Subscriber, onNext?: (value: T) => void) { + super(destination); + if (onNext) { + this._next = function (value: T) { + try { + onNext?.(value); + } catch (err) { + this._error(err); + } + }; + } + } +} diff --git a/src/internal/operators/map.ts b/src/internal/operators/map.ts index 82d72c20ea..fef2b04393 100644 --- a/src/internal/operators/map.ts +++ b/src/internal/operators/map.ts @@ -2,6 +2,7 @@ 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 +43,14 @@ 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) { 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); + source.subscribe(new OperatorSubscriber(subscriber, (value: T) => { + 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 From 5a49c1d3e856f5ac07310816b45c47fc3daa6bb9 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:05:46 -0500 Subject: [PATCH 006/138] refactor(filter): smaller - Uses OperatorSubscriber - Much smaller --- src/internal/operators/filter.ts | 55 +++++++------------------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/src/internal/operators/filter.ts b/src/internal/operators/filter.ts index 55a4d92681..2250722bdc 100644 --- a/src/internal/operators/filter.ts +++ b/src/internal/operators/filter.ts @@ -1,8 +1,8 @@ -import { Operator } from '../Operator'; 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, @@ -57,47 +57,14 @@ export function filter(predicate: (value: T, index: number) => boolean, 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)); + return lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let index = 0; + return source.subscribe(new OperatorSubscriber(subscriber, (value) => { + if (predicate.call(thisArg, value, index++)) { + subscriber.next(value); + } + })) + }); }; } - -class FilterOperator implements Operator { - constructor(private predicate: (value: T, index: number) => boolean, - private thisArg?: any) { - } - - call(subscriber: Subscriber, source: any): TeardownLogic { - return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg)); - } -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class FilterSubscriber extends Subscriber { - - count: number = 0; - - constructor(destination: Subscriber, - private predicate: (value: T, index: number) => boolean, - private thisArg: any) { - super(destination); - } - - // the try catch block below is left specifically for - // optimization and perf reasons. a tryCatcher is not necessary here. - protected _next(value: T) { - let result: any; - try { - result = this.predicate.call(this.thisArg, value, this.count++); - } catch (err) { - this.destination.error(err); - return; - } - if (result) { - this.destination.next(value); - } - } -} From df84881b57caace794aea5c7d980b2694a80b0cd Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:13:49 -0500 Subject: [PATCH 007/138] refactor(skip): Make implementation smaller - Smaller implementation - Uses OperatorSubscriber --- src/internal/operators/skip.ts | 44 ++++++++++------------------------ 1 file changed, 13 insertions(+), 31 deletions(-) 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++; + }) + ); + }); } From 38739ea710f159efe8e12a25b5f76b9163806ab6 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:29:51 -0500 Subject: [PATCH 008/138] refactor(take): smaller implementation - Removes runtime assertions --- spec/operators/take-spec.ts | 28 +--------------- src/internal/operators/take.ts | 61 ++++++++++++---------------------- 2 files changed, 22 insertions(+), 67 deletions(-) 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/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(); + } + } + }) + ); + }); } From f3e422e2c187af112226de54294db32f504f9441 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:39:50 -0500 Subject: [PATCH 009/138] refactor(takeUntil): smaller implementation - Adds supporting features to OperatorSubscriber --- src/internal/operators/OperatorSubscriber.ts | 14 +++++- src/internal/operators/takeUntil.ts | 52 +++++--------------- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index 59a5dc5fa0..e6a876c39e 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -2,7 +2,7 @@ import { Subscriber } from '../Subscriber'; export class OperatorSubscriber extends Subscriber { - constructor(destination: Subscriber, onNext?: (value: T) => void) { + constructor(destination: Subscriber, onNext?: (value: T) => void, onError?: (err: any) => void, onComplete?: () => void) { super(destination); if (onNext) { this._next = function (value: T) { @@ -13,5 +13,17 @@ export class OperatorSubscriber extends Subscriber { } }; } + if (onError) { + this._error = function (err) { + onError(err); + this.unsubscribe(); + }; + } + if (onComplete) { + this._complete = function () { + onComplete(); + this.unsubscribe(); + }; + } } } diff --git a/src/internal/operators/takeUntil.ts b/src/internal/operators/takeUntil.ts index 6bee857af4..6e56af426b 100644 --- a/src/internal/operators/takeUntil.ts +++ b/src/internal/operators/takeUntil.ts @@ -3,9 +3,12 @@ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction, TeardownLogic, 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 +49,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 From c9f75175f9843168f78f0959cba15de4840a98ae Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:47:16 -0500 Subject: [PATCH 010/138] refactor(takeWhile): smaller implementation --- spec/operators/takeWhile-spec.ts | 1 - src/internal/operators/takeWhile.ts | 72 ++++++----------------------- 2 files changed, 15 insertions(+), 58 deletions(-) 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/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(); + }) + ); + }); } From 49bcdd39b1309d097d13d460e84c9785222ec68d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 00:58:02 -0500 Subject: [PATCH 011/138] refactor(count): smaller implementation --- src/internal/operators/count.ts | 68 +++++++-------------------------- 1 file changed, 13 insertions(+), 55 deletions(-) diff --git a/src/internal/operators/count.ts b/src/internal/operators/count.ts index b9b4a8587d..035270db8e 100644 --- a/src/internal/operators/count.ts +++ b/src/internal/operators/count.ts @@ -1,8 +1,10 @@ +/** @pretter */ import { Observable } from '../Observable'; import { Operator } from '../Operator'; import { Observer, 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 +65,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(); + })) + }); } From e94f2cc1d54dad6ec8644384b06c401f0893c857 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:00:59 -0500 Subject: [PATCH 012/138] refactor(bufferTime): Use OperatorSubscriber instead --- src/internal/operators/bufferTime.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index 5efef01b39..c6561d3ae1 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -7,6 +7,7 @@ import { Subscription } from '../Subscription'; import { isScheduler } from '../util/isScheduler'; import { OperatorFunction, SchedulerAction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /* tslint:disable:max-line-length */ export function bufferTime(bufferTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction; @@ -162,9 +163,9 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper startBuffer(); } - const bufferTimeSubscriber = new BufferTimeSubscriber( + 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 @@ -181,6 +182,7 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper } } }, + undefined, () => { // The source completed, emit all of the active // buffers we have before we complete. @@ -191,6 +193,8 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper // Free up memory. bufferRecords = null; bufferTimeSubscriber?.unsubscribe(); + subscriber.complete(); + subscriber.unsubscribe(); } ); @@ -198,14 +202,3 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper }); }; } - -class BufferTimeSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void, protected onBeforeComplete: () => void) { - super(destination); - } - - _complete() { - this.onBeforeComplete(); - super._complete(); - } -} From c9ea9837f3372af63c2a4f320f4a4b5e01b3a667 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:03:52 -0500 Subject: [PATCH 013/138] refactor(dematerialize): smaller impl --- src/internal/operators/dematerialize.ts | 30 +++++-------------------- 1 file changed, 6 insertions(+), 24 deletions(-) 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); - } -} From b33923e7034f95a9ef7c74697f8d55763f22d7d0 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:11:09 -0500 Subject: [PATCH 014/138] refactor(distinct): smaller impl --- src/internal/operators/distinct.ts | 88 +++++++----------------------- 1 file changed, 20 insertions(+), 68 deletions(-) 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)); + }); } From eaa7dc1a808f9856f8e0a0975e28b621c27d4001 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:30:01 -0500 Subject: [PATCH 015/138] refactor(distinctUntilChanged): smaller implementation --- .../operators/distinctUntilChanged.ts | 85 ++++++------------- 1 file changed, 25 insertions(+), 60 deletions(-) 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; } From 05ac435f402ebf22734c95a70b01121792269336 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:35:15 -0500 Subject: [PATCH 016/138] refactor(every): smaller impl --- src/internal/operators/every.ts | 81 +++++++++++---------------------- 1 file changed, 27 insertions(+), 54 deletions(-) diff --git a/src/internal/operators/every.ts b/src/internal/operators/every.ts index 43d349460e..01668bf8e0 100644 --- a/src/internal/operators/every.ts +++ b/src/internal/operators/every.ts @@ -1,8 +1,10 @@ +/** @prettier */ import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { Observer, 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 +31,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(); + } + ) + ); + }); } From c894dd846f0325a2eab7f368968c3bce5926d41b Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:41:38 -0500 Subject: [PATCH 017/138] refactor(exhaust): smaller impl --- src/internal/operators/exhaust.ts | 65 ++++++++++--------------------- 1 file changed, 20 insertions(+), 45 deletions(-) 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 From 990bec5bbc6fb2d7d10339493eae3d1aa4c48389 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 01:50:22 -0500 Subject: [PATCH 018/138] refactor(exhaustMap): smaller impl --- src/internal/operators/exhaustMap.ts | 122 +++++++++------------------ 1 file changed, 42 insertions(+), 80 deletions(-) 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(); + } + ) + ); + }); } From a18d33ec01ae6e1b7f14f7145c25745fcae5cc9f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:18:59 -0500 Subject: [PATCH 019/138] refactor(expand): smaller impl --- spec/operators/expand-spec.ts | 2 +- src/internal/operators/expand.ts | 171 +++++++++++-------------------- 2 files changed, 61 insertions(+), 112 deletions(-) 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/src/internal/operators/expand.ts b/src/internal/operators/expand.ts index 0214fbf27c..72a64df9d5 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,54 @@ 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; + let active = 0; + const buffer: T[] = []; + let index = 0; + let isComplete = false; + + const trySub = () => { + while (0 < buffer.length && active < concurrent) { + const value = buffer.shift()!; + subscriber.next(value); + let inner: Observable; + try { + inner = from(project(value, index++)); + } catch (err) { + subscriber.error(err); + return; + } + active++; + const doSub = () => { + inner.subscribe( + new OperatorSubscriber(subscriber, next, undefined, () => { + --active === 0 && isComplete && buffer.length === 0 ? subscriber.complete() : trySub(); + }) + ); + }; + scheduler ? subscriber.add(scheduler.schedule(doSub)) : doSub(); } - } 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(); - } - } + }; + + const next = (value: T) => { + buffer.push(value); + trySub(); + }; + + source.subscribe( + new OperatorSubscriber(subscriber, next, undefined, () => { + isComplete = true; + active === 0 && subscriber.complete(); + }) + ); + }); } From 723b5c13e94bf6f0f6090ab18d0c443bbc038177 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:21:16 -0500 Subject: [PATCH 020/138] refactor(finalize): smaller impl --- src/internal/operators/finalize.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) 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); + }); } From adceb42ad253878766a67a482fe8f6dad835484c Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:36:19 -0500 Subject: [PATCH 021/138] refactor(find): smaller impl --- spec/operators/find-spec.ts | 6 -- src/internal/operators/find.ts | 111 +++++++++++----------------- src/internal/operators/findIndex.ts | 11 ++- 3 files changed, 52 insertions(+), 76 deletions(-) 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/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')); } From 59cb30a9228fe48086d676bba771fd2cc0b4085b Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:40:04 -0500 Subject: [PATCH 022/138] refactor(ignoreElements): smaller impl --- src/internal/operators/ignoreElements.ts | 29 +++++++----------------- 1 file changed, 8 insertions(+), 21 deletions(-) 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)); + }); } From 65f0e6929c7dc88da15a57818717c7e4eff21159 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:42:38 -0500 Subject: [PATCH 023/138] refactor(isEmpty): smaller impl --- src/internal/operators/isEmpty.ts | 54 ++++++++++++------------------- 1 file changed, 20 insertions(+), 34 deletions(-) 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(); + } + ) + ); + }); } From bf86f6893ad696f3d51e1928a9db54deb76bddc1 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:43:26 -0500 Subject: [PATCH 024/138] refactor(map): a little smaller still --- src/internal/operators/map.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/internal/operators/map.ts b/src/internal/operators/map.ts index fef2b04393..b8d8f08017 100644 --- a/src/internal/operators/map.ts +++ b/src/internal/operators/map.ts @@ -1,3 +1,4 @@ +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction } from '../types'; @@ -43,14 +44,15 @@ import { OperatorSubscriber } from './OperatorSubscriber'; * @name map */ export function map(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction { - return function mapOperation(source: Observable): Observable { - 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 OperatorSubscriber(subscriber, (value: T) => { - subscriber.next(project.call(thisArg, value, index++)); - })) + source.subscribe( + new OperatorSubscriber(subscriber, (value: T) => { + subscriber.next(project.call(thisArg, value, index++)); + }) + ); }); - }; -} \ No newline at end of file +} From 03d3c65cd361d82af58cd38e8545d7ed25dcf57d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:45:43 -0500 Subject: [PATCH 025/138] refactor(mapTo): smaller impl --- src/internal/operators/mapTo.ts | 41 ++++++--------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/src/internal/operators/mapTo.ts b/src/internal/operators/mapTo.ts index 5bc5edd880..03d1eda305 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,9 @@ 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; + source.subscribe(new OperatorSubscriber(subscriber, () => subscriber.next(value))); + }); } From 2023129533f46411644d32f81448d176ff167be5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:49:20 -0500 Subject: [PATCH 026/138] refactor(materialize): smaller impl --- src/internal/operators/materialize.ts | 58 ++++++++++----------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/src/internal/operators/materialize.ts b/src/internal/operators/materialize.ts index 2d50edea3d..ba149a2773 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,23 @@ 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(); + } + ) + ); + }); } From 22b40ed8e7003bba85a9f162a7d5c396de33fd09 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 02:55:19 -0500 Subject: [PATCH 027/138] refactor(pairwise): smaller impl --- src/internal/operators/pairwise.ts | 54 +++++++++--------------------- 1 file changed, 16 insertions(+), 38 deletions(-) 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; + }) + ); + }); } From cbc341f1598ebd04658a4d2e330c46cd1091e6b2 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 03:18:30 -0500 Subject: [PATCH 028/138] chore: golden file update for takeUntil actually accepting ObservableInput --- api_guard/dist/types/operators/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index ff43e7dbf5..87e6c416e0 100644 --- a/api_guard/dist/types/operators/index.d.ts +++ b/api_guard/dist/types/operators/index.d.ts @@ -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; @@ -306,7 +306,7 @@ export declare function tap(observer: PartialObserver): MonoTypeOperatorFu export declare function throttle(durationSelector: (value: T) => SubscribableOrPromise, config?: 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; From 2dbc7ecded69f7d904434a246dc355e2c1b45660 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 10:56:31 -0500 Subject: [PATCH 029/138] refactor(mergeMap): Use OperatorSubscriber instead --- src/internal/operators/mergeMap.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index f0990572d7..870b500874 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -8,6 +8,7 @@ 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>( @@ -129,13 +130,14 @@ export function mergeMap>( let innerSubs: Subscription; subscriber.add( (innerSubs = innerSource.subscribe( - new MergeMapSubscriber( + new OperatorSubscriber( subscriber, (innerValue) => { // INNER SOURCE NEXT // We got a value from the inner source, emit it from the result. subscriber.next(innerValue); }, + undefined, () => { // INNER SOURCE COMPLETE // Decrement the active count to ensure that the next time @@ -164,7 +166,7 @@ export function mergeMap>( let outerSubs: Subscription; outerSubs = source.subscribe( - new MergeMapSubscriber( + new OperatorSubscriber( subscriber, (value) => { // OUTER SOURCE NEXT @@ -174,6 +176,7 @@ export function mergeMap>( buffer.push(value); doInnerSub(); }, + undefined, () => { // OUTER SOURCE COMPLETE // We don't necessarily stop here. If have any pending inner subscriptions @@ -193,17 +196,6 @@ export function mergeMap>( }); } -// 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); - } -} - /** * @deprecated renamed. Use {@link mergeMap}. */ From cbd0ac0478730ec10172b57210e7d269d1ce62a2 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 11:53:59 -0500 Subject: [PATCH 030/138] fix(windowTime): Passing no creation interval will now properly open new window when old one closes This was a long-broken bit of functionality, but windowTime was so little used, I chose to ignore it. With this change, we could make `bufferTime` based off of `windowTime` and `toArray`. --- spec/operators/windowTime-spec.ts | 25 +- src/internal/operators/bufferTime.ts | 5 +- src/internal/operators/windowTime.ts | 328 +++++++++++---------------- 3 files changed, 153 insertions(+), 205 deletions(-) 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/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index c6561d3ae1..7a73207851 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -90,8 +90,8 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper 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 = []; @@ -200,5 +200,4 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper source.subscribe(bufferTimeSubscriber); }); - }; } diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index 4fcb0f18c5..cfc2ab7cf4 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -1,23 +1,26 @@ +/** @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 { 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'; + +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 +100,127 @@ 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> { + let scheduler: SchedulerLike = asyncScheduler; + + if (isScheduler(otherArgs[otherArgs.length - 1])) { + scheduler = otherArgs.pop() as SchedulerLike; + } + + 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; remove: () => void }) => { + const { window } = record; + window.complete(); + record.remove(); + if (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, + remove() { + this.subs.unsubscribe(); + if (windowRecords) { + const index = windowRecords.indexOf(this); + if (0 <= index) { + windowRecords.splice(index, 1); + } + } + }, + }; + windowRecords.push(record); + subscriber.next(window.asObservable()); + subs.add(scheduler.schedule(() => closeWindow(record), windowTimeSpan)); + } + }; + + if (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(); + if (!this.closed) { + subscriber.add(this.schedule(null, windowCreationInterval)); + } + }, windowCreationInterval) + ); + } else { + restartOnClose = true; } - } - } - - 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); -} + 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); + }; + + /** + * Called when the source completes or errors to teardown and clean up. + */ + const cleanup = () => { + windowRecords = null!; + subscriber.unsubscribe(); + }; + + const windowTimeSubscriber = new OperatorSubscriber( + subscriber, + (value: T) => { + loop((record) => { + const { window } = record; + window.next(value); + // If the window is over the max size, we need to close it. + maxWindowSize <= ++record.seen && closeWindow(record); + }); + }, + (err) => { + loop(({ window }) => window.error(err)); + subscriber.error(err); + cleanup(); + }, + () => { + loop(({ window }) => window.complete()); + subscriber.complete(); + cleanup(); + } + ); -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); + source.subscribe(windowTimeSubscriber); + }); } -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; + remove: () => void; } From 0656c5d1dfe8918e39b2e7006b51dcdb58aa8031 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 12:01:42 -0500 Subject: [PATCH 031/138] refactor(buffer): smaller impl --- src/internal/operators/buffer.ts | 56 +++++++++----------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/src/internal/operators/buffer.ts b/src/internal/operators/buffer.ts index 19fe5dfa7e..2deb710779 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,17 @@ 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[] = []; - - constructor(destination: Subscriber) { - super(destination); - } - - protected _next(value: T) { - this.buffer.push(value); - } - - notifyNext(): void { - const buffer = this.buffer; - this.buffer = []; - this.destination.next(buffer); - } + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let buffer: T[] = []; + source.subscribe(new OperatorSubscriber(subscriber, (value) => buffer.push(value))); + closingNotifier.subscribe( + new OperatorSubscriber(subscriber, () => { + const b = buffer; + buffer = []; + subscriber.next(b); + }) + ); + }); } From 4e1e5250b55fe7ab01865daf55a82fd6ff7f6336 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 13 Sep 2020 15:53:08 -0500 Subject: [PATCH 032/138] refactor(delayWhen): smaller impl --- src/internal/operators/delayWhen.ts | 210 ++++++++-------------------- 1 file changed, 59 insertions(+), 151 deletions(-) diff --git a/src/internal/operators/delayWhen.ts b/src/internal/operators/delayWhen.ts index 48628f89cc..83f8fd923d 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,50 @@ 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; + let index = 0; + let isComplete = false; + let active = 0; + const outerSubscriber = new OperatorSubscriber( + subscriber, + (value: T) => { + const durationNotifier = delayDurationSelector(value, index++); + active++; + let closed = false; + const notify = () => { + subscriber.next(value); + durationSubscriber?.unsubscribe(); + !closed && ((closed = true), --active === 0) && isComplete && subscriber.complete(); + }; + const durationSubscriber = new OperatorSubscriber( + subscriber, + notify, + undefined, + // TODO(benlesh): I'm inclined to say this is _incorrect_ behavior. + // A completion should not be a notification. Note the deprecation above + notify + ); + durationNotifier.subscribe(durationSubscriber); + }, + undefined, + () => { + isComplete = true; + active === 0 && subscriber.complete(); + outerSubscriber?.unsubscribe(); + } + ); + source.subscribe(outerSubscriber); + }); } From bbe90ab714465ff976cbb411e3da5de0032abf83 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 09:43:47 -0500 Subject: [PATCH 033/138] refactor(throttle): smaller impl --- src/internal/operators/throttle.ts | 143 ++++++++++------------------- 1 file changed, 50 insertions(+), 93 deletions(-) diff --git a/src/internal/operators/throttle.ts b/src/internal/operators/throttle.ts index 5bb43abf95..09ba255cf2 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,49 @@ 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) => { + let result: Observable; + try { + result = from(durationSelector(value)); + } catch (err) { + subscriber.error(err); + return; + } + subscriber.add((throttled = result.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)); + }) + ); + }); } From 2baf5bf2d5c21833e414df3ec22558ba0409b6fa Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 11:11:07 -0500 Subject: [PATCH 034/138] refactor(bufferToggle): smaller impl --- src/internal/operators/bufferToggle.ts | 199 ++++++++++--------------- 1 file changed, 80 insertions(+), 119 deletions(-) diff --git a/src/internal/operators/bufferToggle.ts b/src/internal/operators/bufferToggle.ts index 2666f353db..1057cf89b1 100644 --- a/src/internal/operators/bufferToggle.ts +++ b/src/internal/operators/bufferToggle.ts @@ -1,10 +1,12 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; 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 { from } from '../observable/from'; +import { OperatorSubscriber } from './OperatorSubscriber'; +import { noop } from '../util/noop'; /** * Buffers the source Observable values starting from an emission from @@ -56,123 +58,82 @@ export function bufferToggle( closingSelector: (value: O) => SubscribableOrPromise ): OperatorFunction { return function bufferToggleOperatorFunction(source: Observable) { - return lift(source, new BufferToggleOperator(openings, closingSelector)); - }; -} - -class BufferToggleOperator implements Operator { - - constructor(private openings: SubscribableOrPromise, - private closingSelector: (value: O) => SubscribableOrPromise) { - } - - call(subscriber: Subscriber, source: any): any { - return source.subscribe(new BufferToggleSubscriber(subscriber, this.openings, this.closingSelector)); - } -} - -interface BufferContext { - buffer: T[]; - subscription: Subscription; -} - -/** - * We need this JSDoc comment for affecting ESDoc. - * @ignore - * @extends {Ignored} - */ -class BufferToggleSubscriber extends 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); + return lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + const buffers: T[][] = []; + + const remove = (buffer: T[]) => { + const index = buffers.indexOf(buffer); + if (0 <= index) { + buffers.splice(index, 1); + } + }; + + // Subscribe to the openings notifier first + let openNotifier: Observable; + try { + openNotifier = from(openings); + } catch (err) { + subscriber.error(err); + return; } - } 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); - } - } + openNotifier.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 = () => { + const b = buffer; + remove(b); + subscriber.next(b); + closingSubscription.unsubscribe(); + }; + + // Get our closing notifier using the open value. + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector(openValue)); + } catch (err) { + subscriber.error(err); + return; + } + + // The line below will add the subscription to the parent subscriber *and* the closing subscription. + closingSubscription.add(closingNotifier.subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit))); + }, + undefined, + noop + ) + ); + + source.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(); + } + ) + ); + }); + }; } From 5cad8121b8d1aba15060516bdd5ecb92148a60f2 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 11:53:42 -0500 Subject: [PATCH 035/138] refactor(windowToggle): smaller impl --- src/internal/operators/OperatorSubscriber.ts | 13 +- src/internal/operators/windowToggle.ts | 236 +++++++------------ 2 files changed, 103 insertions(+), 146 deletions(-) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index e6a876c39e..6dc99abe58 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -2,7 +2,13 @@ import { Subscriber } from '../Subscriber'; export class OperatorSubscriber extends Subscriber { - constructor(destination: Subscriber, onNext?: (value: T) => void, onError?: (err: any) => void, onComplete?: () => void) { + 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) { @@ -26,4 +32,9 @@ export class OperatorSubscriber extends Subscriber { }; } } + + unsubscribe() { + !this.closed && this.onUnsubscribe?.(); + super.unsubscribe(); + } } diff --git a/src/internal/operators/windowToggle.ts b/src/internal/operators/windowToggle.ts index ea2376ad2f..96117de8fa 100644 --- a/src/internal/operators/windowToggle.ts +++ b/src/internal/operators/windowToggle.ts @@ -1,12 +1,13 @@ /** @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'; /** * Branch out the source Observable values as a nested Observable starting from @@ -56,152 +57,97 @@ 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(); - } + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + const windows: Subject[] = []; + + const remove = (window: Subject) => { + const index = windows.indexOf(window); + if (0 <= index) { + windows.splice(index, 1); + } + }; - 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(); + 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 = () => { + remove(window); + window.complete(); + closingSubscription.unsubscribe(); + }; + const closingSubscriber = new OperatorSubscriber(subscriber, closeWindow, handleError, closeWindow); + + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector(openValue)); + } catch (err) { + handleError(err); + return; + } + + subscriber.next(window.asObservable()); + + closingSubscription.add(closingNotifier.subscribe(closingSubscriber)); + }, + 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(); + } + } + ) + ); + }); } From 143ee68d0ea7d08327ba315b8b47dd27e189f052 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 12:34:34 -0500 Subject: [PATCH 036/138] refactor(bufferCount): Smaller impl --- src/internal/operators/bufferCount.ts | 167 +++++++++++--------------- src/internal/operators/timeout.ts | 88 ++++++++------ 2 files changed, 118 insertions(+), 137 deletions(-) diff --git a/src/internal/operators/bufferCount.ts b/src/internal/operators/bufferCount.ts index c39d8e3ecf..625d90865a 100644 --- a/src/internal/operators/bufferCount.ts +++ b/src/internal/operators/bufferCount.ts @@ -1,8 +1,9 @@ -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'; /** * Buffers the source Observable values until the size hits the maximum @@ -59,100 +60,70 @@ 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) { + const index = buffers.indexOf(buffer); + if (0 <= index) { + buffers.splice(index, 1); + } + 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/timeout.ts b/src/internal/operators/timeout.ts index 9563345d2c..cf1acc9612 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 { /** @@ -332,62 +333,71 @@ 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)); + each && 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!); }); }; } From 8ae89b19a095541eb3dfe6e6d9f26367486c435e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:25:58 -0500 Subject: [PATCH 037/138] fix(delay): proper handling of absolute time passed as delay - Makes impl smaller --- spec/operators/delay-spec.ts | 36 +++---- src/internal/operators/delay.ts | 169 ++++++++++++++------------------ 2 files changed, 90 insertions(+), 115 deletions(-) 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/src/internal/operators/delay.ts b/src/internal/operators/delay.ts index e1e620d865..75bf641e68 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,75 @@ 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)); - } -} - -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; - - private static dispatch(this: SchedulerAction>, state: DelayState): void { - const source = state.source; - const queue = source.queue; - const scheduler = state.scheduler; - const destination = state.destination; - - while (queue.length > 0 && queue[0].time - scheduler.now() <= 0) { - 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; - } - } +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; - constructor(protected destination: Subscriber, private delay: number, private scheduler: SchedulerLike) { - super(destination); - } + /** + * 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 _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(); - } -} + 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()) + ); + } -class DelayMessage { - constructor(public readonly time: number, public readonly value: T) {} + // 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) + ); + } + }, + undefined, + () => { + isComplete = true; + checkComplete(); + } + ) + ); + }); } From bf51b9d201a5e1069fbbc3ee26aa584930201fec Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:45:27 -0500 Subject: [PATCH 038/138] refactor(sample): smaller impl --- src/internal/operators/sample.ts | 67 +++++++++++--------------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/src/internal/operators/sample.ts b/src/internal/operators/sample.ts index 9622ac212b..28f2c75493 100644 --- a/src/internal/operators/sample.ts +++ b/src/internal/operators/sample.ts @@ -1,10 +1,11 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; import { lift } from '../util/lift'; import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '../innerSubscribe'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Emits the most recently emitted value from the source Observable whenever @@ -46,47 +47,25 @@ import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '.. * @name sample */ export function sample(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)); + }); } From 1efcf14ff101b0e859d8829e09a7b29dc598d0e2 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:48:26 -0500 Subject: [PATCH 039/138] refactor(sampleTime): derive from sample and interval --- src/internal/operators/sampleTime.ts | 58 ++++------------------------ 1 file changed, 8 insertions(+), 50 deletions(-) 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)); } From 7c3f4e1ac3e788b1a1f1f002400ef775ee7b318b Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:52:58 -0500 Subject: [PATCH 040/138] refactor(observeOn): smaller impl --- spec/operators/observeOn-spec.ts | 42 ---------------- src/internal/operators/observeOn.ts | 77 ++++++----------------------- 2 files changed, 15 insertions(+), 104 deletions(-) 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/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)) + ) + ); + }); } From defc5805adb3f39598b002cc0930ca023c4adfcd Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:56:30 -0500 Subject: [PATCH 041/138] refactor(subscribeOn): much smaller (facepalm edition) --- src/internal/operators/subscribeOn.ts | 61 +++------------------------ 1 file changed, 7 insertions(+), 54 deletions(-) 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)); + }); } From a8b0496bd6488e627d22964d168215601f7175d2 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 13:59:22 -0500 Subject: [PATCH 042/138] refactor(defaultIfEmpty): smaller impl --- src/internal/operators/defaultIfEmpty.ts | 59 +++++++++--------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/internal/operators/defaultIfEmpty.ts b/src/internal/operators/defaultIfEmpty.ts index e2dcf8dcff..e968a49042 100644 --- a/src/internal/operators/defaultIfEmpty.ts +++ b/src/internal/operators/defaultIfEmpty.ts @@ -1,8 +1,10 @@ +/** @prettier */ import { Operator } from '../Operator'; 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 +46,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(); + } + ) + ); + }); } From 70d04248146682e8af2064bb41f29342a4390b7e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 14:14:18 -0500 Subject: [PATCH 043/138] refactor(skipLast): smaller impl --- spec/operators/skipLast-spec.ts | 5 -- src/internal/operators/skipLast.ts | 83 +++++++++++------------------- 2 files changed, 30 insertions(+), 58 deletions(-) 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/src/internal/operators/skipLast.ts b/src/internal/operators/skipLast.ts index 1c4e8dd1ff..627998fdaa 100644 --- a/src/internal/operators/skipLast.ts +++ b/src/internal/operators/skipLast.ts @@ -4,6 +4,7 @@ import { ArgumentOutOfRangeError } from '../util/ArgumentOutOfRangeError'; import { Observable } from '../Observable'; import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Skip the last `count` values emitted by the source Observable. @@ -37,60 +38,36 @@ 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 { + 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 From cd32519e4a56eaa11d9de5659c232eb628e5e006 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 19:10:14 -0500 Subject: [PATCH 044/138] chore: update golden files --- api_guard/dist/types/operators/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index 87e6c416e0..9267e25a3f 100644 --- a/api_guard/dist/types/operators/index.d.ts +++ b/api_guard/dist/types/operators/index.d.ts @@ -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; @@ -304,7 +304,7 @@ 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, { leading, trailing }?: ThrottleConfig): MonoTypeOperatorFunction; @@ -332,9 +332,9 @@ 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>; From 12f9ff6d6e4168ba4e679055cbdaac0e7e725a2d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 19:41:51 -0500 Subject: [PATCH 045/138] refactor(groupBy): A little smaller still, add comments --- src/internal/operators/groupBy.ts | 167 +++++++++++++++++------------- 1 file changed, 97 insertions(+), 70 deletions(-) diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 4b043224a2..6dead63c0f 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -2,8 +2,9 @@ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; -import { OperatorFunction, Observer } from '../types'; +import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; export function groupBy( keySelector: (value: T) => value is K @@ -126,117 +127,143 @@ export function groupBy( 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>(); - let groupBySubscriber: GroupBySubscriber; - function createGroupedObservable(key: K, groupSubject: Subject) { - const result: any = new Observable((goSubscriber) => { - groupBySubscriber.count++; - const innerSub = groupSubject.subscribe(goSubscriber); - return () => { - innerSub.unsubscribe(); - if (--groupBySubscriber.count === 0 && groupBySubscriber.unsubAttempted) { - groupBySubscriber.unsubscribe(); - } - }; - }); - result.key = key; - return result; - } - - groupBySubscriber = new GroupBySubscriber( + // 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); - const element = elementSelector ? elementSelector(value) : value; let group = groups.get(key); if (!group) { + // Create our group subject group = subjectSelector ? subjectSelector() : new Subject(); groups.set(key, group); + + // 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); + if (durationSelector) { - const duration = durationSelector(grouped); - const durationSubscriber = new GroupDurationSubscriber( - group, + // A duration selector was provided, get the duration notifier + // and subscribe to it. + const durationNotifier = durationSelector(grouped); + + 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) ); - groupBySubscriber.add(duration.subscribe(durationSubscriber)); + + // Start our duration notifier. + groupBySourceSubscriber.add(durationNotifier.subscribe(durationSubscriber)); } } - group.next(element!); + // Send the value to our group. + group.next(elementSelector ? elementSelector(value) : value); }, (err) => { + // Error from the source. groups.forEach((group) => group.error(err)); subscriber.error(err); }, () => { + // Source completes. groups.forEach((group) => group.complete()); subscriber.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() ); - return source.subscribe(groupBySubscriber); - }); -} - -export interface RefCountSubscription { - count: number; - unsubscribe: () => void; - closed: boolean; - attemptedToUnsubscribe: boolean; -} - -class GroupBySubscriber extends Subscriber { - count = 0; - unsubAttempted = false; - - constructor( - destination: Subscriber, - protected onNext: (value: T) => void, - protected _error: (err: any) => void, - protected _complete: () => void - ) { - super(destination); - } - - // TODO: Unify this pattern elsewhere to reduce try-catching. - protected _next(value: T) { - try { - this.onNext(value); - } catch (err) { - this._error(err); - } - } + // Subscribe to the source + return source.subscribe(groupBySourceSubscriber); - unsubscribe() { - this.unsubAttempted = true; - if (this.count === 0) { - super.unsubscribe(); - } - } + /** + * 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. + if (--groupBySourceSubscriber.activeGroups === 0 && groupBySourceSubscriber.teardownAttempted) { + groupBySourceSubscriber.unsubscribe(); + } + }; + }); + result.key = key; + return result; + } + }); } -class GroupDurationSubscriber extends Subscriber { - constructor(destination: Observer, protected _next: (value: T) => void, private onUnsubscribe: () => void) { - super(destination); - } +/** + * 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 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) { - this.isStopped = true; - this.onUnsubscribe(); + 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)). + if (this.activeGroups === 0) { super.unsubscribe(); } } } +/** + * An observable of values that is the emitted by the result of a {@link groupBy} operator, + * contains a `key` property for the grouping. + */ export interface GroupedObservable extends Observable { + /** + * The key value for the grouped notifications. + */ readonly key: K; } From 4f7ea8f7c53b79faa2d65419ccb7e24a61c1f096 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 20:11:27 -0500 Subject: [PATCH 046/138] refactor(mergeScan): smaller impl --- src/internal/operators/mergeScan.ts | 165 +++++++++++++--------------- 1 file changed, 74 insertions(+), 91 deletions(-) diff --git a/src/internal/operators/mergeScan.ts b/src/internal/operators/mergeScan.ts index f7163279c6..496d3a0f78 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,78 @@ 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 - )); - } -} - -/** - * 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; - - constructor(protected destination: Subscriber, - private accumulator: (acc: R, value: T, index: number) => ObservableInput, - private acc: R, - private concurrent: number) { - super(destination); - } - - 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); - } - } - - private _innerSub(ish: any): void { - const innerSubscriber = new SimpleInnerSubscriber(this); - this.destination.add(innerSubscriber); - innerSubscribe(ish, innerSubscriber); - } +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; - 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(); - } + /** + * 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(); + } + }; - notifyNext(innerValue: R): void { - const { destination } = this; - this.acc = innerValue; - this.hasValue = true; - destination.next(innerValue); - } + const nextSourceValue = (value: T) => { + // If we're under our concurrency limit, go ahead and + // call the accumulator and subscribe to the result. + if (active < concurrent) { + 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)); + }, + undefined, + () => { + // The inner completed, decrement the number of actives. + active--; + // If we have anything in the buffer, process it, otherwise check to see if we can complete. + buffer.length ? nextSourceValue(buffer.shift()!) : checkComplete(); + } + ) + ); + } else { + // We're over our concurrency limit, push it onto the buffer to be + // process later when one of our inners completes. + buffer.push(value); + } + }; - 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(); - } - } + source.subscribe( + new OperatorSubscriber(subscriber, nextSourceValue, undefined, () => { + // Outer completed, make a note of it, and check to see if we can complete everything. + isComplete = true; + checkComplete(); + }) + ); + }); } From 0c2805d926273fd6ae4e7d1884d21ed0d553bdf5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 20:36:46 -0500 Subject: [PATCH 047/138] chore: update golden files --- api_guard/dist/types/index.d.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index fcc54fcd84..83cd171e67 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -275,10 +275,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; From 0146716348a5b48e28bd56587c4899ab1a071f51 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 20:37:04 -0500 Subject: [PATCH 048/138] refactor(debounce): smaller impl --- spec/operators/debounce-spec.ts | 6 +- src/internal/operators/debounce.ts | 143 +++++++++++------------------ 2 files changed, 58 insertions(+), 91 deletions(-) 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/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; + } + ) + ); + }); } From bda95552a483a648f73575002e6910a1b032619e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 20:57:15 -0500 Subject: [PATCH 049/138] refactor(withLatestFrom): smaller impl --- src/internal/operators/withLatestFrom.ts | 239 ++++++++++++++--------- 1 file changed, 150 insertions(+), 89 deletions(-) diff --git a/src/internal/operators/withLatestFrom.ts b/src/internal/operators/withLatestFrom.ts index 6de8a5e972..7b0c5cb9e4 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: Record = 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); - } + }); + }; } From 64b7b58b1bb276ccef39c6315d31cdd9fcaac07d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 21:04:42 -0500 Subject: [PATCH 050/138] refactor(debounceTime): smaller impl --- src/internal/operators/debounceTime.ts | 113 +++++++++---------------- 1 file changed, 41 insertions(+), 72 deletions(-) diff --git a/src/internal/operators/debounceTime.ts b/src/internal/operators/debounceTime.ts index 4e8b073aa1..176e4692b7 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,42 @@ 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; - - 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; - } - } -} +export function debounceTime(dueTime: number, scheduler: SchedulerLike = asyncScheduler): MonoTypeOperatorFunction { + return (source: Observable) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let hasValue = false; + let lastValue: T | null = null; + let debounceSubscription: Subscription | null = null; -function dispatchNext(subscriber: DebounceTimeSubscriber) { - subscriber.debouncedNext(); + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => { + debounceSubscription?.unsubscribe(); + hasValue = true; + lastValue = value; + subscriber.add( + (debounceSubscription = scheduler.schedule(() => { + debounceSubscription = null; + if (hasValue) { + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + } + }, dueTime)) + ); + }, + undefined, + () => { + if (hasValue) { + subscriber.next(lastValue!); + lastValue = null; + } + subscriber.complete(); + } + ) + ); + }); } From ee64a89fd5a33af787af4fec23625e83d9c2a92f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 22:01:49 -0500 Subject: [PATCH 051/138] refactor(windowWhen): smaller impl --- src/internal/operators/windowWhen.ts | 96 ++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/src/internal/operators/windowWhen.ts b/src/internal/operators/windowWhen.ts index 5f55ab5259..c43af3077c 100644 --- a/src/internal/operators/windowWhen.ts +++ b/src/internal/operators/windowWhen.ts @@ -1,11 +1,15 @@ +/** @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 { 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,15 +54,82 @@ 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)); - }; +export function windowWhen(closingSelector: () => ObservableInput): OperatorFunction> { + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let window: Subject; + let closingSubscriber: Subscriber; + + /** + * 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); + }; + + /** + * 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(); + + // Close our window before starting a new one. + window?.complete(); + + // Start the new window. + window = new Subject(); + subscriber.next(window.asObservable()); + + // Get our closing notifier. + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector()); + } catch (err) { + handleError(err); + return; + } + + // 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))); + }; + + // Start the first window. + openWindow(); + + // 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(); + } + ) + ); + }); } class WindowOperator implements Operator> { - constructor(private closingSelector: () => Observable) { - } + constructor(private closingSelector: () => Observable) {} call(subscriber: Subscriber>, source: any): any { return source.subscribe(new WindowSubscriber(subscriber, this.closingSelector)); @@ -74,15 +145,12 @@ class WindowSubscriber extends ComplexOuterSubscriber { private window: Subject | undefined; private closingNotification: Subscription | undefined; - constructor(protected destination: Subscriber>, - private closingSelector: () => Observable) { + constructor(protected destination: Subscriber>, private closingSelector: () => Observable) { super(destination); this.openWindow(); } - notifyNext(_outerValue: T, _innerValue: any, - _outerIndex: number, - innerSub: ComplexInnerSubscriber): void { + notifyNext(_outerValue: T, _innerValue: any, _outerIndex: number, innerSub: ComplexInnerSubscriber): void { this.openWindow(innerSub); } @@ -127,7 +195,7 @@ class WindowSubscriber extends ComplexOuterSubscriber { prevWindow.complete(); } - const window = this.window = new Subject(); + const window = (this.window = new Subject()); this.destination.next(window); let closingNotifier; @@ -139,6 +207,6 @@ class WindowSubscriber extends ComplexOuterSubscriber { this.window.error(e); return; } - this.add(this.closingNotification = innerSubscribe(closingNotifier, new ComplexInnerSubscriber(this, undefined, 0))); + this.add((this.closingNotification = innerSubscribe(closingNotifier, new ComplexInnerSubscriber(this, undefined, 0)))); } } From 5621095301d20bf8c3580e5e9b0c49d2fe3aa70f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 14 Sep 2020 22:54:19 -0500 Subject: [PATCH 052/138] refactor(windowCount): smaller impl --- src/internal/operators/OperatorSubscriber.ts | 2 +- src/internal/operators/windowCount.ts | 128 +++++++++---------- 2 files changed, 59 insertions(+), 71 deletions(-) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index 6dc99abe58..01cf30b5af 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -15,7 +15,7 @@ export class OperatorSubscriber extends Subscriber { try { onNext?.(value); } catch (err) { - this._error(err); + this.error(err); } }; } diff --git a/src/internal/operators/windowCount.ts b/src/internal/operators/windowCount.ts index f147d171f3..8df8d6728d 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]); - } + const startEvery = startWindowEvery > 0 ? startWindowEvery : windowSize; - 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; + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let windows = [new Subject()]; + let starts: number[] = []; + let count = 0; - 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); - } - } + // Open the first window. + subscriber.next(windows[0].asObservable()); - protected _error(err: any) { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift()!.error(err); - } - } - this.destination.error(err); - } + const outerSubscriber = 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(); + } - protected _complete() { - const windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift()!.complete(); - } - } - this.destination.complete(); - } + // 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!; + } + ); - unsubscribe() { - if (!this.closed) { - this.count = 0; - this.windows = null!; - super.unsubscribe(); - } - } + source.subscribe(outerSubscriber); + }); } From c914c53c3794c462c122c678cdeff5d366ae0837 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 07:57:29 -0500 Subject: [PATCH 053/138] refactor(bufferWhen): smaller impl --- api_guard/dist/types/operators/index.d.ts | 4 +- src/internal/operators/bufferWhen.ts | 57 +++++++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index 9267e25a3f..8ddec76238 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>; @@ -336,7 +336,7 @@ export declare function windowTime(windowTimeSpan: number, windowCreationInte 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/src/internal/operators/bufferWhen.ts b/src/internal/operators/bufferWhen.ts index d724ac8744..0fe35c4391 100644 --- a/src/internal/operators/bufferWhen.ts +++ b/src/internal/operators/bufferWhen.ts @@ -3,9 +3,11 @@ 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,10 +50,55 @@ 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)); - }; +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; + let isComplete = false; + + const openBuffer = () => { + closingSubscriber?.unsubscribe(); + + const b = buffer; + buffer = []; + b && subscriber.next(b); + + let closingNotifier: Observable; + try { + closingNotifier = from(closingSelector()); + } catch (err) { + subscriber.error(err); + return; + } + + closingNotifier.subscribe( + (closingSubscriber = new OperatorSubscriber(subscriber, openBuffer, undefined, () => { + isComplete ? subscriber.complete() : openBuffer(); + })) + ); + }; + + openBuffer(); + + source.subscribe( + new OperatorSubscriber( + subscriber, + (value) => buffer?.push(value), + undefined, + () => { + isComplete = true; + buffer && subscriber.next(buffer); + subscriber.complete(); + }, + () => { + buffer = null!; + closingSubscriber = null!; + } + ) + ); + }); } class BufferWhenOperator implements Operator { From 9f63426df181bdc70f5d60923d7cac707540a445 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 08:11:55 -0500 Subject: [PATCH 054/138] refactor(single): smaller impl --- src/internal/operators/single.ts | 68 +++++++++++--------------------- 1 file changed, 24 insertions(+), 44 deletions(-) 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()) + } + })) + }); } From 7523e7757e509a813267389734a62947fa4f77e6 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 08:34:36 -0500 Subject: [PATCH 055/138] refactor(window): smaller impl --- src/internal/operators/window.ts | 103 ++++++++++--------------------- 1 file changed, 31 insertions(+), 72 deletions(-) diff --git a/src/internal/operators/window.ts b/src/internal/operators/window.ts index 507e3859fd..9f9fe1bf82 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,37 @@ 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); - } - - protected _error(err: any): void { - this.window.error(err); - this.destination.error(err); - } + return (source: Observable) => + lift(source, function (this: Subscriber>, source: Observable) { + const subscriber = this; + let window = new Subject(); - protected _complete(): void { - this.window.complete(); - this.destination.complete(); - } + subscriber.next(window.asObservable()); - unsubscribe() { - if (!this.closed) { - this.window = null!; - super.unsubscribe(); - } - } + const windowSubscribe = (source: Observable, next: (value: any) => void) => + source.subscribe( + new OperatorSubscriber( + subscriber, + next, + (err: any) => { + window.error(err); + subscriber.error(err); + }, + () => { + window.complete(); + subscriber.complete(); + }, + () => { + window?.unsubscribe(); + window = null!; + } + ) + ); - private openWindow(): void { - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - const destination = this.destination; - const newWindow = (this.window = new Subject()); - destination.next(newWindow); - } + windowSubscribe(source, (value) => window.next(value)); + windowSubscribe(windowBoundaries, () => { + window.complete(); + subscriber.next((window = new Subject())); + }); + }); } From 8a2a08c041e0e00905c51a30582b9a530de22803 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 08:49:03 -0500 Subject: [PATCH 056/138] refactor(audit): smaller impl --- src/internal/operators/audit.ts | 105 ++++++++++---------------------- 1 file changed, 32 insertions(+), 73 deletions(-) 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)) + ); + } + }) + ); + }); } From b32d04cbbafd1e5f4708f22270e336dcfeb8c900 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 09:06:54 -0500 Subject: [PATCH 057/138] refactor(throttleTime): using OperatorSubscriber --- src/internal/operators/throttleTime.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) 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(); - } - } -} From 02d32a8938c0abdbebff55f41ff906a6e6addd23 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 09:29:25 -0500 Subject: [PATCH 058/138] refactor(switchMap): smaller impl --- src/internal/operators/switchMap.ts | 138 ++++++++++++---------------- 1 file changed, 60 insertions(+), 78 deletions(-) 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(); + } + ) + ); + }); } From bd1ecb96ec59a38c0dd8a36fe1fd512470d5b40a Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 09:59:34 -0500 Subject: [PATCH 059/138] refactor: remove unused imports --- src/internal/operators/count.ts | 3 +-- src/internal/operators/defaultIfEmpty.ts | 1 - src/internal/operators/every.ts | 3 +-- src/internal/operators/mergeMap.ts | 2 -- src/internal/operators/repeatWhen.ts | 4 +--- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/internal/operators/count.ts b/src/internal/operators/count.ts index 035270db8e..2dae32db12 100644 --- a/src/internal/operators/count.ts +++ b/src/internal/operators/count.ts @@ -1,7 +1,6 @@ /** @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'; diff --git a/src/internal/operators/defaultIfEmpty.ts b/src/internal/operators/defaultIfEmpty.ts index e968a49042..9e4339a0d8 100644 --- a/src/internal/operators/defaultIfEmpty.ts +++ b/src/internal/operators/defaultIfEmpty.ts @@ -1,5 +1,4 @@ /** @prettier */ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; diff --git a/src/internal/operators/every.ts b/src/internal/operators/every.ts index 01668bf8e0..852b5e0bd3 100644 --- a/src/internal/operators/every.ts +++ b/src/internal/operators/every.ts @@ -1,8 +1,7 @@ /** @prettier */ -import { Operator } from '../Operator'; 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'; diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index 870b500874..1a66377b3c 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -1,13 +1,11 @@ /** @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 */ diff --git a/src/internal/operators/repeatWhen.ts b/src/internal/operators/repeatWhen.ts index 2820cc88f3..ff7a907d2b 100644 --- a/src/internal/operators/repeatWhen.ts +++ b/src/internal/operators/repeatWhen.ts @@ -1,12 +1,10 @@ -import { Operator } from '../Operator'; 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'; /** * Returns an Observable that mirrors the source Observable with the exception of a `complete`. If the source From c3f92a58af6de2a8949be6014915737c3332c5ad Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 10:04:18 -0500 Subject: [PATCH 060/138] refactor(scan): even smaller --- src/internal/operators/scan.ts | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/internal/operators/scan.ts b/src/internal/operators/scan.ts index 5c907458c3..2540c7579a 100644 --- a/src/internal/operators/scan.ts +++ b/src/internal/operators/scan.ts @@ -3,6 +3,7 @@ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; import { OperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; 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; @@ -103,35 +104,11 @@ export function scan(accumulator: (acc: V | A | S, value: V, index: num let state: any = hasSeed ? seed! : null!; let index = 0; source.subscribe( - new ScanSubscriber(subscriber, (value) => { + new OperatorSubscriber(subscriber, (value) => { const i = index++; - if (!hasState) { - // If a seed was not passed, we use the first value from the source - // as the initial state. That means we also pass it through, and the - // accumulator (reducer) does not get executed. - hasState = true; - state = value; - } else { - // Otherwise, if we have a seed, or we already have state, we try - // to execute the accumulator, and we handle the error appropriately. - try { - state = accumulator(state, value, i); - } catch (err) { - // An error occurred in the user-provided function, forward it - // to the consumer via error notification. - subscriber.error(err); - return; - } - } - subscriber.next(state); + subscriber.next((state = hasState ? accumulator(state, value, i) : ((hasState = true), value))); }) ); }); }; } - -class ScanSubscriber extends Subscriber { - constructor(destination: Subscriber, protected _next: (value: T) => void) { - super(destination); - } -} From c092714c0fc92e4d1f22c794bb8c2ba46223e4cb Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 10:13:07 -0500 Subject: [PATCH 061/138] refactor: remove unused imports --- src/internal/operators/skipLast.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/internal/operators/skipLast.ts b/src/internal/operators/skipLast.ts index 627998fdaa..7c644d8ebf 100644 --- a/src/internal/operators/skipLast.ts +++ b/src/internal/operators/skipLast.ts @@ -1,8 +1,6 @@ -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'; From 9b9d4fd115151d786948ef875feda53f842ae774 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 10:13:50 -0500 Subject: [PATCH 062/138] refactor(skipUntil): smaller impl --- src/internal/operators/skipUntil.ts | 74 +++++++++++------------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/src/internal/operators/skipUntil.ts b/src/internal/operators/skipUntil.ts index a08c5814df..5d21c2832d 100644 --- a/src/internal/operators/skipUntil.ts +++ b/src/internal/operators/skipUntil.ts @@ -1,10 +1,11 @@ -import { Operator } from '../Operator'; +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { MonoTypeOperatorFunction, TeardownLogic, ObservableInput } from '../types'; -import { Subscription } from '../Subscription'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, innerSubscribe, SimpleInnerSubscriber } from '../innerSubscribe'; +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 +46,30 @@ 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) => + lift(source, function (this: Subscriber, source: Observable) { + const subscriber = this; + let taking = false; - protected _next(value: T) { - if (this.isTaking) { - super._next(value); - } - } + let skipNotifier: Observable; + try { + skipNotifier = from(notifier); + } catch (err) { + subscriber.error(err); + return; + } + const skipSubscriber = new OperatorSubscriber( + subscriber, + () => { + skipSubscriber?.unsubscribe(); + taking = true; + }, + undefined, + noop + ); - notifyNext(): void { - this.isTaking = true; - if (this.innerSubscription) { - this.innerSubscription.unsubscribe(); - } - } + skipNotifier.subscribe(skipSubscriber); - notifyComplete() { - /* do nothing */ - } + source.subscribe(new OperatorSubscriber(subscriber, (value) => taking && subscriber.next(value))); + }); } From 7a207c5a0b63f5beb6d184c6be61507466b8bd1c Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 11:39:19 -0500 Subject: [PATCH 063/138] chore: Remove unused types --- src/internal/operators/bufferWhen.ts | 84 ---------------------------- 1 file changed, 84 deletions(-) diff --git a/src/internal/operators/bufferWhen.ts b/src/internal/operators/bufferWhen.ts index 0fe35c4391..c077eac6e7 100644 --- a/src/internal/operators/bufferWhen.ts +++ b/src/internal/operators/bufferWhen.ts @@ -1,11 +1,8 @@ /** @prettier */ -import { Operator } from '../Operator'; import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; -import { Subscription } from '../Subscription'; 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'; @@ -100,84 +97,3 @@ export function bufferWhen(closingSelector: () => ObservableInput): Oper ); }); } - -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(); - } - - notifyComplete(): void { - if (this.subscribing) { - this.complete(); - } else { - this.openBuffer(); - } - } - - openBuffer() { - let { closingSubscription } = this; - - if (closingSubscription) { - this.remove(closingSubscription); - closingSubscription.unsubscribe(); - } - - const buffer = this.buffer; - if (this.buffer) { - this.destination.next(buffer); - } - - this.buffer = []; - - 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; - } -} From 9559dce96d60cc38fdbeb8022e28fde7243df774 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:03:28 -0500 Subject: [PATCH 064/138] refactor(repeatWhen): smaller impl --- src/internal/operators/repeatWhen.ts | 170 +++++++++++++-------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/internal/operators/repeatWhen.ts b/src/internal/operators/repeatWhen.ts index ff7a907d2b..0426a4deb8 100644 --- a/src/internal/operators/repeatWhen.ts +++ b/src/internal/operators/repeatWhen.ts @@ -1,3 +1,4 @@ +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; @@ -5,6 +6,7 @@ import { Subscription } from '../Subscription'; import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; +import { OperatorSubscriber } from './OperatorSubscriber'; /** * Returns an Observable that mirrors the source Observable with the exception of a `complete`. If the source @@ -36,96 +38,94 @@ import { lift } from '../util/lift'; * @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; + const subscription = new Subscription(); + 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; + /** + * 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`. + subscription.add( + 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(); + } + ) + ) + ); } - 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(); - } - }, + 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); - } - }; + 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(); + } else { + subscription.add(innerSub); + } + }; - // Start the subscription - subscribeForRepeatWhen(); + // Start the subscription + subscribeForRepeatWhen(); - return subscription; - }); + return subscription; + }); } From f278105d17134ae2c4351481700f6d50a2187da5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:20:46 -0500 Subject: [PATCH 065/138] refactor(catchError): smaller impl --- src/internal/operators/catchError.ts | 63 +++++++++++++--------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/internal/operators/catchError.ts b/src/internal/operators/catchError.ts index f793d1b3de..69ecc7b674 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,46 @@ 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) => { + // NOTE: The try/catch is here instead of OperatorSubscriber because + // this is the only operator that requires a try/catch in the error handler + // adding it to OperatorSubscriber would add that weight to to something + // used by all other operators just to make this one operator smaller. + try { + handledResult = from(selector(err, catchError(selector)(source))); + } catch (err) { + subscriber.error(err); + return; + } + + 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(); - } -} From 78207a6df7d64001d58be8eaac054bae3eeffce3 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:24:52 -0500 Subject: [PATCH 066/138] refactor(sequenceEqual): smaller impl --- src/internal/operators/sequenceEqual.ts | 60 +++++++------------------ 1 file changed, 17 insertions(+), 43 deletions(-) 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); - } -} From 473c9c3350564422ac0f2fe75bf12c4613aa099e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:38:03 -0500 Subject: [PATCH 067/138] refactor(retryWhen): smaller impl --- src/internal/operators/OperatorSubscriber.ts | 12 +++- src/internal/operators/retryWhen.ts | 72 +++++++------------- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index 01cf30b5af..6ba19748af 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -21,13 +21,21 @@ export class OperatorSubscriber extends Subscriber { } if (onError) { this._error = function (err) { - onError(err); + try { + onError(err); + } catch (err) { + this.destination.error(err); + } this.unsubscribe(); }; } if (onComplete) { this._complete = function () { - onComplete(); + try { + onComplete(); + } catch (err) { + this.destination.error(err); + } this.unsubscribe(); }; } diff --git a/src/internal/operators/retryWhen.ts b/src/internal/operators/retryWhen.ts index 7695398a5d..0a353624bd 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,37 @@ 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; + wrappedLift(source, (subscriber: Subscriber, source: Observable) => { const subscription = new Subscription(); 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 - // 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 = source.subscribe( + new OperatorSubscriber(subscriber, undefined, (err) => { + if (!errors$) { + errors$ = new Subject(); + subscription.add( + 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. + 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. From b612901bd088608aa8d5e0d67c234e07b1e46cae Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:38:20 -0500 Subject: [PATCH 068/138] refactor(catchError): even smaller --- src/internal/operators/catchError.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/internal/operators/catchError.ts b/src/internal/operators/catchError.ts index 69ecc7b674..77f3ece3e0 100644 --- a/src/internal/operators/catchError.ts +++ b/src/internal/operators/catchError.ts @@ -119,17 +119,7 @@ export function catchError>( innerSub = source.subscribe( new OperatorSubscriber(subscriber, undefined, (err) => { - // NOTE: The try/catch is here instead of OperatorSubscriber because - // this is the only operator that requires a try/catch in the error handler - // adding it to OperatorSubscriber would add that weight to to something - // used by all other operators just to make this one operator smaller. - try { - handledResult = from(selector(err, catchError(selector)(source))); - } catch (err) { - subscriber.error(err); - return; - } - + handledResult = from(selector(err, catchError(selector)(source))); if (innerSub) { innerSub.unsubscribe(); innerSub = null; From f81e274fc7c55dfd21026aa7da84795c28cdd05f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:41:57 -0500 Subject: [PATCH 069/138] refactor: add wrappedLift (should have been a few commits ago, oops) --- src/internal/util/lift.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/internal/util/lift.ts b/src/internal/util/lift.ts index e9fbf7c316..3cdd12d353 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,6 +25,25 @@ export function lift(source: Observable, operator?: Operator): Ob throw new TypeError('Unable to lift unknown Observable type'); } +/** + * 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 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); + } + }); +} + // 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. From cf5c3ef72ea76942da0bc75964704455e3410df5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:43:28 -0500 Subject: [PATCH 070/138] refactor(throttle): a bit smaller --- src/internal/operators/throttle.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/internal/operators/throttle.ts b/src/internal/operators/throttle.ts index 09ba255cf2..c656a0f61b 100644 --- a/src/internal/operators/throttle.ts +++ b/src/internal/operators/throttle.ts @@ -80,16 +80,10 @@ export function throttle( trailing && send(); }; - const throttle = (value: T) => { - let result: Observable; - try { - result = from(durationSelector(value)); - } catch (err) { - subscriber.error(err); - return; - } - subscriber.add((throttled = result.subscribe(new OperatorSubscriber(subscriber, throttlingDone, undefined, throttlingDone)))); - }; + const throttle = (value: T) => + (throttled = from(durationSelector(value)).subscribe( + new OperatorSubscriber(subscriber, throttlingDone, undefined, throttlingDone) + )); const send = () => { if (hasValue) { From e539538476b253bac3ebe03b345dbbfd85d3a3e4 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:48:28 -0500 Subject: [PATCH 071/138] refactor(skipUntil): even smaller --- src/internal/operators/skipUntil.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/internal/operators/skipUntil.ts b/src/internal/operators/skipUntil.ts index 5d21c2832d..5ad08f0f99 100644 --- a/src/internal/operators/skipUntil.ts +++ b/src/internal/operators/skipUntil.ts @@ -1,8 +1,7 @@ /** @prettier */ -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { MonoTypeOperatorFunction } from '../types'; -import { lift } from '../util/lift'; +import { wrappedLift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; import { from } from '../observable/from'; import { noop } from '../util/noop'; @@ -47,17 +46,9 @@ import { noop } from '../util/noop'; */ export function skipUntil(notifier: Observable): MonoTypeOperatorFunction { return (source: Observable) => - lift(source, function (this: Subscriber, source: Observable) { - const subscriber = this; + wrappedLift(source, (subscriber, liftedSource) => { let taking = false; - let skipNotifier: Observable; - try { - skipNotifier = from(notifier); - } catch (err) { - subscriber.error(err); - return; - } const skipSubscriber = new OperatorSubscriber( subscriber, () => { @@ -68,8 +59,8 @@ export function skipUntil(notifier: Observable): MonoTypeOperatorFunctio noop ); - skipNotifier.subscribe(skipSubscriber); + from(notifier).subscribe(skipSubscriber); - source.subscribe(new OperatorSubscriber(subscriber, (value) => taking && subscriber.next(value))); + liftedSource.subscribe(new OperatorSubscriber(subscriber, (value) => taking && subscriber.next(value))); }); } From f812780d50c8bfe57e0530ad7e90f9ac14431a2a Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:48:43 -0500 Subject: [PATCH 072/138] refactor(expand): even smaller --- src/internal/operators/expand.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/internal/operators/expand.ts b/src/internal/operators/expand.ts index 72a64df9d5..b53ac603ef 100644 --- a/src/internal/operators/expand.ts +++ b/src/internal/operators/expand.ts @@ -81,7 +81,7 @@ export function expand( lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; let active = 0; - const buffer: T[] = []; + const buffer: (T | R)[] = []; let index = 0; let isComplete = false; @@ -89,13 +89,8 @@ export function expand( while (0 < buffer.length && active < concurrent) { const value = buffer.shift()!; subscriber.next(value); - let inner: Observable; - try { - inner = from(project(value, index++)); - } catch (err) { - subscriber.error(err); - return; - } + // TODO: Correct the types here. `project` could be R or T. + const inner = from(project(value as any, index++)); active++; const doSub = () => { inner.subscribe( @@ -108,7 +103,7 @@ export function expand( } }; - const next = (value: T) => { + const next = (value: T | R) => { buffer.push(value); trySub(); }; From 4081eae44aa3cc5264b5b4389c45dd974d63cb4f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 12:48:58 -0500 Subject: [PATCH 073/138] refactor(bufferToggle): even smaller --- src/internal/operators/bufferToggle.ts | 29 +++++--------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/internal/operators/bufferToggle.ts b/src/internal/operators/bufferToggle.ts index 1057cf89b1..1800414394 100644 --- a/src/internal/operators/bufferToggle.ts +++ b/src/internal/operators/bufferToggle.ts @@ -1,9 +1,8 @@ /** @prettier */ -import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subscription } from '../Subscription'; 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'; @@ -58,8 +57,7 @@ export function bufferToggle( closingSelector: (value: O) => SubscribableOrPromise ): OperatorFunction { return function bufferToggleOperatorFunction(source: Observable) { - return lift(source, function (this: Subscriber, source: Observable) { - const subscriber = this; + return wrappedLift(source, (subscriber, liftedSource) => { const buffers: T[][] = []; const remove = (buffer: T[]) => { @@ -70,15 +68,7 @@ export function bufferToggle( }; // Subscribe to the openings notifier first - let openNotifier: Observable; - try { - openNotifier = from(openings); - } catch (err) { - subscriber.error(err); - return; - } - - openNotifier.subscribe( + from(openings).subscribe( new OperatorSubscriber( subscriber, (openValue) => { @@ -98,24 +88,15 @@ export function bufferToggle( closingSubscription.unsubscribe(); }; - // Get our closing notifier using the open value. - let closingNotifier: Observable; - try { - closingNotifier = from(closingSelector(openValue)); - } catch (err) { - subscriber.error(err); - return; - } - // The line below will add the subscription to the parent subscriber *and* the closing subscription. - closingSubscription.add(closingNotifier.subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit))); + closingSubscription.add(from(closingSelector(openValue)).subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit))); }, undefined, noop ) ); - source.subscribe( + liftedSource.subscribe( new OperatorSubscriber( subscriber, (value) => { From d51cc37b28d34dab8c5eb2295c2fefc2feae1dad Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 13:04:33 -0500 Subject: [PATCH 074/138] refactor(skipWhile): smaller impl --- src/internal/operators/skipWhile.ts | 59 ++++++----------------------- 1 file changed, 12 insertions(+), 47 deletions(-) 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)) + ); + }); } From 26b5462cd4e0347a1e0c3265977d65838ce7d1c4 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 13:08:12 -0500 Subject: [PATCH 075/138] refactor(throwIfEmpty): smaller impl --- src/internal/operators/throwIfEmpty.ts | 59 +++++++++----------------- 1 file changed, 19 insertions(+), 40 deletions(-) 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() { From f5c3f692627d4b65452a8b5f29e90a9c2fa372a8 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 13:18:42 -0500 Subject: [PATCH 076/138] refactor(takeLast): smaller impl --- spec/operators/takeLast-spec.ts | 20 ------- src/internal/operators/takeLast.ts | 88 +++++++++--------------------- 2 files changed, 26 insertions(+), 82 deletions(-) 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/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!; + } + ) + ); + }); } From e12556977960494eb687be0bd651ae96aebaab9c Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 13:26:43 -0500 Subject: [PATCH 077/138] refactor(mergeMap): smaller still --- src/internal/operators/mergeMap.ts | 57 +++++++++++------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index 1a66377b3c..c451727ad8 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -105,6 +105,12 @@ 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 @@ -114,50 +120,31 @@ export function mergeMap>( 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( + from(project(value, index++)).subscribe( new OperatorSubscriber( subscriber, - (innerValue) => { - // INNER SOURCE NEXT - // We got a value from the inner source, emit it from the result. - subscriber.next(innerValue); - }, + // INNER SOURCE NEXT + // We got a value from the inner source, emit it from the result. + (innerValue) => subscriber.next(innerValue), 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 (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(); + // 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 && doInnerSub(); + // Check to see if we can complete, and complete if so. + checkComplete(); } ) - )) + ) ); } }; @@ -181,11 +168,9 @@ export function mergeMap>( // 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(); } From b00427d8dc80941bd673c85d4cc76e6e9030b3f9 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 13:59:14 -0500 Subject: [PATCH 078/138] refactor(refCount): smaller impl --- src/internal/operators/refCount.ts | 125 +++++++++++------------------ 1 file changed, 49 insertions(+), 76 deletions(-) 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; } From 51d6e2188340cd730fef275580497787c0c782c0 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 14:46:19 -0500 Subject: [PATCH 079/138] refactor(ConnectableObservable/multicast): get rid of Object.create, make it smaller. --- .../observable/ConnectableObservable.ts | 91 ++++++------------- src/internal/operators/multicast.ts | 64 ++++++------- src/internal/util/lift.ts | 2 +- 3 files changed, 64 insertions(+), 93 deletions(-) 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/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/util/lift.ts b/src/internal/util/lift.ts index 3cdd12d353..ba27dcdedb 100644 --- a/src/internal/util/lift.ts +++ b/src/internal/util/lift.ts @@ -70,6 +70,6 @@ export function stankyLift(source: Observable, liftedSource: Observable['lift'] } { +export function hasLift(source: any): source is { lift: InstanceType['lift'] } { return source && typeof source.lift === 'function'; } From 54913109072190e76d28d4c971f228f1ff4c1292 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 15:05:52 -0500 Subject: [PATCH 080/138] refactor(windowWhen): remove unused types --- src/internal/operators/windowWhen.ts | 86 ---------------------------- 1 file changed, 86 deletions(-) diff --git a/src/internal/operators/windowWhen.ts b/src/internal/operators/windowWhen.ts index c43af3077c..8fbf943bb2 100644 --- a/src/internal/operators/windowWhen.ts +++ b/src/internal/operators/windowWhen.ts @@ -1,10 +1,7 @@ /** @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 { ObservableInput, OperatorFunction } from '../types'; import { lift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; @@ -127,86 +124,3 @@ export function windowWhen(closingSelector: () => ObservableInput): Oper ); }); } - -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); - } - - protected _next(value: T): void { - this.window!.next(value); - } - - protected _error(err: any): void { - this.window!.error(err); - this.destination.error(err); - this.unsubscribeClosingNotification(); - } - - protected _complete(): void { - this.window!.complete(); - this.destination.complete(); - this.unsubscribeClosingNotification(); - } - - private unsubscribeClosingNotification(): void { - if (this.closingNotification) { - this.closingNotification.unsubscribe(); - } - } - - private openWindow(innerSub: ComplexInnerSubscriber | null = null): void { - if (innerSub) { - this.remove(innerSub); - innerSub.unsubscribe(); - } - - const prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - - const window = (this.window = new Subject()); - this.destination.next(window); - - 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)))); - } -} From ef16d8d2cd152b80aa4bd38604568506a08df031 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 15:34:37 -0500 Subject: [PATCH 081/138] refactor(race/raceWith): smaller impl, remove stankyLift --- api_guard/dist/types/index.d.ts | 8 +- api_guard/dist/types/operators/index.d.ts | 2 +- src/internal/observable/race.ts | 112 +++++++--------------- src/internal/operators/raceWith.ts | 20 ++-- 4 files changed, 48 insertions(+), 94 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 83cd171e67..1cc5a504ac 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -197,14 +197,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; diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index 8ddec76238..1d900260e1 100644 --- a/api_guard/dist/types/operators/index.d.ts +++ b/api_guard/dist/types/operators/index.d.ts @@ -308,7 +308,7 @@ export declare function throttle(durationSelector: (value: T) => Subscribable 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>; 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/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); + }); } From 45452ca418c05f7ef3669883496b880212f94246 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 15:37:00 -0500 Subject: [PATCH 082/138] refactor: remove unused inner and outer subscribers --- src/internal/innerSubscribe.ts | 114 ---------------------------- src/internal/operators/repeat.ts | 1 - src/internal/operators/sample.ts | 3 +- src/internal/operators/takeUntil.ts | 4 +- 4 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 src/internal/innerSubscribe.ts 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/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/sample.ts b/src/internal/operators/sample.ts index 28f2c75493..849234f84b 100644 --- a/src/internal/operators/sample.ts +++ b/src/internal/operators/sample.ts @@ -2,9 +2,8 @@ import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic } from '../types'; +import { MonoTypeOperatorFunction } from '../types'; import { lift } from '../util/lift'; -import { SimpleOuterSubscriber, SimpleInnerSubscriber, innerSubscribe } from '../innerSubscribe'; import { OperatorSubscriber } from './OperatorSubscriber'; /** diff --git a/src/internal/operators/takeUntil.ts b/src/internal/operators/takeUntil.ts index 6e56af426b..6b620efc3e 100644 --- a/src/internal/operators/takeUntil.ts +++ b/src/internal/operators/takeUntil.ts @@ -1,11 +1,9 @@ -import { Operator } from '../Operator'; import { Observable } from '../Observable'; import { Subscriber } from '../Subscriber'; -import { MonoTypeOperatorFunction, TeardownLogic, ObservableInput } 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'; From 2880b5b4217e25bd13611cc21254040961068335 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 15:49:12 -0500 Subject: [PATCH 083/138] refactor: move subscribeTo, remove cruft - Removes old fromX functions that have not been exported since the earliest days of 5.x - Removes redundant tests over non-exported function - Moves subscribeTo to where `from` is, as it is the only place it is used now - Updates partition to just use `from`. --- spec/observables/IteratorObservable-spec.ts | 184 -------------------- src/internal/observable/from.ts | 36 +++- src/internal/observable/fromIterable.ts | 15 -- src/internal/observable/fromObservable.ts | 12 -- src/internal/observable/fromPromise.ts | 12 -- src/internal/observable/partition.ts | 6 +- src/internal/util/subscribeTo.ts | 35 ---- 7 files changed, 38 insertions(+), 262 deletions(-) delete mode 100644 spec/observables/IteratorObservable-spec.ts delete mode 100644 src/internal/observable/fromIterable.ts delete mode 100644 src/internal/observable/fromObservable.ts delete mode 100644 src/internal/observable/fromPromise.ts delete mode 100644 src/internal/util/subscribeTo.ts 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/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/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/partition.ts b/src/internal/observable/partition.ts index 637172fc86..511d36a3bb 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) as any)(from(source)) ] as [Observable, Observable]; } 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); - } -}; From 6ed55b1c9595322eb7df36b5cffb61748d3e9e77 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 16:34:57 -0500 Subject: [PATCH 084/138] refactor: Remove stankyLift, delete cruft - Removes last uses of stankyLift. - deletes weird internal/operators/index.ts remnant of 5.x that was still in the codebase - Moves deprecated concat to the same file with concatWith - Updates merge and concat operators not to use stankyLift. END THE STANK --- api_guard/dist/types/index.d.ts | 12 +- api_guard/dist/types/operators/index.d.ts | 2 +- src/internal/observable/concat.ts | 13 +- src/internal/observable/merge.ts | 179 ++++++++++++++++++---- src/internal/operators/concat.ts | 33 ---- src/internal/operators/concatWith.ts | 47 +++++- src/internal/operators/index.ts | 102 ------------ src/internal/operators/mergeWith.ts | 132 ++++++++++++---- src/internal/util/lift.ts | 26 ---- src/operators/index.ts | 3 +- 10 files changed, 312 insertions(+), 237 deletions(-) delete mode 100644 src/internal/operators/concat.ts delete mode 100644 src/internal/operators/index.ts diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index 1cc5a504ac..d6d5ffd15c 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -308,17 +308,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; diff --git a/api_guard/dist/types/operators/index.d.ts b/api_guard/dist/types/operators/index.d.ts index 1d900260e1..68c73b4872 100644 --- a/api_guard/dist/types/operators/index.d.ts +++ b/api_guard/dist/types/operators/index.d.ts @@ -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; 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/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/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/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/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/util/lift.ts b/src/internal/util/lift.ts index ba27dcdedb..f250c665f7 100644 --- a/src/internal/util/lift.ts +++ b/src/internal/util/lift.ts @@ -44,32 +44,6 @@ export function wrappedLift( }); } -// 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) - */ -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 hasLift(source: any): source is { lift: InstanceType['lift'] } { return source && typeof source.lift === 'function'; } 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'; From b0c2b4aeea858191b8e4b082eed722397e5b0382 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 17:00:18 -0500 Subject: [PATCH 085/138] refactor(generate): smaller impl --- src/internal/observable/generate.ts | 172 +++++++++------------------- 1 file changed, 56 insertions(+), 116 deletions(-) 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); + }); } From 066de7408810864891b9fd16e05c6c8b4ca88087 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 17:51:49 -0500 Subject: [PATCH 086/138] fix(fromEvent): properly teardown for arraylike targets - smaller impl --- spec/observables/fromEvent-spec.ts | 46 +++++++++++++++++ src/internal/observable/fromEvent.ts | 75 +++++++++++++--------------- 2 files changed, 81 insertions(+), 40 deletions(-) diff --git a/spec/observables/fromEvent-spec.ts b/spec/observables/fromEvent-spec.ts index cffdb64992..88c46f77e3 100644 --- a/spec/observables/fromEvent-spec.ts +++ b/spec/observables/fromEvent-spec.ts @@ -3,6 +3,7 @@ import { expectObservable } from '../helpers/marble-testing'; import { fromEvent, NEVER, timer } from 'rxjs'; import { mapTo, take, concat } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; +import sinon = require('sinon'); declare const rxTestScheduler: TestScheduler; @@ -392,4 +393,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/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 { From 7fd28f3662947e22ed81b18c210d260a6f9ecbec Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 18:06:50 -0500 Subject: [PATCH 087/138] refactor(Subject): smaller impl --- src/internal/Subject.ts | 84 +++++++++++++----------------------- src/internal/Subscription.ts | 2 + 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/internal/Subject.ts b/src/internal/Subject.ts index 16de75692d..e88fc9045b 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -1,7 +1,8 @@ +/** @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'; @@ -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,8 +42,8 @@ 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) { @@ -50,11 +51,9 @@ export class Subject extends Observable implements SubscriptionLike { throw new ObjectUnsubscribedError(); } 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); } } } @@ -63,16 +62,12 @@ export class Subject extends Observable implements SubscriptionLike { if (this.closed) { throw new ObjectUnsubscribedError(); } - this.hasError = true; + this.hasError = this.isStopped = 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); + while (observers.length) { + observers.shift()!.error(err); } - this.observers.length = 0; } complete() { @@ -81,17 +76,13 @@ export class Subject extends Observable implements SubscriptionLike { } this.isStopped = true; const { observers } = this; - const len = observers.length; - const copy = observers.slice(); - for (let i = 0; i < len; i++) { - copy[i].complete(); + 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!; } @@ -99,25 +90,22 @@ export class Subject extends Observable implements SubscriptionLike { _trySubscribe(subscriber: Subscriber): TeardownLogic { if (this.closed) { throw new ObjectUnsubscribedError(); - } else { - return super._trySubscribe(subscriber); } + return super._trySubscribe(subscriber); } /** @deprecated This is an internal implementation detail, do not use. */ _subscribe(subscriber: Subscriber): Subscription { - if (this.closed) { + const { closed, hasError, thrownError, isStopped, observers } = this; + if (closed) { throw new ObjectUnsubscribedError(); - } else if (this.hasError) { - subscriber.error(this.thrownError); - return Subscription.EMPTY; - } else if (this.isStopped) { - subscriber.complete(); - return Subscription.EMPTY; - } else { - this.observers.push(subscriber); - return new SubjectSubscription(this, subscriber); } + + return hasError + ? (subscriber.error(thrownError), EMPTY_SUBSCRIPTION) + : isStopped + ? (subscriber.complete(), EMPTY_SUBSCRIPTION) + : (observers.push(subscriber), new SubjectSubscription(this, subscriber)); } /** @@ -127,8 +115,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 +131,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/Subscription.ts b/src/internal/Subscription.ts index 22bcff7fa2..079b8e24bc 100644 --- a/src/internal/Subscription.ts +++ b/src/internal/Subscription.ts @@ -202,6 +202,8 @@ export class Subscription implements SubscriptionLike { } } +export const EMPTY_SUBSCRIPTION = Subscription.EMPTY; + export function isSubscription(value: any): value is Subscription { return ( value instanceof Subscription || From 7b2e9b3cb912bdbfb54cc80c6588b3d1caba9249 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 15 Sep 2020 19:33:18 -0500 Subject: [PATCH 088/138] refactor(Notification): smaller impl --- src/internal/Notification.ts | 80 ++++++++++-------------------------- 1 file changed, 22 insertions(+), 58 deletions(-) diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index 1d600f1446..5daa0c9318 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,16 @@ 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; - } - throw new Error('unexpected notification kind value'); + const { kind, value, error } = this; + return kind === 'N' + ? of(value!) + : kind === 'E' + ? throwError(error) + : kind === 'C' + ? EMPTY + : (() => { + throw new TypeError(`Unexpected notification kind ${kind}`); + })(); } private static completeNotification = new Notification('C') as Notification & CompleteNotification; @@ -245,20 +218,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?.(); } /** From 9d4294ce0e9f0b521792b79af81516449ba1744e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 13:18:13 -0500 Subject: [PATCH 089/138] refactor(Observable): more code golf --- src/internal/Observable.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/internal/Observable.ts b/src/internal/Observable.ts index 396e30ffc2..558852574c 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, @@ -500,13 +494,9 @@ function getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) { export function canReportError(subscriber: Subscriber): boolean { while (subscriber) { const { closed, destination, isStopped } = subscriber as any; - if (closed || isStopped) { - return false; - } else if (destination && destination instanceof Subscriber) { - subscriber = destination; - } else { - subscriber = null!; - } + return closed || isStopped + ? false + : (destination && destination instanceof Subscriber ? (subscriber = destination) : (subscriber = null!), true); } return true; } From a2ad641fc20d1bf15d82e287314201df11094dce Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 16:12:38 -0500 Subject: [PATCH 090/138] refactor(not): smaller impl Haha... sounds like a Wayne's World joke. --- src/internal/observable/partition.ts | 2 +- src/internal/operators/partition.ts | 2 +- src/internal/util/not.ts | 9 ++------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/internal/observable/partition.ts b/src/internal/observable/partition.ts index 511d36a3bb..7c3ce21d6c 100644 --- a/src/internal/observable/partition.ts +++ b/src/internal/observable/partition.ts @@ -62,6 +62,6 @@ export function partition( ): [Observable, Observable] { return [ filter(predicate, thisArg)(from(source)), - filter(not(predicate, thisArg) as any)(from(source)) + filter(not(predicate, thisArg))(from(source)) ] as [Observable, Observable]; } 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/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 From a01f107fd9108022404c745d66155fd2c3d16de1 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 16:13:10 -0500 Subject: [PATCH 091/138] refactor: add array item removal utility --- src/internal/SubjectSubscription.ts | 7 ++----- src/internal/Subscription.ts | 13 +++---------- src/internal/operators/bufferCount.ts | 6 ++---- src/internal/operators/bufferTime.ts | 8 ++------ src/internal/operators/bufferToggle.ts | 13 +++---------- src/internal/operators/windowTime.ts | 8 ++------ src/internal/operators/windowToggle.ts | 10 ++-------- src/internal/scheduler/AsyncAction.ts | 13 ++++--------- src/internal/util/arrRemove.ts | 11 +++++++++++ 9 files changed, 31 insertions(+), 58 deletions(-) create mode 100644 src/internal/util/arrRemove.ts diff --git a/src/internal/SubjectSubscription.ts b/src/internal/SubjectSubscription.ts index cd9b1e5a3e..e4c2b81e15 100644 --- a/src/internal/SubjectSubscription.ts +++ b/src/internal/SubjectSubscription.ts @@ -1,6 +1,7 @@ import { Subject } from './Subject'; import { Observer } from './types'; import { Subscription } from './Subscription'; +import { arrRemove } from './util/arrRemove'; /** * We need this JSDoc comment for affecting ESDoc. @@ -30,10 +31,6 @@ export class SubjectSubscription extends Subscription { return; } - const subscriberIndex = observers.indexOf(this.subscriber); - - if (subscriberIndex !== -1) { - observers.splice(subscriberIndex, 1); - } + arrRemove(observers, this.subscriber); } } diff --git a/src/internal/Subscription.ts b/src/internal/Subscription.ts index 079b8e24bc..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); diff --git a/src/internal/operators/bufferCount.ts b/src/internal/operators/bufferCount.ts index 625d90865a..081702cc87 100644 --- a/src/internal/operators/bufferCount.ts +++ b/src/internal/operators/bufferCount.ts @@ -4,6 +4,7 @@ import { Observable } from '../Observable'; 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 @@ -102,10 +103,7 @@ export function bufferCount(bufferSize: number, startBufferEvery: number | nu // `bufferSize`. Emit them, and remove them from our // buffers list. for (const buffer of toEmit) { - const index = buffers.indexOf(buffer); - if (0 <= index) { - buffers.splice(index, 1); - } + arrRemove(buffers, buffer); subscriber.next(buffer); } } diff --git a/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index 7a73207851..e0aa216617 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -8,6 +8,7 @@ import { isScheduler } from '../util/isScheduler'; import { OperatorFunction, SchedulerAction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; +import { arrRemove } from '../util/arrRemove'; /* tslint:disable:max-line-length */ export function bufferTime(bufferTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction; @@ -128,12 +129,7 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper subs, remove() { this.subs.unsubscribe(); - if (bufferRecords) { - const index = bufferRecords.indexOf(this); - if (0 <= index) { - bufferRecords.splice(index, 1); - } - } + bufferRecords && arrRemove(bufferRecords, this); }, }; bufferRecords.push(record); diff --git a/src/internal/operators/bufferToggle.ts b/src/internal/operators/bufferToggle.ts index 1800414394..c2abf5df93 100644 --- a/src/internal/operators/bufferToggle.ts +++ b/src/internal/operators/bufferToggle.ts @@ -6,6 +6,7 @@ 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 @@ -60,13 +61,6 @@ export function bufferToggle( return wrappedLift(source, (subscriber, liftedSource) => { const buffers: T[][] = []; - const remove = (buffer: T[]) => { - const index = buffers.indexOf(buffer); - if (0 <= index) { - buffers.splice(index, 1); - } - }; - // Subscribe to the openings notifier first from(openings).subscribe( new OperatorSubscriber( @@ -82,9 +76,8 @@ export function bufferToggle( // if the closing notifier completes without value. // TODO: We probably want to not have closing notifiers emit!! const emit = () => { - const b = buffer; - remove(b); - subscriber.next(b); + arrRemove(buffers, buffer); + subscriber.next(buffer); closingSubscription.unsubscribe(); }; diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index cfc2ab7cf4..a7f6eba438 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -8,6 +8,7 @@ import { isScheduler } from '../util/isScheduler'; import { OperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; +import { arrRemove } from '../util/arrRemove'; export function windowTime(windowTimeSpan: number, scheduler?: SchedulerLike): OperatorFunction>; export function windowTime( @@ -143,12 +144,7 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper seen: 0, remove() { this.subs.unsubscribe(); - if (windowRecords) { - const index = windowRecords.indexOf(this); - if (0 <= index) { - windowRecords.splice(index, 1); - } - } + windowRecords && arrRemove(windowRecords, this); }, }; windowRecords.push(record); diff --git a/src/internal/operators/windowToggle.ts b/src/internal/operators/windowToggle.ts index 96117de8fa..897837b4ed 100644 --- a/src/internal/operators/windowToggle.ts +++ b/src/internal/operators/windowToggle.ts @@ -8,6 +8,7 @@ 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 @@ -65,13 +66,6 @@ export function windowToggle( const subscriber = this; const windows: Subject[] = []; - const remove = (window: Subject) => { - const index = windows.indexOf(window); - if (0 <= index) { - windows.splice(index, 1); - } - }; - const handleError = (err: any) => { while (0 < windows.length) { windows.shift()!.error(err); @@ -94,7 +88,7 @@ export function windowToggle( windows.push(window); const closingSubscription = new Subscription(); const closeWindow = () => { - remove(window); + arrRemove(windows, window); window.complete(); closingSubscription.unsubscribe(); }; 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..18c7479664 --- /dev/null +++ b/src/internal/util/arrRemove.ts @@ -0,0 +1,11 @@ +/** @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[], item: T) { + const index = arr.indexOf(item); + 0 <= index && arr.splice(index, 1); +} From a0a5a5decc39f963fd3545785b36ab5598e7eec8 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 16:18:51 -0500 Subject: [PATCH 092/138] refactor: Remove SubjectSubscriber entirely --- src/internal/ReplaySubject.ts | 4 ++-- src/internal/Subject.ts | 4 ++-- src/internal/SubjectSubscription.ts | 36 ---------------------------- src/internal/operators/bufferTime.ts | 2 +- src/internal/operators/windowTime.ts | 2 +- src/internal/util/arrRemove.ts | 8 ++++--- 6 files changed, 11 insertions(+), 45 deletions(-) delete mode 100644 src/internal/SubjectSubscription.ts diff --git a/src/internal/ReplaySubject.ts b/src/internal/ReplaySubject.ts index 18214a8b34..fea5d234c4 100644 --- a/src/internal/ReplaySubject.ts +++ b/src/internal/ReplaySubject.ts @@ -3,8 +3,8 @@ 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 { arrRemove } from './util/arrRemove'; /** * A variant of {@link Subject} that "replays" old values to new subscribers by emitting them when they first subscribe. @@ -99,7 +99,7 @@ export class ReplaySubject extends Subject { subscription = Subscription.EMPTY; } else { this.observers.push(subscriber); - subscription = new SubjectSubscription(this, subscriber); + subscription = new Subscription(() => arrRemove(this.observers, subscriber)); } if (_infiniteTimeWindow) { diff --git a/src/internal/Subject.ts b/src/internal/Subject.ts index e88fc9045b..cc4fb0bf84 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -5,7 +5,7 @@ import { Subscriber } from './Subscriber'; 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 @@ -105,7 +105,7 @@ export class Subject extends Observable implements SubscriptionLike { ? (subscriber.error(thrownError), EMPTY_SUBSCRIPTION) : isStopped ? (subscriber.complete(), EMPTY_SUBSCRIPTION) - : (observers.push(subscriber), new SubjectSubscription(this, subscriber)); + : (observers.push(subscriber), new Subscription(() => arrRemove(this.observers, subscriber))); } /** diff --git a/src/internal/SubjectSubscription.ts b/src/internal/SubjectSubscription.ts deleted file mode 100644 index e4c2b81e15..0000000000 --- a/src/internal/SubjectSubscription.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Subject } from './Subject'; -import { Observer } from './types'; -import { Subscription } from './Subscription'; -import { arrRemove } from './util/arrRemove'; - -/** - * 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; - } - - arrRemove(observers, this.subscriber); - } -} diff --git a/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index e0aa216617..74dfdbaa69 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -129,7 +129,7 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper subs, remove() { this.subs.unsubscribe(); - bufferRecords && arrRemove(bufferRecords, this); + arrRemove(bufferRecords, this); }, }; bufferRecords.push(record); diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index a7f6eba438..01ce80c352 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -144,7 +144,7 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper seen: 0, remove() { this.subs.unsubscribe(); - windowRecords && arrRemove(windowRecords, this); + arrRemove(windowRecords, this); }, }; windowRecords.push(record); diff --git a/src/internal/util/arrRemove.ts b/src/internal/util/arrRemove.ts index 18c7479664..7763267b3c 100644 --- a/src/internal/util/arrRemove.ts +++ b/src/internal/util/arrRemove.ts @@ -5,7 +5,9 @@ * @param arr The array to remove the item from * @param item The item to remove */ -export function arrRemove(arr: T[], item: T) { - const index = arr.indexOf(item); - 0 <= index && arr.splice(index, 1); +export function arrRemove(arr: T[] | undefined | null, item: T) { + if (arr) { + const index = arr.indexOf(item); + 0 <= index && arr.splice(index, 1); + } } From 675ffe3743e8ae3cdde8f0836e52c37c546cf264 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 18:22:07 -0500 Subject: [PATCH 093/138] refactor: Fix broken while loop in canReportError https://github.com/ReactiveX/rxjs/pull/5729/files\#r489645413 --- src/internal/Observable.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/internal/Observable.ts b/src/internal/Observable.ts index 558852574c..77e8872e2b 100644 --- a/src/internal/Observable.ts +++ b/src/internal/Observable.ts @@ -494,9 +494,10 @@ function getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) { export function canReportError(subscriber: Subscriber): boolean { while (subscriber) { const { closed, destination, isStopped } = subscriber as any; - return closed || isStopped - ? false - : (destination && destination instanceof Subscriber ? (subscriber = destination) : (subscriber = null!), true); + if (closed || isStopped) { + return false; + } + subscriber = destination && destination instanceof Subscriber ? destination : null!; } return true; } From 3a0e650b2f60f19db4379781e7ea8a19cb2b8dfb Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 19:27:53 -0500 Subject: [PATCH 094/138] refactor: remove unused import --- spec/observables/fromEvent-spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/observables/fromEvent-spec.ts b/spec/observables/fromEvent-spec.ts index 88c46f77e3..ee76bb72ca 100644 --- a/spec/observables/fromEvent-spec.ts +++ b/spec/observables/fromEvent-spec.ts @@ -3,7 +3,6 @@ import { expectObservable } from '../helpers/marble-testing'; import { fromEvent, NEVER, timer } from 'rxjs'; import { mapTo, take, concat } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; -import sinon = require('sinon'); declare const rxTestScheduler: TestScheduler; From f1dcada1e1838ffb01cd0dac60340c6fa068f3cf Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Wed, 16 Sep 2020 21:23:51 -0500 Subject: [PATCH 095/138] refactor(subjects): Smaller subjects --- src/internal/AsyncSubject.ts | 34 +++---- src/internal/BehaviorSubject.ts | 23 ++--- src/internal/ReplaySubject.ts | 153 +++++++++++--------------------- src/internal/Subject.ts | 68 ++++++++------ 4 files changed, 109 insertions(+), 169 deletions(-) diff --git a/src/internal/AsyncSubject.ts b/src/internal/AsyncSubject.ts index a9369eb79b..b666113dbb 100644 --- a/src/internal/AsyncSubject.ts +++ b/src/internal/AsyncSubject.ts @@ -10,40 +10,28 @@ 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; - /** @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 } = this; + hasValue && super.next(value!); super.complete(); } } diff --git a/src/internal/BehaviorSubject.ts b/src/internal/BehaviorSubject.ts index 20de21c668..d5c27ef975 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/ReplaySubject.ts b/src/internal/ReplaySubject.ts index fea5d234c4..12a2fc99b4 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 { dateTimestampProvider } from "./scheduler/dateTimestampProvider"; -import { arrRemove } from './util/arrRemove'; +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 { arrRemove } from './util/arrRemove'; * {@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 Subscription(() => arrRemove(this.observers, 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 cc4fb0bf84..566bffd779 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -46,10 +46,14 @@ export class Subject extends Observable implements SubscriptionLike { return subject as any; } - next(value: T) { + protected throwIfClosed() { if (this.closed) { throw new ObjectUnsubscribedError(); } + } + + next(value: T) { + this.throwIfClosed(); if (!this.isStopped) { const copy = this.observers.slice(); for (const observer of copy) { @@ -59,25 +63,25 @@ export class Subject extends Observable implements SubscriptionLike { } error(err: any) { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } - this.hasError = this.isStopped = true; - this.thrownError = err; - const { observers } = this; - while (observers.length) { - observers.shift()!.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); + } } } complete() { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } - this.isStopped = true; - const { observers } = this; - while (observers.length) { - observers.shift()!.complete(); + this.throwIfClosed(); + if (!this.isStopped) { + this.isStopped = true; + const { observers } = this; + while (observers.length) { + observers.shift()!.complete(); + } } } @@ -87,27 +91,33 @@ export class Subject extends Observable implements SubscriptionLike { } /** @deprecated This is an internal implementation detail, do not use. */ - _trySubscribe(subscriber: Subscriber): TeardownLogic { - if (this.closed) { - throw new ObjectUnsubscribedError(); - } + 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 { - const { closed, hasError, thrownError, isStopped, observers } = this; - if (closed) { - throw new ObjectUnsubscribedError(); - } + protected _subscribe(subscriber: Subscriber): Subscription { + this.throwIfClosed(); + this.checkFinalizedStatuses(subscriber); + return this._innerSubscribe(subscriber); + } - return hasError - ? (subscriber.error(thrownError), EMPTY_SUBSCRIPTION) - : isStopped - ? (subscriber.complete(), EMPTY_SUBSCRIPTION) + 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(); + } + } /** * 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 From e9f2701d4324381066635a585ce5ab1b5ee173e7 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:07:08 -0500 Subject: [PATCH 096/138] refactor(bufferTime): smaller still --- src/internal/operators/bufferTime.ts | 84 ++++++++++------------------ 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/src/internal/operators/bufferTime.ts b/src/internal/operators/bufferTime.ts index 74dfdbaa69..715a1d8e2e 100644 --- a/src/internal/operators/bufferTime.ts +++ b/src/internal/operators/bufferTime.ts @@ -1,14 +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; @@ -82,12 +81,7 @@ 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; @@ -95,7 +89,7 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper 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; @@ -106,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(); }; /** @@ -127,37 +121,25 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper const record = { buffer, subs, - remove() { - this.subs.unsubscribe(); - arrRemove(bufferRecords, this); - }, }; 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); + + startBuffer(); const bufferTimeSubscriber = new OperatorSubscriber( subscriber, @@ -167,31 +149,27 @@ export function bufferTime(bufferTimeSpan: number, ...otherArgs: any[]): Oper // 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); From 580d9b80abc842ddf231cb25607e01bc71a5a10d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:07:30 -0500 Subject: [PATCH 097/138] refactor(windowTime): smaller still --- src/internal/operators/windowTime.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index 01ce80c352..740347ec4b 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -120,10 +120,11 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper // This is only really used for when *just* the time span is passed. let restartOnClose = false; - const closeWindow = (record: { window: Subject; subs: Subscription; remove: () => void }) => { - const { window } = record; + const closeWindow = (record: { window: Subject; subs: Subscription }) => { + const { window, subs } = record; window.complete(); - record.remove(); + subs.unsubscribe(); + arrRemove(windowRecords, record); if (restartOnClose) { startWindow(); } @@ -142,10 +143,6 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper window, subs, seen: 0, - remove() { - this.subs.unsubscribe(); - arrRemove(windowRecords, this); - }, }; windowRecords.push(record); subscriber.next(window.asObservable()); @@ -192,8 +189,7 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper subscriber, (value: T) => { loop((record) => { - const { window } = record; - window.next(value); + record.window.next(value); // If the window is over the max size, we need to close it. maxWindowSize <= ++record.seen && closeWindow(record); }); @@ -218,5 +214,4 @@ interface WindowRecord { seen: number; window: Subject; subs: Subscription; - remove: () => void; } From d5cd238e936dcd6b9ec8ea3d77a82eded5c41bee Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:09:45 -0500 Subject: [PATCH 098/138] chore: update golden files --- api_guard/dist/types/index.d.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index d6d5ffd15c..f772037d04 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; } @@ -496,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; @@ -528,13 +528,16 @@ export declare class Subject extends Observable implements SubscriptionLik observers: Observer[]; thrownError: any; constructor(); - _subscribe(subscriber: Subscriber): Subscription; - _trySubscribe(subscriber: Subscriber): TeardownLogic; + protected _innerSubscribe(subscriber: Subscriber): Subscription; + protected _subscribe(subscriber: Subscriber): Subscription; + protected _trySubscribe(subscriber: Subscriber): TeardownLogic; asObservable(): Observable; + protected checkFinalizedStatuses(subscriber: Subscriber): void; complete(): void; error(err: any): void; lift(operator: Operator): Observable; next(value: T): void; + protected throwIfClosed(): void; unsubscribe(): void; static create: Function; } From 75b0a34588df070fcc46bc8e91d3875485789df1 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:32:24 -0500 Subject: [PATCH 099/138] refactor(windowTime): a little smaller --- src/internal/operators/windowTime.ts | 63 ++++++++++------------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index 740347ec4b..cb1c03e755 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -5,7 +5,7 @@ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subscription } from '../Subscription'; import { isScheduler } from '../util/isScheduler'; -import { OperatorFunction, SchedulerLike } from '../types'; +import { Observer, OperatorFunction, SchedulerLike } from '../types'; import { lift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; import { arrRemove } from '../util/arrRemove'; @@ -102,12 +102,7 @@ export function windowTime( * @returnAn observable of windows, which in turn are Observables. */ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): OperatorFunction> { - let scheduler: SchedulerLike = asyncScheduler; - - if (isScheduler(otherArgs[otherArgs.length - 1])) { - scheduler = otherArgs.pop() as SchedulerLike; - } - + 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; @@ -125,9 +120,7 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper window.complete(); subs.unsubscribe(); arrRemove(windowRecords, record); - if (restartOnClose) { - startWindow(); - } + restartOnClose && startWindow(); }; /** @@ -150,21 +143,18 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper } }; - if (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(); - if (!this.closed) { - subscriber.add(this.schedule(null, windowCreationInterval)); - } - }, windowCreationInterval) - ); - } else { - restartOnClose = true; - } + 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(); /** @@ -173,14 +163,15 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper * 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); - }; + const loop = (cb: (record: WindowRecord) => void) => windowRecords!.slice().forEach(cb); /** - * Called when the source completes or errors to teardown and clean up. + * Used to notify all of the windows and the subscriber in the same way + * in the error and complete handlers. */ - const cleanup = () => { + const terminate = (cb: (consumer: Observer) => void) => { + loop(({ window }) => cb(window)); + cb(subscriber); windowRecords = null!; subscriber.unsubscribe(); }; @@ -194,16 +185,8 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper maxWindowSize <= ++record.seen && closeWindow(record); }); }, - (err) => { - loop(({ window }) => window.error(err)); - subscriber.error(err); - cleanup(); - }, - () => { - loop(({ window }) => window.complete()); - subscriber.complete(); - cleanup(); - } + (err) => terminate((consumer) => consumer.error(err)), + () => terminate((consumer) => consumer.complete()) ); source.subscribe(windowTimeSubscriber); From 8635dc6ff4c7e3ca2814d1f2164a8d6a87ac4504 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:32:56 -0500 Subject: [PATCH 100/138] refactor(groupBy): A little more --- src/internal/operators/groupBy.ts | 39 +++++++++++++------------------ 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 6dead63c0f..43d753403c 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -2,7 +2,7 @@ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { Subject } from '../Subject'; -import { OperatorFunction } from '../types'; +import { Observer, OperatorFunction } from '../types'; import { lift } from '../util/lift'; import { OperatorSubscriber } from './OperatorSubscriber'; @@ -130,6 +130,12 @@ export function groupBy( // A lookup for the groups that we have so far. const groups = new Map>(); + // Used for notifying all groups and the subscriber in the same way. + const notify = (cb: (group: Observer) => void) => { + groups.forEach(cb); + cb(subscriber); + }; + // 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 @@ -145,8 +151,7 @@ export function groupBy( let group = groups.get(key); if (!group) { // Create our group subject - group = subjectSelector ? subjectSelector() : new Subject(); - groups.set(key, group); + groups.set(key, (group = subjectSelector ? subjectSelector() : new Subject())); // Emit the grouped observable. Note that we can't do a simple `asObservable()` here, // because the grouped observable has special semantics around reference counting @@ -155,10 +160,6 @@ export function groupBy( subscriber.next(grouped); if (durationSelector) { - // A duration selector was provided, get the duration notifier - // and subscribe to it. - const durationNotifier = durationSelector(grouped); - 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 @@ -179,23 +180,17 @@ export function groupBy( ); // Start our duration notifier. - groupBySourceSubscriber.add(durationNotifier.subscribe(durationSubscriber)); + groupBySourceSubscriber.add(durationSelector(grouped).subscribe(durationSubscriber)); } } // Send the value to our group. group.next(elementSelector ? elementSelector(value) : value); }, - (err) => { - // Error from the source. - groups.forEach((group) => group.error(err)); - subscriber.error(err); - }, - () => { - // Source completes. - groups.forEach((group) => group.complete()); - subscriber.complete(); - }, + // 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 @@ -220,9 +215,9 @@ export function groupBy( // 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. - if (--groupBySourceSubscriber.activeGroups === 0 && groupBySourceSubscriber.teardownAttempted) { + --groupBySourceSubscriber.activeGroups === 0 && + groupBySourceSubscriber.teardownAttempted && groupBySourceSubscriber.unsubscribe(); - } }; }); result.key = key; @@ -251,9 +246,7 @@ class GroupBySubscriber extends OperatorSubscriber { // 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)). - if (this.activeGroups === 0) { - super.unsubscribe(); - } + this.activeGroups === 0 && super.unsubscribe(); } } From ddc7db419d170882551d1e42953314d885b5d1e4 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 17 Sep 2020 00:53:25 -0500 Subject: [PATCH 101/138] refactor(timeout): reduce the size of the argument parsing --- src/internal/operators/timeout.ts | 38 ++++++++++++------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/internal/operators/timeout.ts b/src/internal/operators/timeout.ts index cf1acc9612..74477e5e9a 100644 --- a/src/internal/operators/timeout.ts +++ b/src/internal/operators/timeout.ts @@ -301,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. From 834353efb7e78b43712ce35f27d12ccdd7608442 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 20 Sep 2020 12:48:30 -0500 Subject: [PATCH 102/138] refactor: remove unnecessary optional chaining --- src/internal/operators/OperatorSubscriber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index 6ba19748af..4da74a7335 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -13,7 +13,7 @@ export class OperatorSubscriber extends Subscriber { if (onNext) { this._next = function (value: T) { try { - onNext?.(value); + onNext(value); } catch (err) { this.error(err); } From 21f7fc30bef282b4dd9a026fd2227d66bf819006 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 20 Sep 2020 12:50:06 -0500 Subject: [PATCH 103/138] refactor(materialize): use a semicolon, not comma. --- src/internal/operators/materialize.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/internal/operators/materialize.ts b/src/internal/operators/materialize.ts index ba149a2773..af32536a72 100644 --- a/src/internal/operators/materialize.ts +++ b/src/internal/operators/materialize.ts @@ -75,7 +75,8 @@ export function materialize(): OperatorFunction & Observab subscriber.complete(); }, () => { - subscriber.next(Notification.createComplete()), subscriber.complete(); + subscriber.next(Notification.createComplete()); + subscriber.complete(); } ) ); From 444844320d6a1aeb61cf12b60be1d01dd8e7330f Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Sun, 20 Sep 2020 13:23:56 -0500 Subject: [PATCH 104/138] refactor(bufferWhen): remove unnecessary isComplete boolean --- src/internal/operators/bufferWhen.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/internal/operators/bufferWhen.ts b/src/internal/operators/bufferWhen.ts index c077eac6e7..6571654124 100644 --- a/src/internal/operators/bufferWhen.ts +++ b/src/internal/operators/bufferWhen.ts @@ -53,7 +53,6 @@ export function bufferWhen(closingSelector: () => ObservableInput): Oper const subscriber = this; let buffer: T[] | null = null; let closingSubscriber: Subscriber | null = null; - let isComplete = false; const openBuffer = () => { closingSubscriber?.unsubscribe(); @@ -70,11 +69,7 @@ export function bufferWhen(closingSelector: () => ObservableInput): Oper return; } - closingNotifier.subscribe( - (closingSubscriber = new OperatorSubscriber(subscriber, openBuffer, undefined, () => { - isComplete ? subscriber.complete() : openBuffer(); - })) - ); + closingNotifier.subscribe((closingSubscriber = new OperatorSubscriber(subscriber, openBuffer, undefined, () => openBuffer()))); }; openBuffer(); @@ -85,14 +80,10 @@ export function bufferWhen(closingSelector: () => ObservableInput): Oper (value) => buffer?.push(value), undefined, () => { - isComplete = true; buffer && subscriber.next(buffer); subscriber.complete(); }, - () => { - buffer = null!; - closingSubscriber = null!; - } + () => (buffer = closingSubscriber = null!) ) ); }); From 3009efda027b1b62bd92ceb0e44f42c4ee9ac5cf Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 08:33:37 -0500 Subject: [PATCH 105/138] refactor(window): ensure teardown isn't called twice, add comments --- src/internal/operators/window.ts | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/internal/operators/window.ts b/src/internal/operators/window.ts index 9f9fe1bf82..c6aae9780d 100644 --- a/src/internal/operators/window.ts +++ b/src/internal/operators/window.ts @@ -52,34 +52,46 @@ export function window(windowBoundaries: Observable): OperatorFunction) => lift(source, function (this: Subscriber>, source: Observable) { const subscriber = this; - let window = new Subject(); + let windowSubject = new Subject(); - subscriber.next(window.asObservable()); + subscriber.next(windowSubject.asObservable()); + /** + * 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) => { - window.error(err); + windowSubject.error(err); subscriber.error(err); }, () => { - window.complete(); + windowSubject.complete(); subscriber.complete(); - }, - () => { - window?.unsubscribe(); - window = null!; } ) ); - windowSubscribe(source, (value) => window.next(value)); + // Subscribe to our source + windowSubscribe(source, (value) => windowSubject.next(value)); + // Subscribe to the window boundaries. windowSubscribe(windowBoundaries, () => { - window.complete(); - subscriber.next((window = new Subject())); + windowSubject.complete(); + subscriber.next((windowSubject = new Subject())); }); + + // 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!; + }; }); } From 31d297a30ca10b8eec87cdb172bdc5de0601816a Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 08:35:48 -0500 Subject: [PATCH 106/138] refactor(withLatestFrom): remove unnecessary/incorrect typing, use inferrence --- src/internal/operators/withLatestFrom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/operators/withLatestFrom.ts b/src/internal/operators/withLatestFrom.ts index 7b0c5cb9e4..e986133e1a 100644 --- a/src/internal/operators/withLatestFrom.ts +++ b/src/internal/operators/withLatestFrom.ts @@ -161,7 +161,7 @@ export function withLatestFrom(...inputs: any[]): OperatorFunction { // 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: Record = inputs.map(() => false); + 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; From 30d429cf1b791db15c04a61f6a683e189b53fb3e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 08:50:01 -0500 Subject: [PATCH 107/138] docs(OperatorSubscriber): add comments --- src/internal/operators/OperatorSubscriber.ts | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/internal/operators/OperatorSubscriber.ts b/src/internal/operators/OperatorSubscriber.ts index 4da74a7335..ed28e1a0ab 100644 --- a/src/internal/operators/OperatorSubscriber.ts +++ b/src/internal/operators/OperatorSubscriber.ts @@ -1,7 +1,23 @@ /** @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, @@ -15,6 +31,12 @@ export class OperatorSubscriber extends Subscriber { 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); } }; @@ -24,8 +46,10 @@ export class OperatorSubscriber extends Subscriber { try { onError(err); } catch (err) { + // Send any errors that occur down stream. this.destination.error(err); } + // Ensure teardown. this.unsubscribe(); }; } @@ -34,14 +58,17 @@ export class OperatorSubscriber extends Subscriber { 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(); } From 9e00f11e992d223edf1013d0a44c7cad41b72470 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 16:18:27 -0500 Subject: [PATCH 108/138] fix(AsyncSubject): fix reentrancy issue in complete From comment https://github.com/ReactiveX/rxjs/pull/5729/files/30d429cf1b791db15c04a61f6a683e189b53fb3e#r492314703 --- spec/subjects/AsyncSubject-spec.ts | 38 ++++++++++++++++++++++++++++++ src/internal/AsyncSubject.ts | 11 ++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) 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 b666113dbb..e773db622e 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'; @@ -11,6 +12,7 @@ import { Subscription } from './Subscription'; export class AsyncSubject extends Subject { private value: T | null = null; private hasValue = false; + private isComplete = false; protected checkFinalizedStatuses(subscriber: Subscriber) { const { hasError, hasValue, value, thrownError, isStopped } = this; @@ -30,8 +32,11 @@ export class AsyncSubject extends Subject { } complete(): void { - const { hasValue, value } = this; - hasValue && super.next(value!); - super.complete(); + const { hasValue, value, isComplete } = this; + if (!isComplete) { + this.isComplete = true; + hasValue && super.next(value!); + super.complete(); + } } } From 29dd6ec38e3d975586cd0bee1f3afde6795a410c Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 16:21:39 -0500 Subject: [PATCH 109/138] refactor: Add underscore to name of internal API checkFinalizedStatuses --- src/internal/AsyncSubject.ts | 2 +- src/internal/Subject.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/internal/AsyncSubject.ts b/src/internal/AsyncSubject.ts index e773db622e..2951e88a3a 100644 --- a/src/internal/AsyncSubject.ts +++ b/src/internal/AsyncSubject.ts @@ -14,7 +14,7 @@ export class AsyncSubject extends Subject { private hasValue = false; private isComplete = false; - protected checkFinalizedStatuses(subscriber: Subscriber) { + protected _checkFinalizedStatuses(subscriber: Subscriber) { const { hasError, hasValue, value, thrownError, isStopped } = this; if (hasError) { subscriber.error(thrownError); diff --git a/src/internal/Subject.ts b/src/internal/Subject.ts index 566bffd779..00ba96e829 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -99,7 +99,7 @@ export class Subject extends Observable implements SubscriptionLike { /** @deprecated This is an internal implementation detail, do not use. */ protected _subscribe(subscriber: Subscriber): Subscription { this.throwIfClosed(); - this.checkFinalizedStatuses(subscriber); + this._checkFinalizedStatuses(subscriber); return this._innerSubscribe(subscriber); } @@ -110,7 +110,7 @@ export class Subject extends Observable implements SubscriptionLike { : (observers.push(subscriber), new Subscription(() => arrRemove(this.observers, subscriber))); } - protected checkFinalizedStatuses(subscriber: Subscriber) { + protected _checkFinalizedStatuses(subscriber: Subscriber) { const { hasError, thrownError, isStopped } = this; if (hasError) { subscriber.error(thrownError); From 02e113b3345a9efe8f7c29f8b9c1c0d088aaf726 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 16:32:54 -0500 Subject: [PATCH 110/138] feat(skipLast): counts zero or less will mirror the source BREAKING CHANGE: `skipLast` will no longer error when passed a negative number, rather it will simply return the source, as though `0` was passed. --- src/internal/ReplaySubject.ts | 2 +- src/internal/operators/skipLast.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/internal/ReplaySubject.ts b/src/internal/ReplaySubject.ts index 12a2fc99b4..ac71b809ff 100644 --- a/src/internal/ReplaySubject.ts +++ b/src/internal/ReplaySubject.ts @@ -81,7 +81,7 @@ export class ReplaySubject extends Subject { subscriber.next(copy[i] as T); } - this.checkFinalizedStatuses(subscriber); + this._checkFinalizedStatuses(subscriber); return subscription; } diff --git a/src/internal/operators/skipLast.ts b/src/internal/operators/skipLast.ts index 7c644d8ebf..f6a3e95b43 100644 --- a/src/internal/operators/skipLast.ts +++ b/src/internal/operators/skipLast.ts @@ -42,6 +42,7 @@ import { OperatorSubscriber } from './OperatorSubscriber'; * @name skipLast */ 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 From fb92045d6cac6f0fe6ef68651d667ea782ec9bcd Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 16:55:27 -0500 Subject: [PATCH 111/138] refactor(Notification): improve readability of large ternary. --- src/internal/Notification.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index 5daa0c9318..b7aaecc75d 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -155,15 +155,26 @@ export class Notification { */ toObservable(): Observable { const { kind, value, error } = this; - return kind === 'N' - ? of(value!) - : kind === 'E' - ? throwError(error) - : kind === 'C' - ? EMPTY - : (() => { - throw new TypeError(`Unexpected notification kind ${kind}`); - })(); + // 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) { + // We might think about removing this check, as the + throw new TypeError(`Unexpected notification kind ${kind}`); + } + return result; } private static completeNotification = new Notification('C') as Notification & CompleteNotification; From f5b29bf4ab745900de736d1f50d3bf7570f38485 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 17:05:24 -0500 Subject: [PATCH 112/138] chore: update golden files --- api_guard/dist/types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index f772037d04..aabc2773ac 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -21,7 +21,7 @@ export declare const async: AsyncScheduler; export declare const asyncScheduler: AsyncScheduler; export declare class AsyncSubject extends Subject { - protected checkFinalizedStatuses(subscriber: Subscriber): void; + protected _checkFinalizedStatuses(subscriber: Subscriber): void; complete(): void; next(value: T): void; } @@ -528,11 +528,11 @@ export declare class Subject extends Observable implements SubscriptionLik observers: Observer[]; thrownError: any; constructor(); + protected _checkFinalizedStatuses(subscriber: Subscriber): void; protected _innerSubscribe(subscriber: Subscriber): Subscription; protected _subscribe(subscriber: Subscriber): Subscription; protected _trySubscribe(subscriber: Subscriber): TeardownLogic; asObservable(): Observable; - protected checkFinalizedStatuses(subscriber: Subscriber): void; complete(): void; error(err: any): void; lift(operator: Operator): Observable; From ce8804ec67dad664f7351d57fc264618d1b72747 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 21 Sep 2020 17:08:21 -0500 Subject: [PATCH 113/138] chore: update comment on thrown error --- src/internal/Notification.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/internal/Notification.ts b/src/internal/Notification.ts index b7aaecc75d..7aea05b3e1 100644 --- a/src/internal/Notification.ts +++ b/src/internal/Notification.ts @@ -171,7 +171,9 @@ export class Notification { : // Unknown kind, return falsy, so we error below. 0; if (!result) { - // We might think about removing this check, as the + // 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}`); } return result; From c21b62c69fc13ded9a6c455608ca1580769eae92 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 08:38:59 -0500 Subject: [PATCH 114/138] refactor(timeout): remove unnecessary falsy check --- src/internal/operators/timeout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/internal/operators/timeout.ts b/src/internal/operators/timeout.ts index 74477e5e9a..c2a2ad297e 100644 --- a/src/internal/operators/timeout.ts +++ b/src/internal/operators/timeout.ts @@ -372,7 +372,8 @@ export function timeout(config: number | Date | TimeoutConfig, seen++; // Emit subscriber.next((lastValue = value)); - each && each > 0 && startTimer(each); + // null | undefined are both < 0. Thanks, JavaScript. + each! > 0 && startTimer(each!); }, undefined, undefined, From ed8c5972938a108f661c7a9a3ae216f76aa376a4 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 08:40:29 -0500 Subject: [PATCH 115/138] refactor(windowCount): remove unnecessary assignment --- src/internal/operators/windowCount.ts | 92 +++++++++++++-------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/internal/operators/windowCount.ts b/src/internal/operators/windowCount.ts index 8df8d6728d..c65fba6f19 100644 --- a/src/internal/operators/windowCount.ts +++ b/src/internal/operators/windowCount.ts @@ -82,54 +82,54 @@ export function windowCount(windowSize: number, startWindowEvery: number = 0) // Open the first window. subscriber.next(windows[0].asObservable()); - const outerSubscriber = 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(); - } + 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(); + } - // 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); + // 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!; } - subscriber.error(err); - }, - () => { - while (windows.length > 0) { - windows.shift()!.complete(); - } - subscriber.complete(); - }, - () => { - starts = null!; - windows = null!; - } + ) ); - - source.subscribe(outerSubscriber); }); } From f9550f3ce6e1c7d24af8af9aa510cbc0b6809788 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 08:42:13 -0500 Subject: [PATCH 116/138] refactor(windowToggle): remove unnecessary assignment --- src/internal/operators/windowToggle.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/internal/operators/windowToggle.ts b/src/internal/operators/windowToggle.ts index 897837b4ed..d049547e00 100644 --- a/src/internal/operators/windowToggle.ts +++ b/src/internal/operators/windowToggle.ts @@ -92,7 +92,6 @@ export function windowToggle( window.complete(); closingSubscription.unsubscribe(); }; - const closingSubscriber = new OperatorSubscriber(subscriber, closeWindow, handleError, closeWindow); let closingNotifier: Observable; try { @@ -104,7 +103,7 @@ export function windowToggle( subscriber.next(window.asObservable()); - closingSubscription.add(closingNotifier.subscribe(closingSubscriber)); + closingSubscription.add(closingNotifier.subscribe(new OperatorSubscriber(subscriber, closeWindow, handleError, closeWindow))); }, undefined, noop From b9938e2a73eb33b32d27aa8095d73d17e5c74c6d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:00:42 -0500 Subject: [PATCH 117/138] refactor(windowWhen): fix types, ensure window subject is released --- src/internal/operators/windowWhen.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/internal/operators/windowWhen.ts b/src/internal/operators/windowWhen.ts index 8fbf943bb2..4860fc0f9a 100644 --- a/src/internal/operators/windowWhen.ts +++ b/src/internal/operators/windowWhen.ts @@ -55,15 +55,15 @@ export function windowWhen(closingSelector: () => ObservableInput): Oper return (source: Observable) => lift(source, function (this: Subscriber>, source: Observable) { const subscriber = this; - let window: Subject; - let closingSubscriber: Subscriber; + let window: Subject | null; + let closingSubscriber: Subscriber | undefined; /** * When we get an error, we have to notify both the * destiation subscriber and the window. */ const handleError = (err: any) => { - window.error(err); + window!.error(err); subscriber.error(err); }; @@ -108,17 +108,18 @@ export function windowWhen(closingSelector: () => ObservableInput): Oper source.subscribe( new OperatorSubscriber( subscriber, - (value) => window.next(value), + (value) => window!.next(value), handleError, () => { // The source completed, close the window and complete. - window.complete(); + window!.complete(); subscriber.complete(); }, () => { // Be sure to clean up our closing subscription // when this tears down. closingSubscriber?.unsubscribe(); + window = null!; } ) ); From 0733183b4248066ee769aef6e5c5438ff21aaf45 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:01:24 -0500 Subject: [PATCH 118/138] refactor(mergeMap): ensure buffered values are released. --- src/internal/operators/mergeMap.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index c451727ad8..57b2be3961 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -109,7 +109,13 @@ export function mergeMap>( * Called to check to see if we can complete, and completes the result if * nothing is active. */ - const checkComplete = () => isComplete && !active && subscriber.complete(); + const checkComplete = () => { + if (isComplete && !active) { + subscriber.complete(); + // Ensure any buffered values are released. + buffer = null!; + } + }; /** * Attempts to start an inner subscription from a buffered value, From 006c118969d73e75129bebdcfbe0c2e8e4f20688 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:11:35 -0500 Subject: [PATCH 119/138] refactor(debounceTime): unify emit approach. Add comments. --- src/internal/operators/debounceTime.ts | 38 +++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/internal/operators/debounceTime.ts b/src/internal/operators/debounceTime.ts index 176e4692b7..7df0b93c40 100644 --- a/src/internal/operators/debounceTime.ts +++ b/src/internal/operators/debounceTime.ts @@ -69,35 +69,53 @@ export function debounceTime(dueTime: number, scheduler: SchedulerLike = asyn 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; + /** + * Emits the last value seen and clears it. + */ + const emitLastValue = () => { + hasValue = false; + const value = lastValue!; + lastValue = null; + subscriber.next(value); + }; + 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; - if (hasValue) { - hasValue = false; - const value = lastValue!; - lastValue = null; - subscriber.next(value); - } + // 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 (hasValue) { - subscriber.next(lastValue!); - lastValue = null; - } + // If we have a value waiting, emit it. + hasValue && emitLastValue(); subscriber.complete(); } ) From 732802cd381a9545b0158ab4ca2f2bfcf47b7d03 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:15:12 -0500 Subject: [PATCH 120/138] refactor(delay): Ensure buffer is released. --- src/internal/operators/delay.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/internal/operators/delay.ts b/src/internal/operators/delay.ts index 75bf641e68..526e0e861a 100644 --- a/src/internal/operators/delay.ts +++ b/src/internal/operators/delay.ts @@ -118,6 +118,7 @@ export function delay(delay: number | Date, scheduler: SchedulerLike = asyncS ); } }, + // Allow errors to pass through. undefined, () => { isComplete = true; @@ -125,5 +126,12 @@ export function delay(delay: number | Date, scheduler: SchedulerLike = asyncS } ) ); + + // Additional teardown. The other teardown is set up + // implicitly by subscribing with Subscribers. + return () => { + // Release the buffered values. + absoluteTimeValues = null!; + }; }); } From aae8d8efda76342d2d1008382aadf14f4efd6a9e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:19:51 -0500 Subject: [PATCH 121/138] refactor(mergeScan): Ensure buffered values are released. --- src/internal/operators/mergeScan.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/internal/operators/mergeScan.ts b/src/internal/operators/mergeScan.ts index 496d3a0f78..96f994a73d 100644 --- a/src/internal/operators/mergeScan.ts +++ b/src/internal/operators/mergeScan.ts @@ -94,6 +94,7 @@ export function mergeScan( // Intentially terse. Set the state, then emit it. subscriber.next((state = innerValue)); }, + // Errors are passed to the destination. undefined, () => { // The inner completed, decrement the number of actives. @@ -111,11 +112,24 @@ export function mergeScan( }; source.subscribe( - new OperatorSubscriber(subscriber, nextSourceValue, undefined, () => { - // Outer completed, make a note of it, and check to see if we can complete everything. - isComplete = true; - checkComplete(); - }) + new OperatorSubscriber( + subscriber, + nextSourceValue, + // Errors are passed through + undefined, + () => { + // Outer completed, make a note of it, and check to see if we can complete everything. + isComplete = true; + checkComplete(); + } + ) ); + + // 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!; + }; }); } From f73a80be67b170739c59bae993c982f8188476e5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:21:43 -0500 Subject: [PATCH 122/138] refactor(mergeMap): Ensure buffer is released on all teardown, not just completion. --- src/internal/operators/mergeMap.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index 57b2be3961..dbe9652b16 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -109,13 +109,7 @@ export function mergeMap>( * Called to check to see if we can complete, and completes the result if * nothing is active. */ - const checkComplete = () => { - if (isComplete && !active) { - subscriber.complete(); - // Ensure any buffered values are released. - buffer = null!; - } - }; + const checkComplete = () => isComplete && !active && subscriber.complete(); /** * Attempts to start an inner subscription from a buffered value, @@ -182,6 +176,13 @@ export function mergeMap>( } ) ); + + // Additional teardown. Called when the destination is torn down. + // Other teardown is registered implicitly above during subscription. + return () => { + // Release buffered values + buffer = null!; + }; }); } From cc8cf6f7fcb19385260f7e83b222956953ebfb8e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:39:18 -0500 Subject: [PATCH 123/138] refactor(expand): release buffer. Improved efficiency. Add comments. - Does a little work to make sure that we don't push onto the buffer unless we have to. --- src/internal/operators/expand.ts | 72 ++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/internal/operators/expand.ts b/src/internal/operators/expand.ts index b53ac603ef..83994db329 100644 --- a/src/internal/operators/expand.ts +++ b/src/internal/operators/expand.ts @@ -80,39 +80,75 @@ export function expand( return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; + // The number of active subscriptions. let active = 0; - const buffer: (T | R)[] = []; + // 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) { - const value = buffer.shift()!; - subscriber.next(value); - // 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 === 0 ? subscriber.complete() : trySub(); - }) - ); - }; - scheduler ? subscriber.add(scheduler.schedule(doSub)) : doSub(); + emitAndExpand(buffer.shift()!); } }; - const next = (value: T | R) => { - buffer.push(value); - trySub(); - }; + /** + * 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!; + }; }); } From 34ebee28d3845fe5d4863f22fced81730bfd7dc0 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:43:50 -0500 Subject: [PATCH 124/138] refactor(mergeMap): slight efficiency improvement. Adds comments --- src/internal/operators/mergeMap.ts | 77 ++++++++++++++++-------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/internal/operators/mergeMap.ts b/src/internal/operators/mergeMap.ts index dbe9652b16..28479c08a0 100644 --- a/src/internal/operators/mergeMap.ts +++ b/src/internal/operators/mergeMap.ts @@ -116,51 +116,56 @@ export function mergeMap>( * 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()!; + doInnerSub(buffer.shift()!); + } + }; - // 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), - 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 && doInnerSub(); - // Check to see if we can complete, and complete if so. - checkComplete(); - } - ) + /** + * 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 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 From b5673971a7c822ff97b38beb286d7bb9dcf9bcba Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 09:54:57 -0500 Subject: [PATCH 125/138] refactor(delayWhen): Clean up, add comments. --- src/internal/operators/delayWhen.ts | 42 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/internal/operators/delayWhen.ts b/src/internal/operators/delayWhen.ts index 83f8fd923d..cf7ee58019 100644 --- a/src/internal/operators/delayWhen.ts +++ b/src/internal/operators/delayWhen.ts @@ -93,37 +93,69 @@ export function delayWhen( 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(); + const outerSubscriber = new OperatorSubscriber( subscriber, (value: T) => { - const durationNotifier = delayDurationSelector(value, index++); - active++; + // 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(); - !closed && ((closed = true), --active === 0) && isComplete && subscriber.complete(); + + if (!closed) { + active--; + closed = true; + checkComplete(); + } }; + 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 ); - durationNotifier.subscribe(durationSubscriber); + + active++; + delayDurationSelector(value, index++).subscribe(durationSubscriber); }, + // Errors are passed through to consumer. undefined, () => { isComplete = true; - active === 0 && subscriber.complete(); + checkComplete(); + // Ensure the subscription to source is torn down as soon + // as possible. Otherwise it will hang until the final delay + // fires. outerSubscriber?.unsubscribe(); } ); + source.subscribe(outerSubscriber); }); } From efabb33c31b7497524731650cbee2393ff0e22a5 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 10:02:12 -0500 Subject: [PATCH 126/138] refactor(mergeScan): unify approach with mergeMap --- src/internal/operators/mergeScan.ts | 66 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/internal/operators/mergeScan.ts b/src/internal/operators/mergeScan.ts index 96f994a73d..ead8063c41 100644 --- a/src/internal/operators/mergeScan.ts +++ b/src/internal/operators/mergeScan.ts @@ -81,40 +81,50 @@ export function mergeScan( } }; - const nextSourceValue = (value: T) => { - // If we're under our concurrency limit, go ahead and - // call the accumulator and subscribe to the result. - if (active < concurrent) { - 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, - () => { - // The inner completed, decrement the number of actives. - active--; - // If we have anything in the buffer, process it, otherwise check to see if we can complete. - buffer.length ? nextSourceValue(buffer.shift()!) : checkComplete(); - } - ) - ); - } else { - // We're over our concurrency limit, push it onto the buffer to be - // process later when one of our inners completes. - buffer.push(value); + 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, + + // TODO: Much of this code is duplicated from mergeMap. Perhaps + // look into a way to unify this. + + () => { + // 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(); + } + ) + ); + }; + + const tryInnerSub = () => { + while (buffer.length && active < concurrent) { + doInnerSub(buffer.shift()!); } }; source.subscribe( new OperatorSubscriber( subscriber, - nextSourceValue, + // 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, () => { From d2853b112349edaa71674bfe9019700efd8981b8 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 10:07:31 -0500 Subject: [PATCH 127/138] refactor(windowTime): ensure buffer is released. Clean up code. --- src/internal/operators/windowTime.ts | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/internal/operators/windowTime.ts b/src/internal/operators/windowTime.ts index cb1c03e755..e7d2b64077 100644 --- a/src/internal/operators/windowTime.ts +++ b/src/internal/operators/windowTime.ts @@ -172,24 +172,34 @@ export function windowTime(windowTimeSpan: number, ...otherArgs: any[]): Oper const terminate = (cb: (consumer: Observer) => void) => { loop(({ window }) => cb(window)); cb(subscriber); - windowRecords = null!; subscriber.unsubscribe(); }; - const windowTimeSubscriber = new OperatorSubscriber( - subscriber, - (value: T) => { - loop((record) => { - record.window.next(value); - // If the window is over the max size, we need to close it. - maxWindowSize <= ++record.seen && closeWindow(record); - }); - }, - (err) => terminate((consumer) => consumer.error(err)), - () => terminate((consumer) => consumer.complete()) + 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()) + ) ); - source.subscribe(windowTimeSubscriber); + // 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!; + }; }); } From 2474dea3cff7daa7ac9fb6c92dca91de491ebec0 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 10:19:51 -0500 Subject: [PATCH 128/138] refactor(buffer): Ensure buffered values are released on teardown. --- src/internal/operators/buffer.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/internal/operators/buffer.ts b/src/internal/operators/buffer.ts index 2deb710779..d0793c1ef1 100644 --- a/src/internal/operators/buffer.ts +++ b/src/internal/operators/buffer.ts @@ -49,13 +49,23 @@ export function buffer(closingNotifier: Observable): OperatorFunction, source: Observable) { const subscriber = this; let buffer: T[] = []; + + // Subscribe to our source. source.subscribe(new OperatorSubscriber(subscriber, (value) => 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); }) ); + + return () => { + // Ensure buffered values are released on teardown. + buffer = null!; + }; }); } From 891d6da6bcde862c4ba03a8bf8aab91058e54d02 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 12:04:15 -0500 Subject: [PATCH 129/138] refactor(delayWhen): remove unnecessary unsubscribe --- src/internal/operators/delayWhen.ts | 90 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/src/internal/operators/delayWhen.ts b/src/internal/operators/delayWhen.ts index cf7ee58019..067461bc03 100644 --- a/src/internal/operators/delayWhen.ts +++ b/src/internal/operators/delayWhen.ts @@ -105,57 +105,55 @@ export function delayWhen( */ const checkComplete = () => isComplete && !active && subscriber.complete(); - const outerSubscriber = 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; + 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); + /** + * 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(); + // 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(); - } - }; + if (!closed) { + active--; + closed = true; + checkComplete(); + } + }; - 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 - ); + // 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(); - // Ensure the subscription to source is torn down as soon - // as possible. Otherwise it will hang until the final delay - // fires. - outerSubscriber?.unsubscribe(); - } + active++; + delayDurationSelector(value, index++).subscribe(durationSubscriber); + }, + // Errors are passed through to consumer. + undefined, + () => { + isComplete = true; + checkComplete(); + } + ) ); - - source.subscribe(outerSubscriber); }); } From affa71608ef1d58255a4a8cc88ce7fe149516861 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 12:36:34 -0500 Subject: [PATCH 130/138] docs(scan): add comments --- src/internal/operators/scan.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/internal/operators/scan.ts b/src/internal/operators/scan.ts index 2540c7579a..2ff64a015d 100644 --- a/src/internal/operators/scan.ts +++ b/src/internal/operators/scan.ts @@ -100,13 +100,30 @@ export function scan(accumulator: (acc: V | A | S, value: V, index: num 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; - let state: any = hasSeed ? seed! : null!; + // 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; + + // Subscribe to our source. All errors and completions are passed through. source.subscribe( new OperatorSubscriber(subscriber, (value) => { const i = index++; - subscriber.next((state = hasState ? accumulator(state, value, i) : ((hasState = true), value))); + // 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)) + ); }) ); }); From 58c89237cdb376a1f08b08bc6daeb3340efa880b Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 12:37:47 -0500 Subject: [PATCH 131/138] docs(map): add comments --- src/internal/operators/map.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/internal/operators/map.ts b/src/internal/operators/map.ts index b8d8f08017..5f93c0bad3 100644 --- a/src/internal/operators/map.ts +++ b/src/internal/operators/map.ts @@ -49,8 +49,12 @@ export function map(project: (value: T, index: number) => R, thisArg?: any const subscriber = this; // The index of the value from the source. Used with projection. let index = 0; + // 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++)); }) ); From 415dd2bc4abbd7f21acaecc5d13ed11233e78a19 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 12:39:53 -0500 Subject: [PATCH 132/138] docs(filter): add comments --- src/internal/operators/filter.ts | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/internal/operators/filter.ts b/src/internal/operators/filter.ts index 2250722bdc..0b2a1b7670 100644 --- a/src/internal/operators/filter.ts +++ b/src/internal/operators/filter.ts @@ -1,3 +1,4 @@ +/** @prettier */ import { Subscriber } from '../Subscriber'; import { Observable } from '../Observable'; import { OperatorFunction, MonoTypeOperatorFunction } from '../types'; @@ -5,12 +6,10 @@ 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,17 +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, function (this: Subscriber, source: Observable) { +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; - return source.subscribe(new OperatorSubscriber(subscriber, (value) => { - if (predicate.call(thisArg, value, index++)) { - subscriber.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); + } + }) + ); }); - }; } From 61ef56d1473f6dd199f76f19b3099bda9c3e856e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 12:41:14 -0500 Subject: [PATCH 133/138] docs(mapTo): add comments --- src/internal/operators/mapTo.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/internal/operators/mapTo.ts b/src/internal/operators/mapTo.ts index 03d1eda305..3aba80d8a3 100644 --- a/src/internal/operators/mapTo.ts +++ b/src/internal/operators/mapTo.ts @@ -44,6 +44,13 @@ export function mapTo(value: R): OperatorFunction { return (source: Observable) => lift(source, function (this: Subscriber, source: Observable) { const subscriber = this; - source.subscribe(new OperatorSubscriber(subscriber, () => subscriber.next(value))); + // 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) + ) + ); }); } From e9bff8efe34112616ab9679ae05e61646923b0c1 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 21:07:02 -0500 Subject: [PATCH 134/138] refactor(repeatWhen): remove unnecessary Subscription allocation --- src/internal/operators/repeatWhen.ts | 43 ++++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/internal/operators/repeatWhen.ts b/src/internal/operators/repeatWhen.ts index 0426a4deb8..49ba25a83e 100644 --- a/src/internal/operators/repeatWhen.ts +++ b/src/internal/operators/repeatWhen.ts @@ -41,7 +41,6 @@ export function repeatWhen(notifier: (notifications: Observable) => Obs 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; @@ -62,27 +61,25 @@ export function repeatWhen(notifier: (notifications: Observable) => Obs // If the call to `notifier` throws, it will be caught by the OperatorSubscriber // In the main subscription -- in `subscribeForRepeatWhen`. - subscription.add( - 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(); + 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(); + } ) ); } @@ -118,14 +115,10 @@ export function repeatWhen(notifier: (notifications: Observable) => Obs syncResub = false; // Resubscribe subscribeForRepeatWhen(); - } else { - subscription.add(innerSub); } }; // Start the subscription subscribeForRepeatWhen(); - - return subscription; }); } From 529ad62d1b74d0e1b7be6e162d6535a1834254ac Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 21:08:54 -0500 Subject: [PATCH 135/138] refactor(retryWhen): remove unnecessary Subscription allocation --- src/internal/operators/retryWhen.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/internal/operators/retryWhen.ts b/src/internal/operators/retryWhen.ts index 0a353624bd..77916eb691 100644 --- a/src/internal/operators/retryWhen.ts +++ b/src/internal/operators/retryWhen.ts @@ -63,7 +63,6 @@ import { OperatorSubscriber } from './OperatorSubscriber'; export function retryWhen(notifier: (errors: Observable) => Observable): MonoTypeOperatorFunction { return (source: Observable) => wrappedLift(source, (subscriber: Subscriber, source: Observable) => { - const subscription = new Subscription(); let innerSub: Subscription | null; let syncResub = false; let errors$: Subject; @@ -73,16 +72,14 @@ export function retryWhen(notifier: (errors: Observable) => Observable { if (!errors$) { errors$ = new Subject(); - subscription.add( - 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. - innerSub ? subscribeForRetryWhen() : (syncResub = true) - ) + 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. + innerSub ? subscribeForRetryWhen() : (syncResub = true) ) ); } @@ -104,14 +101,10 @@ export function retryWhen(notifier: (errors: Observable) => Observable Date: Tue, 22 Sep 2020 21:11:34 -0500 Subject: [PATCH 136/138] refactor: add underscore to protected method _throwIfClosed --- src/internal/BehaviorSubject.ts | 2 +- src/internal/ReplaySubject.ts | 2 +- src/internal/Subject.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/internal/BehaviorSubject.ts b/src/internal/BehaviorSubject.ts index d5c27ef975..d2b6ec2737 100644 --- a/src/internal/BehaviorSubject.ts +++ b/src/internal/BehaviorSubject.ts @@ -30,7 +30,7 @@ export class BehaviorSubject extends Subject { if (hasError) { throw thrownError; } - this.throwIfClosed(); + this._throwIfClosed(); return _value; } diff --git a/src/internal/ReplaySubject.ts b/src/internal/ReplaySubject.ts index ac71b809ff..5f05404bbc 100644 --- a/src/internal/ReplaySubject.ts +++ b/src/internal/ReplaySubject.ts @@ -68,7 +68,7 @@ export class ReplaySubject extends Subject { /** @deprecated Remove in v8. This is an internal implementation detail, do not use. */ protected _subscribe(subscriber: Subscriber): Subscription { - this.throwIfClosed(); + this._throwIfClosed(); this.trimBuffer(); const subscription = this._innerSubscribe(subscriber); diff --git a/src/internal/Subject.ts b/src/internal/Subject.ts index 00ba96e829..ada7029c89 100644 --- a/src/internal/Subject.ts +++ b/src/internal/Subject.ts @@ -46,14 +46,14 @@ export class Subject extends Observable implements SubscriptionLike { return subject as any; } - protected throwIfClosed() { + protected _throwIfClosed() { if (this.closed) { throw new ObjectUnsubscribedError(); } } next(value: T) { - this.throwIfClosed(); + this._throwIfClosed(); if (!this.isStopped) { const copy = this.observers.slice(); for (const observer of copy) { @@ -63,7 +63,7 @@ export class Subject extends Observable implements SubscriptionLike { } error(err: any) { - this.throwIfClosed(); + this._throwIfClosed(); if (!this.isStopped) { this.hasError = this.isStopped = true; this.thrownError = err; @@ -75,7 +75,7 @@ export class Subject extends Observable implements SubscriptionLike { } complete() { - this.throwIfClosed(); + this._throwIfClosed(); if (!this.isStopped) { this.isStopped = true; const { observers } = this; @@ -92,13 +92,13 @@ export class Subject extends Observable implements SubscriptionLike { /** @deprecated This is an internal implementation detail, do not use. */ protected _trySubscribe(subscriber: Subscriber): TeardownLogic { - this.throwIfClosed(); + this._throwIfClosed(); return super._trySubscribe(subscriber); } /** @deprecated This is an internal implementation detail, do not use. */ protected _subscribe(subscriber: Subscriber): Subscription { - this.throwIfClosed(); + this._throwIfClosed(); this._checkFinalizedStatuses(subscriber); return this._innerSubscribe(subscriber); } From 6241024bfad6f02307a077086fd94bea96c93d6e Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 21:12:41 -0500 Subject: [PATCH 137/138] chore: update golden files --- api_guard/dist/types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index aabc2773ac..10dac1e61b 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -531,13 +531,13 @@ export declare class Subject extends Observable implements SubscriptionLik 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; lift(operator: Operator): Observable; next(value: T): void; - protected throwIfClosed(): void; unsubscribe(): void; static create: Function; } From 33ec81c5a5c79fc09751268872651892b4594dfc Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Tue, 22 Sep 2020 21:25:51 -0500 Subject: [PATCH 138/138] refactor(groupBy): remove unnecessary return --- src/internal/operators/groupBy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/operators/groupBy.ts b/src/internal/operators/groupBy.ts index 43d753403c..ce97c96e82 100644 --- a/src/internal/operators/groupBy.ts +++ b/src/internal/operators/groupBy.ts @@ -199,7 +199,7 @@ export function groupBy( ); // Subscribe to the source - return source.subscribe(groupBySourceSubscriber); + source.subscribe(groupBySourceSubscriber); /** * Creates the actual grouped observable returned.