forked from ReactiveX/rxjs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(switchScan): add switchScan operator
- Loading branch information
Showing
4 changed files
with
415 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,380 @@ | ||
import { expect } from 'chai'; | ||
import { concat, defer, Observable, of } from 'rxjs'; | ||
import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing'; | ||
import { switchScan, map, mergeMap, takeWhile } from 'rxjs/operators'; | ||
|
||
/** @test {switchScan} */ | ||
describe.only('switchScan', () => { | ||
it('should map-and-flatten each item to an Observable while passing the seed', () => { | ||
const e1 = hot('--1-----3--5-------|'); | ||
const e1subs = '^ !'; | ||
const e2 = cold('x-x-x| ', {x: 10}); | ||
const expected = '--x-x-x-y-yz-z-z---|'; | ||
const values = {x: 10, y: 40, z: 90}; | ||
|
||
const result = e1.pipe(switchScan((acc, x) => e2.pipe(map(i => i * Number(x) + acc)), 0)); | ||
|
||
expectObservable(result).toBe(expected, values); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should pass seed even when seed is not defined', () => { | ||
const seeds: any[] = []; | ||
const result = of(1, 3, 5).pipe(switchScan((acc, x) => { | ||
seeds.push(acc); | ||
return of(10).pipe(map(v => (v * Number(x)) + ((acc || 0) as number))); | ||
})).subscribe(); | ||
|
||
expect(seeds).to.deep.equal([undefined, 10, 40]); | ||
}); | ||
|
||
it('should unsub inner observables', () => { | ||
const unsubbed: string[] = []; | ||
|
||
of('a', 'b').pipe( | ||
switchScan((acc, x) => | ||
new Observable<string>((subscriber) => { | ||
subscriber.complete(); | ||
return () => { | ||
unsubbed.push(x); | ||
}; | ||
}), | ||
) | ||
).subscribe(); | ||
|
||
expect(unsubbed).to.deep.equal(['a', 'b']); | ||
}); | ||
|
||
it('should switch inner cold observables', () => { | ||
const x = cold( '--a--b--c--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = cold( '---f---g---h---i--|'); | ||
const ysubs = ' ^ !'; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------a--b--c----f---g---h---i--|'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should raise error when projection throws', () => { | ||
const e1 = hot('-------x-----y---|'); | ||
const e1subs = '^ ! '; | ||
const expected = '-------# '; | ||
function project(): any[] { | ||
throw 'error'; | ||
} | ||
|
||
expectObservable(e1.pipe(switchScan(project))).toBe(expected); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner cold observables, outer is unsubscribed early', () => { | ||
const x = cold( '--a--b--c--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = cold( '---f---g---h---i--|'); | ||
const ysubs = ' ^ ! '; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const unsub = ' ! '; | ||
const expected = '-----------a--b--c---- '; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result, unsub).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should not break unsubscription chains when result is unsubscribed explicitly', () => { | ||
const x = cold( '--a--b--c--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = cold( '---f---g---h---i--|'); | ||
const ysubs = ' ^ ! '; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------a--b--c---- '; | ||
const unsub = ' ! '; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe( | ||
mergeMap(x => of(x)), | ||
switchScan((acc, value) => observableLookup[value]), | ||
mergeMap(x => of(x)), | ||
); | ||
|
||
expectObservable(result, unsub).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should stop listening to a synchronous observable when unsubscribed', () => { | ||
const sideEffects: number[] = []; | ||
const synchronousObservable = concat( | ||
defer(() => { | ||
sideEffects.push(1); | ||
return of(1); | ||
}), | ||
defer(() => { | ||
sideEffects.push(2); | ||
return of(2); | ||
}), | ||
defer(() => { | ||
sideEffects.push(3); | ||
return of(3); | ||
}) | ||
); | ||
|
||
of(null).pipe( | ||
switchScan(() => synchronousObservable), | ||
takeWhile((x) => x != 2) // unsubscribe at the second side-effect | ||
).subscribe(() => { /* noop */ }); | ||
|
||
expect(sideEffects).to.deep.equal([1, 2]); | ||
}); | ||
|
||
it('should switch inner cold observables, inner never completes', () => { | ||
const x = cold( '--a--b--c--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = cold( '---f---g---h---i--'); | ||
const ysubs = ' ^ '; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------a--b--c----f---g---h---i--'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should handle a synchronous switch to the second inner observable', () => { | ||
const x = cold( '--a--b--c--d--e--| '); | ||
const xsubs = ' (^!) '; | ||
const y = cold( '---f---g---h---i--| '); | ||
const ysubs = ' ^ ! '; | ||
const e1 = hot('---------(xy)----------------|'); | ||
const e1subs = '^ !'; | ||
const expected = '------------f---g---h---i----|'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner cold observables, one inner throws', () => { | ||
const x = cold( '--a--b--#--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = cold( '---f---g---h---i--'); | ||
const ysubs: string[] = []; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------a--b--# '; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner hot observables', () => { | ||
const x = hot('-----a--b--c--d--e--| '); | ||
const xsubs = ' ^ ! '; | ||
const y = hot('--p-o-o-p-------------f---g---h---i--|'); | ||
const ysubs = ' ^ !'; | ||
const e1 = hot('---------x---------y---------| '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------c--d--e----f---g---h---i--|'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner empty and empty', () => { | ||
const x = cold('|'); | ||
const y = cold('|'); | ||
const xsubs = ' (^!) '; | ||
const ysubs = ' (^!) '; | ||
const e1 = hot('---------x---------y---------|'); | ||
const e1subs = '^ !'; | ||
const expected = '-----------------------------|'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner empty and never', () => { | ||
const x = cold('|'); | ||
const y = cold('-'); | ||
const xsubs = ' (^!) '; | ||
const ysubs = ' ^ '; | ||
const e1 = hot('---------x---------y---------|'); | ||
const e1subs = '^ !'; | ||
const expected = '------------------------------'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner never and empty', () => { | ||
const x = cold('-'); | ||
const y = cold('|'); | ||
const xsubs = ' ^ ! '; | ||
const ysubs = ' (^!) '; | ||
const e1 = hot('---------x---------y---------|'); | ||
const e1subs = '^ !'; | ||
const expected = '-----------------------------|'; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner never and throw', () => { | ||
const x = cold('-'); | ||
const y = cold('#', null, 'sad'); | ||
const xsubs = ' ^ ! '; | ||
const ysubs = ' (^!) '; | ||
const e1 = hot('---------x---------y---------|'); | ||
const e1subs = '^ ! '; | ||
const expected = '-------------------# '; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected, undefined, 'sad'); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should switch inner empty and throw', () => { | ||
const x = cold('|'); | ||
const y = cold('#', null, 'sad'); | ||
const xsubs = ' (^!) '; | ||
const ysubs = ' (^!) '; | ||
const e1 = hot('---------x---------y---------|'); | ||
const e1subs = '^ ! '; | ||
const expected = '-------------------# '; | ||
|
||
const observableLookup = { x: x, y: y }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected, undefined, 'sad'); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(y.subscriptions).toBe(ysubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should handle outer empty', () => { | ||
const e1 = cold('|'); | ||
const e1subs = '(^!)'; | ||
const expected = '|'; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => of(value))); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should handle outer never', () => { | ||
const e1 = cold('-'); | ||
const e1subs = '^'; | ||
const expected = '-'; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => of(value))); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should handle outer throw', () => { | ||
const e1 = cold('#'); | ||
const e1subs = '(^!)'; | ||
const expected = '#'; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => of(value))); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should handle outer error', () => { | ||
const x = cold( '--a--b--c--d--e--|'); | ||
const xsubs = ' ^ ! '; | ||
const e1 = hot('---------x---------# '); | ||
const e1subs = '^ ! '; | ||
const expected = '-----------a--b--c-# '; | ||
|
||
const observableLookup = { x: x }; | ||
|
||
const result = e1.pipe(switchScan((acc, value) => observableLookup[value])); | ||
|
||
expectObservable(result).toBe(expected); | ||
expectSubscriptions(x.subscriptions).toBe(xsubs); | ||
expectSubscriptions(e1.subscriptions).toBe(e1subs); | ||
}); | ||
|
||
it('should pass index to the project function', () => { | ||
const indices: number[] = []; | ||
|
||
of('a', 'b', 'c', 'd').pipe( | ||
switchScan((acc, x, index) => { | ||
indices.push(index); | ||
return of(); | ||
}), | ||
).subscribe(); | ||
|
||
expect(indices).to.deep.equal([0, 1, 2, 3]); | ||
}); | ||
}); |
Oops, something went wrong.