Skip to content

Commit cb393dc

Browse files
committed
feat(withLatestFrom): default array output, handle other types
- now handles promises, iteratables, lowercase-o observables and Observables - updates marble tests to be more comprehensive - fixes issue where values were emitted before all observables responded - makes project function optional and will output arrays - updates documentation
1 parent 4d37812 commit cb393dc

File tree

2 files changed

+169
-52
lines changed

2 files changed

+169
-52
lines changed

spec/operators/withLatestFrom-spec.js

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,117 @@
1-
/* globals describe, it, expect */
1+
/* globals describe, it, expect, expectObservable, hot, cold, lowerCaseO */
22
var Rx = require('../../dist/cjs/Rx');
33
var Observable = Rx.Observable;
4+
var Promise = require('promise');
45

5-
describe('Observable.prototype.withLatestFrom()', function () {
6-
it('should merge the emitted value with the latest values of the other observables', function (done) {
7-
var a = Observable.of('a');
8-
var b = Observable.of('b', 'c');
9-
10-
Observable.of('d').delay(100)
11-
.withLatestFrom(a, b, function (x, a, b) { return [x, a, b]; })
12-
.subscribe(function (x) {
13-
expect(x).toEqual(['d', 'a', 'c']);
14-
}, null, done);
6+
describe('Observable.prototype.withLatestFrom()', function () {
7+
it('should merge the value with the latest values from the other observables into arrays', function () {
8+
var e1 = hot('--a--^---b---c---d--|');
9+
var e2 = hot('--e--^-f---g---h----|');
10+
var e3 = hot('--i--^-j---k---l----|');
11+
var expected = '----x---y---z--|';
12+
var values = {
13+
x: ['b', 'f', 'j'],
14+
y: ['c', 'g', 'k'],
15+
z: ['d', 'h', 'l']
16+
};
17+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected, values);
1518
});
1619

17-
it('should emit nothing if the other observables never emit', function (done) {
18-
var a = Observable.of('a');
19-
var b = Observable.never();
20-
21-
Observable.of('d').delay(100)
22-
.withLatestFrom(a, b, function (x, a, b) { return [x, a, b]; })
23-
.subscribe(function (x) {
24-
expect('this was called').toBe(false);
20+
it('should merge the value with the latest values from the other observables into arrays and a project argument', function () {
21+
var e1 = hot('--a--^---b---c---d--|');
22+
var e2 = hot('--e--^-f---g---h----|');
23+
var e3 = hot('--i--^-j---k---l----|');
24+
var expected = '----x---y---z--|';
25+
var values = {
26+
x: 'bfj',
27+
y: 'cgk',
28+
z: 'dhl'
29+
};
30+
var project = function(a, b, c) { return a + b + c };
31+
expectObservable(e1.withLatestFrom(e2, e3, project)).toBe(expected, values);
32+
});
33+
34+
it('should handle empty', function (){
35+
var e1 = Observable.empty();
36+
var e2 = hot('--e--^-f---g---h----|');
37+
var e3 = hot('--i--^-j---k---l----|');
38+
var expected = '|'; // empty
39+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected);
40+
});
41+
42+
it('should handle never', function (){
43+
var e1 = Observable.never();
44+
var e2 = hot('--e--^-f---g---h----|');
45+
var e3 = hot('--i--^-j---k---l----|');
46+
var expected = '--------------------'; // never
47+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected);
48+
});
49+
50+
it('should handle throw', function (){
51+
var e1 = Observable.throw(new Error('sad'));
52+
var e2 = hot('--e--^-f---g---h----|');
53+
var e3 = hot('--i--^-j---k---l----|');
54+
var expected = '#'; // throw
55+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected, null, new Error('sad'));
56+
});
57+
58+
it('should handle error', function (){
59+
var e1 = hot('--a--^---b---#', undefined, new Error('boo-hoo'));
60+
var e2 = hot('--e--^-f---g---h----|');
61+
var e3 = hot('--i--^-j---k---l----|');
62+
var expected = '----x---#'; // throw
63+
var values = {
64+
x: ['b','f','j']
65+
};
66+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected, values, new Error('boo-hoo'));
67+
});
68+
69+
it('should handle error with project argument', function (){
70+
var e1 = hot('--a--^---b---#', undefined, new Error('boo-hoo'));
71+
var e2 = hot('--e--^-f---g---h----|');
72+
var e3 = hot('--i--^-j---k---l----|');
73+
var expected = '----x---#'; // throw
74+
var values = {
75+
x: 'bfj'
76+
};
77+
var project = function(a, b, c) { return a + b + c; };
78+
expectObservable(e1.withLatestFrom(e2, e3, project)).toBe(expected, values, new Error('boo-hoo'));
79+
});
80+
81+
it('should handle merging with empty', function (){
82+
var e1 = hot('--a--^---b---c---d--|');
83+
var e2 = Observable.empty();
84+
var e3 = hot('--i--^-j---k---l----|');
85+
var expected = '---------------|';
86+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected);
87+
});
88+
89+
it('should handle merging with never', function (){
90+
var e1 = hot('--a--^---b---c---d--|');
91+
var e2 = Observable.never();
92+
var e3 = hot('--i--^-j---k---l----|');
93+
var expected = '---------------|';
94+
expectObservable(e1.withLatestFrom(e2, e3)).toBe(expected);
95+
});
96+
97+
it('should handle promises', function (done){
98+
Observable.of(1).delay(1).withLatestFrom(Promise.resolve(2), Promise.resolve(3))
99+
.subscribe(function(x) {
100+
expect(x).toEqual([1,2,3]);
25101
}, null, done);
26102
});
103+
104+
it('should handle arrays', function() {
105+
Observable.of(1).delay(1).withLatestFrom([2,3,4], [4,5,6])
106+
.subscribe(function(x) {
107+
expect(x).toEqual([1,4,6]);
108+
});
109+
});
110+
111+
it('should handle lowercase-o observables', function (){
112+
Observable.of(1).delay(1).withLatestFrom(lowerCaseO(2, 3, 4), lowerCaseO(4, 5, 6))
113+
.subscribe(function(x) {
114+
expect(x).toEqual([1,4,6]);
115+
});
116+
});
27117
});

src/operators/withLatestFrom.ts

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,95 @@ import Observable from '../Observable';
55

66
import tryCatch from '../util/tryCatch';
77
import {errorObject} from '../util/errorObject';
8+
import OuterSubscriber from '../OuterSubscriber';
9+
import subscribeToResult from '../util/subscribeToResult';
810

11+
/**
12+
* @param {Observable} observables the observables to get the latest values from.
13+
* @param {Function} [project] optional projection function for merging values together. Receives all values in order
14+
* of observables passed. (e.g. `a.withLatestFrom(b, c, (a1, b1, c1) => a1 + b1 + c1)`). If this is not passed, arrays
15+
* will be returned.
16+
* @description merges each value from an observable with the latest values from the other passed observables.
17+
* All observables must emit at least one value before the resulting observable will emit
18+
*
19+
* #### example
20+
* ```
21+
* A.withLatestFrom(B, C)
22+
*
23+
* A: ----a-----------------b---------------c-----------|
24+
* B: ---d----------------e--------------f---------|
25+
* C: --x----------------y-------------z-------------|
26+
* result: ---([a,d,x])---------([b,e,y])--------([c,f,z])---|
27+
* ```
28+
*/
929
export default function withLatestFrom<R>(...args: (Observable<any>|((...values: any[]) => Observable<R>))[]): Observable<R> {
10-
const project = <((...values: any[]) => Observable<R>)>args.pop();
30+
let project;
31+
if(typeof args[args.length - 1] === 'function') {
32+
project = args.pop();
33+
}
1134
const observables = <Observable<any>[]>args;
1235
return this.lift(new WithLatestFromOperator(observables, project));
1336
}
1437

1538
class WithLatestFromOperator<T, R> implements Operator<T, R> {
16-
constructor(private observables: Observable<any>[], private project: (...values: any[]) => Observable<R>) {
39+
constructor(private observables: Observable<any>[], private project?: (...values: any[]) => Observable<R>) {
1740
}
1841

1942
call(subscriber: Subscriber<T>): Subscriber<T> {
2043
return new WithLatestFromSubscriber<T, R>(subscriber, this.observables, this.project);
2144
}
2245
}
2346

24-
class WithLatestFromSubscriber<T, R> extends Subscriber<T> {
47+
class WithLatestFromSubscriber<T, R> extends OuterSubscriber<T, R> {
2548
private values: any[];
26-
private toSet: number;
49+
private toRespond: number[] = [];
2750

28-
constructor(destination: Subscriber<T>, private observables: Observable<any>[], private project: (...values: any[]) => Observable<R>) {
51+
constructor(destination: Subscriber<T>, private observables: Observable<any>[], private project?: (...values: any[]) => Observable<R>) {
2952
super(destination);
3053
const len = observables.length;
3154
this.values = new Array(len);
32-
this.toSet = len;
55+
56+
for (let i = 0; i < len; i++) {
57+
this.toRespond.push(i);
58+
}
59+
3360
for (let i = 0; i < len; i++) {
34-
this.add(observables[i]._subscribe(new WithLatestInnerSubscriber(this, i)))
61+
let observable = observables[i];
62+
this.add(subscribeToResult<T, R>(this, observable, <any>observable, i));
63+
}
64+
}
65+
66+
notifyNext(value, observable, index, observableIndex) {
67+
this.values[observableIndex] = value;
68+
const toRespond = this.toRespond;
69+
if(toRespond.length > 0) {
70+
const found = toRespond.indexOf(observableIndex);
71+
if(found !== -1) {
72+
toRespond.splice(found, 1);
73+
}
3574
}
3675
}
3776

38-
notifyValue(index, value) {
39-
this.values[index] = value;
40-
this.toSet--;
77+
notifyComplete() {
78+
// noop
4179
}
4280

4381
_next(value: T) {
44-
if (this.toSet === 0) {
82+
if (this.toRespond.length === 0) {
4583
const values = this.values;
46-
let result = tryCatch(this.project)([value, ...values]);
47-
if (result === errorObject) {
48-
this.destination.error(result.e);
84+
const destination = this.destination;
85+
const project = this.project;
86+
const args = [value, ...values];
87+
if(project) {
88+
let result = tryCatch(this.project).apply(this, args);
89+
if (result === errorObject) {
90+
destination.error(result.e);
91+
} else {
92+
destination.next(result);
93+
}
4994
} else {
50-
this.destination.next(result);
95+
destination.next(args);
5196
}
5297
}
5398
}
5499
}
55-
56-
class WithLatestInnerSubscriber<T, R> extends Subscriber<T> {
57-
constructor(private parent: WithLatestFromSubscriber<T, R>, private valueIndex: number) {
58-
super(null)
59-
}
60-
61-
_next(value: T) {
62-
this.parent.notifyValue(this.valueIndex, value);
63-
}
64-
65-
_error(err: any) {
66-
this.parent.error(err);
67-
}
68-
69-
_complete() {
70-
// noop
71-
}
72-
}

0 commit comments

Comments
 (0)