From 60eb3b98d40d7f7e5107ccae08b9d56f02488426 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Mon, 20 Jan 2020 21:48:57 -0600 Subject: [PATCH] feat(combineLatestWith): adds combineLatestWith - renamed legacy combineWith operator - creates tests for new operator - really just a renamed version of the old operator that was deprecated - updates deprecations --- .../observables/combineLatest-spec.ts | 82 ++- spec-dtslint/observables/of-spec.ts | 21 +- .../operators/combineLatestWith-spec.ts | 57 ++ spec-dtslint/operators/concatWith-spec.ts | 6 +- ...t-spec.ts => combineLatest-legacy-spec.ts} | 2 +- spec/operators/combineLatestWith-spec.ts | 533 ++++++++++++++++++ ...{combineLatest.ts => combineLatestWith.ts} | 86 ++- src/internal/operators/index.ts | 2 +- src/operators/index.ts | 2 +- 9 files changed, 703 insertions(+), 88 deletions(-) create mode 100644 spec-dtslint/operators/combineLatestWith-spec.ts rename spec/operators/{combineLatest-spec.ts => combineLatest-legacy-spec.ts} (99%) create mode 100644 spec/operators/combineLatestWith-spec.ts rename src/internal/operators/{combineLatest.ts => combineLatestWith.ts} (50%) diff --git a/spec-dtslint/observables/combineLatest-spec.ts b/spec-dtslint/observables/combineLatest-spec.ts index 2360db11a3..065a41dcf5 100644 --- a/spec-dtslint/observables/combineLatest-spec.ts +++ b/spec-dtslint/observables/combineLatest-spec.ts @@ -1,139 +1,125 @@ -import { of, combineLatest, asyncScheduler } from 'rxjs'; - -class A { a = 0; } -class B { b = 0; } -class C { c = 0; } -class D { d = 0; } -class E { e = 0; } -class F { f = 0; } -class G { g = 0; } - -const a = of(new A()); -const b = of(new B()); -const c = of(new C()); -const d = of(new D()); -const e = of(new E()); -const f = of(new F()); -const g = of(new G()); +import { combineLatest } from 'rxjs'; +import { a$, b$, c$, d$, e$, f$, g$, A, B, C, D, E, F } from '../helpers'; + it('should accept 1 param', () => { - const o = combineLatest(a); // $ExpectType Observable<[A]> + const o = combineLatest(a$); // $ExpectType Observable<[A]> }); it('should accept 2 params', () => { - const o = combineLatest(a, b); // $ExpectType Observable<[A, B]> + const o = combineLatest(a$, b$); // $ExpectType Observable<[A, B]> }); it('should accept 3 params', () => { - const o = combineLatest(a, b, c); // $ExpectType Observable<[A, B, C]> + const o = combineLatest(a$, b$, c$); // $ExpectType Observable<[A, B, C]> }); it('should accept 4 params', () => { - const o = combineLatest(a, b, c, d); // $ExpectType Observable<[A, B, C, D]> + const o = combineLatest(a$, b$, c$, d$); // $ExpectType Observable<[A, B, C, D]> }); it('should accept 5 params', () => { - const o = combineLatest(a, b, c, d, e); // $ExpectType Observable<[A, B, C, D, E]> + const o = combineLatest(a$, b$, c$, d$, e$); // $ExpectType Observable<[A, B, C, D, E]> }); it('should accept 6 params', () => { - const o = combineLatest(a, b, c, d, e, f); // $ExpectType Observable<[A, B, C, D, E, F]> + const o = combineLatest(a$, b$, c$, d$, e$, f$); // $ExpectType Observable<[A, B, C, D, E, F]> }); it('should result in Observable for 7 or more params', () => { - const o = combineLatest(a, b, c, d, e, f, g); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, d$, e$, f$, g$); // $ExpectType Observable }); it('should accept union types', () => { - const u1: typeof a | typeof b = Math.random() > 0.5 ? a : b; - const u2: typeof c | typeof d = Math.random() > 0.5 ? c : d; + const u1: typeof a$ | typeof b$ = Math.random() > 0.5 ? a$ : b$; + const u2: typeof c$ | typeof d$ = Math.random() > 0.5 ? c$ : d$; const o = combineLatest(u1, u2); // $ExpectType Observable<[A | B, C | D]> }); it('should accept 1 param and a result selector', () => { - const o = combineLatest(a, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, () => new A()); // $ExpectType Observable }); it('should accept 2 params and a result selector', () => { - const o = combineLatest(a, b, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, () => new A()); // $ExpectType Observable }); it('should accept 3 params and a result selector', () => { - const o = combineLatest(a, b, c, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, () => new A()); // $ExpectType Observable }); it('should accept 4 params and a result selector', () => { - const o = combineLatest(a, b, c, d, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, d$, () => new A()); // $ExpectType Observable }); it('should accept 5 params and a result selector', () => { - const o = combineLatest(a, b, c, d, e, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, d$, e$, () => new A()); // $ExpectType Observable }); it('should accept 6 params and a result selector', () => { - const o = combineLatest(a, b, c, d, e, f, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, d$, e$, f$, () => new A()); // $ExpectType Observable }); it('should accept 7 or more params and a result selector', () => { - const o = combineLatest(a, b, c, d, e, f, g, g, g, () => new A()); // $ExpectType Observable + const o = combineLatest(a$, b$, c$, d$, e$, f$, g$, g$, g$, () => new A()); // $ExpectType Observable }); it('should accept 1 param', () => { - const o = combineLatest([a]); // $ExpectType Observable<[A]> + const o = combineLatest([a$]); // $ExpectType Observable<[A]> }); it('should accept 2 params', () => { - const o = combineLatest([a, b]); // $ExpectType Observable<[A, B]> + const o = combineLatest([a$, b$]); // $ExpectType Observable<[A, B]> }); it('should accept 3 params', () => { - const o = combineLatest([a, b, c]); // $ExpectType Observable<[A, B, C]> + const o = combineLatest([a$, b$, c$]); // $ExpectType Observable<[A, B, C]> }); it('should accept 4 params', () => { - const o = combineLatest([a, b, c, d]); // $ExpectType Observable<[A, B, C, D]> + const o = combineLatest([a$, b$, c$, d$]); // $ExpectType Observable<[A, B, C, D]> }); it('should accept 5 params', () => { - const o = combineLatest([a, b, c, d, e]); // $ExpectType Observable<[A, B, C, D, E]> + const o = combineLatest([a$, b$, c$, d$, e$]); // $ExpectType Observable<[A, B, C, D, E]> }); it('should accept 6 params', () => { - const o = combineLatest([a, b, c, d, e, f]); // $ExpectType Observable<[A, B, C, D, E, F]> + const o = combineLatest([a$, b$, c$, d$, e$, f$]); // $ExpectType Observable<[A, B, C, D, E, F]> }); it('should have basic support for 7 or more params', () => { - const o = combineLatest([a, b, c, d, e, f, g]); // $ExpectType Observable<(A | B | C | D | E | F | G)[]> + const o = combineLatest([a$, b$, c$, d$, e$, f$, g$]); // $ExpectType Observable<(A | B | C | D | E | F | G)[]> }); it('should handle an array of Observables', () => { - const o = combineLatest([a, a, a, a, a, a, a, a, a, a, a]); // $ExpectType Observable + const o = combineLatest([a$, a$, a$, a$, a$, a$, a$, a$, a$, a$, a$]); // $ExpectType Observable }); it('should accept 1 param and a result selector', () => { - const o = combineLatest([a], (a: A) => new A()); // $ExpectType Observable + const o = combineLatest([a$], (a: A) => new A()); // $ExpectType Observable }); it('should accept 2 params and a result selector', () => { - const o = combineLatest([a, b], (a: A, b: B) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$], (a: A, b: B) => new A()); // $ExpectType Observable }); it('should accept 3 params and a result selector', () => { - const o = combineLatest([a, b, c], (a: A, b: B, c: C) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$, c$], (a: A, b: B, c: C) => new A()); // $ExpectType Observable }); it('should accept 4 params and a result selector', () => { - const o = combineLatest([a, b, c, d], (a: A, b: B, c: C, d: D) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$, c$, d$], (a: A, b: B, c: C, d: D) => new A()); // $ExpectType Observable }); it('should accept 5 params and a result selector', () => { - const o = combineLatest([a, b, c, d, e], (a: A, b: B, c: C, d: D, e: E) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$, c$, d$, e$], (a: A, b: B, c: C, d: D, e: E) => new A()); // $ExpectType Observable }); it('should accept 6 params and a result selector', () => { - const o = combineLatest([a, b, c, d, e, f], (a: A, b: B, c: C, d: D, e: E, f: F) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$, c$, d$, e$, f$], (a: A, b: B, c: C, d: D, e: E, f: F) => new A()); // $ExpectType Observable }); it('should accept 7 or more params and a result selector', () => { - const o = combineLatest([a, b, c, d, e, f, g, g, g], (a: any, b: any, c: any, d: any, e: any, f: any, g1: any, g2: any, g3: any) => new A()); // $ExpectType Observable + const o = combineLatest([a$, b$, c$, d$, e$, f$, g$, g$, g$], (a: any, b: any, c: any, d: any, e: any, f: any, g1: any, g2: any, g3: any) => new A()); // $ExpectType Observable }); diff --git a/spec-dtslint/observables/of-spec.ts b/spec-dtslint/observables/of-spec.ts index ecef02f5ca..34cfde5fb5 100644 --- a/spec-dtslint/observables/of-spec.ts +++ b/spec-dtslint/observables/of-spec.ts @@ -1,16 +1,5 @@ import { of, animationFrameScheduler, queueScheduler } from 'rxjs'; -import { A, B, C, D, E, F, G, H, I, J } from '../helpers'; - -const a = new A(); -const b = new B(); -const c = new C(); -const d = new D(); -const e = new E(); -const f = new F(); -const g = new G(); -const h = new H(); -const i = new I(); -const j = new J(); +import { A, a, b, c, d, e, f, g, h, i, j } from '../helpers'; it('should infer never with 0 params', () => { const res = of(); // $ExpectType Observable @@ -23,7 +12,7 @@ it('forced generic should not cause an issue', () => { }); it('should infer correctly with 1 param', () => { - const res = of(new A()); // $ExpectType Observable + const res = of(a); // $ExpectType Observable }); it('should infer correctly with mixed type of 2 params', () => { @@ -67,7 +56,7 @@ it('should support mixed type of 9 params', () => { }); it('should support mixed type of 13 params', () => { - const res = of(a, b, c, d, e, f, g, h, i, j, '', true, 123, [1, 2, 3]); // $ExpectType Observable + const res = of(a, b, c, d, e, f, g, h, i, j, '', true, 123, [1, 2, 3]); // $ExpectType Observable }); it('should support a rest of params', () => { @@ -95,7 +84,7 @@ it('should infer never with 0 params', () => { }); it('should infer correctly with 1 param', () => { - const res = of(new A(), queueScheduler); // $ExpectType Observable + const res = of(a, queueScheduler); // $ExpectType Observable }); it('should infer correctly with mixed type of 2 params', () => { @@ -140,7 +129,7 @@ it('should deprecate correctly', () => { of(a, b, c, d, e, f, queueScheduler); // $ExpectDeprecation of(a, b, c, d, e, f, g, queueScheduler); // $ExpectDeprecation of(a, b, c, d, e, f, g, h, queueScheduler); // $ExpectDeprecation - of(a, b, c, d, e, f, g, h, i, queueScheduler); // $ExpectDeprecation + of(a, b, c, d, e, f, g, h, i, queueScheduler); // $ExpectDeprecation of(); // $ExpectDeprecation of(); // $ExpectNoDeprecation of(a); // $ExpectNoDeprecation diff --git a/spec-dtslint/operators/combineLatestWith-spec.ts b/spec-dtslint/operators/combineLatestWith-spec.ts new file mode 100644 index 0000000000..9003a811ac --- /dev/null +++ b/spec-dtslint/operators/combineLatestWith-spec.ts @@ -0,0 +1,57 @@ +import { of } from 'rxjs'; +import { combineLatestWith } from 'rxjs/operators'; + +describe('combineLatestWith', () => { + describe('without project parameter', () => { + it('should infer correctly with 1 param', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const res = a.pipe(combineLatestWith(b)); // $ExpectType Observable<[number, string]> + }); + + it('should infer correctly with 2 params', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const c = of('d', 'e', 'f'); + const res = a.pipe(combineLatestWith(b, c)); // $ExpectType Observable<[number, string, string]> + }); + + it('should infer correctly with 3 params', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const c = of('d', 'e', 'f'); + const d = of('g', 'h', 'i'); + const res = a.pipe(combineLatestWith(b, c, d)); // $ExpectType Observable<[number, string, string, string]> + }); + + it('should infer correctly with 4 params', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const c = of('d', 'e', 'f'); + const d = of('g', 'h', 'i'); + const e = of('j', 'k', 'l'); + const res = a.pipe(combineLatestWith(b, c, d, e)); // $ExpectType Observable<[number, string, string, string, string]> + }); + + it('should infer correctly with 5 params', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const c = of('d', 'e', 'f'); + const d = of('g', 'h', 'i'); + const e = of('j', 'k', 'l'); + const f = of('m', 'n', 'o'); + const res = a.pipe(combineLatestWith(b, c, d, e, f)); // $ExpectType Observable<[number, string, string, string, string, string]> + }); + + it('should accept N params', () => { + const a = of(1, 2, 3); + const b = of('a', 'b', 'c'); + const c = of('d', 'e', 'f'); + const d = of('g', 'h', 'i'); + const e = of('j', 'k', 'l'); + const f = of('m', 'n', 'o'); + const g = of('p', 'q', 'r'); + const res = a.pipe(combineLatestWith(b, c, d, e, f, g)); // $ExpectType Observable<(string | number)[]> + }); + }); +}); diff --git a/spec-dtslint/operators/concatWith-spec.ts b/spec-dtslint/operators/concatWith-spec.ts index 9248195e77..a7e113e2af 100644 --- a/spec-dtslint/operators/concatWith-spec.ts +++ b/spec-dtslint/operators/concatWith-spec.ts @@ -1,11 +1,11 @@ import { of } from 'rxjs'; import { concatWith } from 'rxjs/operators'; -import { a, b$, c$, d$, e$ } from 'helpers'; +import { a$, b$, c$, d$, e$ } from 'helpers'; it('should support rest params', () => { const arr = [b$, c$]; - const o = of(a).pipe(concatWith(...arr)); // $ExpectType Observable - const o2 = of(a).pipe(concatWith(d$, ...arr, e$)); // $ExpectType Observable + const o = a$.pipe(concatWith(...arr)); // $ExpectType Observable + const o2 = a$.pipe(concatWith(d$, ...arr, e$)); // $ExpectType Observable }); it('should infer correctly', () => { diff --git a/spec/operators/combineLatest-spec.ts b/spec/operators/combineLatest-legacy-spec.ts similarity index 99% rename from spec/operators/combineLatest-spec.ts rename to spec/operators/combineLatest-legacy-spec.ts index 63da8c4bf3..7440b09478 100644 --- a/spec/operators/combineLatest-spec.ts +++ b/spec/operators/combineLatest-legacy-spec.ts @@ -6,7 +6,7 @@ import { observableMatcher } from '../helpers/observableMatcher'; declare function asDiagram(arg: string): Function; /** @test {combineLatest} */ -describe('combineLatest operator', () => { +describe('combineLatest', () => { let testScheduler: TestScheduler; beforeEach(() => { diff --git a/spec/operators/combineLatestWith-spec.ts b/spec/operators/combineLatestWith-spec.ts new file mode 100644 index 0000000000..858f0952e6 --- /dev/null +++ b/spec/operators/combineLatestWith-spec.ts @@ -0,0 +1,533 @@ +import { of } from 'rxjs'; +import { combineLatestWith, mergeMap, distinct, count, map } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { observableMatcher } from '../helpers/observableMatcher'; + +/** @test {combineLatestWith} */ +describe('combineLatestWith', () => { + let testScheduler: TestScheduler; + + beforeEach(() => { + testScheduler = new TestScheduler(observableMatcher); + }); + + it('should combine events from two cold observables', () => { + testScheduler.run(({ cold, expectObservable }) => { + const e1 = cold(' -a--b-----c-d-e-|'); + const e2 = cold(' --1--2-3-4---| '); + const expected = '--A-BC-D-EF-G-H-|'; + + const result = e1.pipe(combineLatestWith(e2), map(([a, b]) => a + b)); + + expectObservable(result).toBe(expected, { + A: 'a1', B: 'b1', C: 'b2', D: 'b3', E: 'b4', F: 'c4', G: 'd4', H: 'e4' + }); + }); + }); + + it('should work with two nevers', () => { + testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { + const e1 = cold(' -'); + const e1subs = ' ^'; + const e2 = cold(' -'); + const e2subs = ' ^'; + const expected = '-'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with never and empty', () => { + testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { + const e1 = cold(' -'); + const e1subs = ' ^'; + const e2 = cold(' |'); + const e2subs = ' (^!)'; + const expected = '-'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with empty and never', () => { + testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { + const e1 = cold(' |'); + const e1subs = ' (^!)'; + const e2 = cold(' -'); + const e2subs = ' ^'; + const expected = '-'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with empty and empty', () => { + testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { + const e1 = cold(' |'); + const e1subs = ' (^!)'; + const e2 = cold(' |'); + const e2subs = ' (^!)'; + const expected = '|'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with hot-empty and hot-single', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const values = { + a: 1, + b: 2, + c: 3, + r: 1 + 3 //a + c + }; + const e1 = hot('-a-^-|', values); + const e1subs = ' ^-!'; + const e2 = hot('-b-^-c-|', values); + const e2subs = ' ^---!'; + const expected = ' ----|'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with hot-single and hot-empty', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const values = { + a: 1, b: 2, c: 3 + }; + const e1 = hot('-a-^-|', values); + const e1subs = ' ^-!'; + const e2 = hot('-b-^-c-|', values); + const e2subs = ' ^---!'; + const expected = ' ----|'; + + const result = e2.pipe(combineLatestWith(e1), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with hot-single and never', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const values = { + a: 1 + }; + const e1 = hot('-a-^-|', values); + const e1subs = ' ^-!'; + const e2 = hot('------', values); //never + const e2subs = ' ^--'; + const expected = ' ---'; //never + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with never and hot-single', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const values = { + a: 1, b: 2 + }; + const e1 = hot('--------', values); //never + const e1subs = ' ^ '; + const e2 = hot('-a-^-b-|', values); + const e2subs = ' ^---!'; + const expected = ' -----'; //never + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with hot and hot', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--b--c--|', { a: 'a', b: 'b', c: 'c' }); + const e1subs = ' ^--------!'; + const e2 = hot('---e-^---f--g--|', { e: 'e', f: 'f', g: 'g' }); + const e2subs = ' ^---------!'; + const expected = ' ----x-yz--|'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { x: 'bf', y: 'cf', z: 'cg' }); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should accept array of observables', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--b--c--|'); + const e1subs = ' ^--------!'; + const e2 = hot('---e-^---f--g--|'); + const e2subs = ' ^---------!'; + const e3 = hot('---h-^----i--j-|'); + const e3subs = ' ^---------!'; + const expected = ' -----wxyz-|'; + + const result = e1.pipe(combineLatestWith([e2, e3], (x: string, y: string, z: string) => x + y + z)); + + expectObservable(result).toBe(expected, { w: 'bfi', x: 'cfi', y: 'cgi', z: 'cgj' }); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + expectSubscriptions(e3.subscriptions).toBe(e3subs); + }); + }); + + it('should work with empty and error', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' ----------|'); //empty + const e1subs = ' ^-----!'; + const e2 = hot(' ------#', undefined, 'shazbot!'); //error + const e2subs = ' ^-----!'; + const expected = '------#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'shazbot!'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with error and empty', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--^---#', undefined, 'too bad, honk'); //error + const e1subs = ' ^---!'; + const e2 = hot('--^--------|'); //empty + const e2subs = ' ^---!'; + const expected = '----#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'too bad, honk'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with hot and throw', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('-a-^--b--c--|', { a: 1, b: 2, c: 3}); + const e1subs = ' ^-!'; + const e2 = hot('---^-#', undefined, 'bazinga'); + const e2subs = ' ^-!'; + const expected = ' --#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'bazinga'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with throw and hot', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^-#', undefined, 'bazinga'); + const e1subs = ' ^-!'; + const e2 = hot('-a-^--b--c--|', { a: 1, b: 2, c: 3}); + const e2subs = ' ^-!'; + const expected = ' --#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'bazinga'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with throw and throw', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^----#', undefined, 'jenga'); + const e1subs = ' ^-!'; + const e2 = hot('---^-#', undefined, 'bazinga'); + const e2subs = ' ^-!'; + const expected = ' --#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'bazinga'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with error and throw', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('-a-^--b--#', { a: 1, b: 2 }, 'wokka wokka'); + const e1subs = ' ^-!'; + const e2 = hot('---^-#', undefined, 'flurp'); + const e2subs = ' ^-!'; + const expected = ' --#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'flurp'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with throw and error', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^-#', undefined, 'flurp'); + const e1subs = ' ^-!'; + const e2 = hot('-a-^--b--#', { a: 1, b: 2 }, 'wokka wokka'); + const e2subs = ' ^-!'; + const expected = ' --#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'flurp'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with never and throw', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^-----------'); + const e1subs = ' ^-----!'; + const e2 = hot('---^-----#', undefined, 'wokka wokka'); + const e2subs = ' ^-----!'; + const expected = ' ------#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'wokka wokka'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with throw and never', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^----#', undefined, 'wokka wokka'); + const e1subs = ' ^----!'; + const e2 = hot('---^-----------'); + const e2subs = ' ^----!'; + const expected = ' -----#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'wokka wokka'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with some and throw', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot(' ---^----a---b--|', { a: 1, b: 2 }); + const e1subs = ' ^--!'; + const e2 = hot(' ---^--#', undefined, 'wokka wokka'); + const e2subs = ' ^--!'; + const expected = ' ---#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { a: 1, b: 2}, 'wokka wokka'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should work with throw and some', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('---^--#', undefined, 'wokka wokka'); + const e1subs = ' ^--!'; + const e2 = hot('---^----a---b--|', { a: 1, b: 2 }); + const e2subs = ' ^--!'; + const expected = ' ---#'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { a: 1, b: 2}, 'wokka wokka'); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should handle throw after complete left', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const left = hot(' --a--^--b---|', { a: 1, b: 2 }); + const leftSubs = ' ^------!'; + const right = hot('-----^--------#', undefined, 'bad things'); + const rightSubs = ' ^--------!'; + const expected = ' ---------#'; + + const result = left.pipe(combineLatestWith(right), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'bad things'); + expectSubscriptions(left.subscriptions).toBe(leftSubs); + expectSubscriptions(right.subscriptions).toBe(rightSubs); + }); + }); + + it('should handle throw after complete right', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const left = hot(' -----^--------#', undefined, 'bad things'); + const leftSubs = ' ^--------!'; + const right = hot(' --a--^--b---|', { a: 1, b: 2 }); + const rightSubs = ' ^------!'; + const expected = ' ---------#'; + + const result = left.pipe(combineLatestWith(right), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'bad things'); + expectSubscriptions(left.subscriptions).toBe(leftSubs); + expectSubscriptions(right.subscriptions).toBe(rightSubs); + }); + }); + + it('should handle interleaved with tail', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('-a--^--b---c---|', { a: 'a', b: 'b', c: 'c' }); + const e1subs = ' ^----------!'; + const e2 = hot('--d-^----e---f--|', { d: 'd', e: 'e', f: 'f'}); + const e2subs = ' ^-----------!'; + const expected = ' -----x-y-z--|'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { x: 'be', y: 'ce', z: 'cf' }); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should handle two consecutive hot observables', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--b--c--|', { a: 'a', b: 'b', c: 'c' }); + const e1subs = ' ^--------!'; + const e2 = hot('-----^----------d--e--f--|', { d: 'd', e: 'e', f: 'f' }); + const e2subs = ' ^-------------------!'; + const expected = ' -----------x--y--z--|'; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { x: 'cd', y: 'ce', z: 'cf' }); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should handle two consecutive hot observables with error left', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const left = hot(' --a--^--b--c--#', { a: 'a', b: 'b', c: 'c' }, 'jenga'); + const leftSubs = ' ^--------!'; + const right = hot('-----^----------d--e--f--|', { d: 'd', e: 'e', f: 'f' }); + const rightSubs = ' ^--------!'; + const expected = ' ---------#'; + + const result = left.pipe(combineLatestWith(right), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, null, 'jenga'); + expectSubscriptions(left.subscriptions).toBe(leftSubs); + expectSubscriptions(right.subscriptions).toBe(rightSubs); + }); + }); + + it('should handle two consecutive hot observables with error right', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const left = hot(' --a--^--b--c--|', { a: 'a', b: 'b', c: 'c' }); + const leftSubs = ' ^--------!'; + const right = hot('-----^----------d--e--f--#', { d: 'd', e: 'e', f: 'f' }, 'dun dun dun'); + const rightSubs = ' ^-------------------!'; + const expected = ' -----------x--y--z--#'; + + const result = left.pipe(combineLatestWith(right), map(([x, y]) => x + y)); + + expectObservable(result).toBe(expected, { x: 'cd', y: 'ce', z: 'cf' }, 'dun dun dun'); + expectSubscriptions(left.subscriptions).toBe(leftSubs); + expectSubscriptions(right.subscriptions).toBe(rightSubs); + }); + }); + + it('should allow unsubscribing early and explicitly', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--b--c---d-| '); + const e1subs = ' ^--------! '; + const e2 = hot('---e-^---f--g---h-|'); + const e2subs = ' ^--------! '; + const expected = ' ----x-yz-- '; + const unsub = ' ---------! '; + const values = { x: 'bf', y: 'cf', z: 'cg' }; + + const result = e1.pipe(combineLatestWith(e2), map(([x, y]) => x + y)); + + expectObservable(result, unsub).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should not break unsubscription chains when unsubscribed explicitly', () => { + testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => { + const e1 = hot('--a--^--b--c---d-| '); + const e1subs = ' ^--------! '; + const e2 = hot('---e-^---f--g---h-|'); + const e2subs = ' ^--------! '; + const expected = ' ----x-yz-- '; + const unsub = ' ---------! '; + const values = { x: 'bf', y: 'cf', z: 'cg' }; + + const result = e1.pipe( + mergeMap((x) => of(x)), + combineLatestWith(e2), map(([x, y]) => x + y), + mergeMap((x) => of(x)) + ); + + expectObservable(result, unsub).toBe(expected, values); + expectSubscriptions(e1.subscriptions).toBe(e1subs); + expectSubscriptions(e2.subscriptions).toBe(e2subs); + }); + }); + + it('should emit unique array instances with the default projection', () => { + testScheduler.run(({ hot, expectObservable }) => { + const e1 = hot(' -a--b--|'); + const e2 = hot(' --1--2-|'); + const expected = '-------(c|)'; + + const result = e1.pipe( + combineLatestWith(e2), + distinct(), + count() + ); + + expectObservable(result).toBe(expected, { c: 3 }); + }); + }); +}); diff --git a/src/internal/operators/combineLatest.ts b/src/internal/operators/combineLatestWith.ts similarity index 50% rename from src/internal/operators/combineLatest.ts rename to src/internal/operators/combineLatestWith.ts index 5441479fd4..ce71889b47 100644 --- a/src/internal/operators/combineLatest.ts +++ b/src/internal/operators/combineLatestWith.ts @@ -3,43 +3,41 @@ import { isArray } from '../util/isArray'; import { CombineLatestOperator } from '../observable/combineLatest'; import { from } from '../observable/from'; import { Observable } from '../Observable'; -import { ObservableInput, OperatorFunction } from '../types'; - -const none = {}; +import { ObservableInput, OperatorFunction, ObservedValuesFromArray } from '../types'; /* tslint:disable:max-line-length */ -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(project: (v1: T) => R): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, project: (v1: T, v2: T2) => R): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, project: (v1: T, v2: T2, v3: T3) => R): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4) => R): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5) => R): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput, project: (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6) => R): OperatorFunction ; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): OperatorFunction ; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(...observables: Array | ((...values: Array) => R)>): OperatorFunction; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(array: ObservableInput[]): OperatorFunction>; -/** @deprecated Deprecated in favor of static combineLatest. */ +/** @deprecated use {@link combineLatestWith} */ export function combineLatest(array: ObservableInput[], project: (v1: T, ...values: Array) => R): OperatorFunction; /* tslint:enable:max-line-length */ /** - * @deprecated Deprecated in favor of static {@link combineLatest}. + * @deprecated Deprecated, use {@link combineLatestWith} or static {@link combineLatest} */ export function combineLatest(...observables: Array | Array> | @@ -60,3 +58,55 @@ export function combineLatest(...observables: Array | new CombineLatestOperator(project) ) as Observable; } +/* tslint:disable:max-line-length */ +export function combineLatestWith(v2: ObservableInput): OperatorFunction; +export function combineLatestWith(v2: ObservableInput, v3: ObservableInput): OperatorFunction; +export function combineLatestWith(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput): OperatorFunction; +export function combineLatestWith(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput): OperatorFunction; +export function combineLatestWith(v2: ObservableInput, v3: ObservableInput, v4: ObservableInput, v5: ObservableInput, v6: ObservableInput): OperatorFunction ; +export function combineLatestWith[]>( + ...otherSources: A +): OperatorFunction>>; +/* tslint:enable:max-line-length */ + +/** + * Create an observable that combines the latest values from all passed observables and the source + * into arrays and emits them. + * + * Returns an observable, that when subscribed to, will subscribe to the source observable and all + * sources provided as arguments. Once all sources emit at least one value, all of the latest values + * will be emitted as an array. After that, every time any source emits a value, all of the latest values + * will be emitted as an array. + * + * This is a useful operator for eagerly calculating values based off of changed inputs. + * + * ### Example + * + * Simple calculation from two inputs. + * + * ``` + * // Setup: Add two inputs to the page + * const input1 = document.createElement('input'); + * document.body.appendChild(input1); + * const input2 = document.createElement('input'); + * document.body.appendChild(input2); + * + * // Get streams of changes + * const input1Changes$ = fromEvent(input1, 'change'); + * const input2Changes$ = fromEvent(input2, 'change'); + * + * // Combine the changes by adding them together + * input1Changes$.pipe( + * combineLatestWith(input2Changes$), + * map(([e1, e2]) => Number(e1.target.value) + Number(e2.target.value)), + * ) + * .subscribe(x => console.log(x)); + * + * ``` + * @param otherSources the other sources to subscribe to. + */ +export function combineLatestWith[]>( + ...otherSources: A +): OperatorFunction>> { + return combineLatest(...otherSources); +} \ No newline at end of file diff --git a/src/internal/operators/index.ts b/src/internal/operators/index.ts index 7321559511..a3df1d2640 100644 --- a/src/internal/operators/index.ts +++ b/src/internal/operators/index.ts @@ -7,7 +7,7 @@ export { bufferToggle } from './bufferToggle'; export { bufferWhen } from './bufferWhen'; export { catchError } from './catchError'; export { combineAll } from './combineAll'; -export { combineLatest } from './combineLatest'; +export { combineLatest, combineLatestWith } from './combineLatestWith'; export { concat } from './concat'; export { concatAll } from './concatAll'; export { concatMap } from './concatMap'; diff --git a/src/operators/index.ts b/src/operators/index.ts index 80a311fe10..fe89d62356 100644 --- a/src/operators/index.ts +++ b/src/operators/index.ts @@ -9,7 +9,7 @@ export { bufferToggle } from '../internal/operators/bufferToggle'; export { bufferWhen } from '../internal/operators/bufferWhen'; export { catchError } from '../internal/operators/catchError'; export { combineAll } from '../internal/operators/combineAll'; -export { combineLatest } from '../internal/operators/combineLatest'; +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';