I have two observable streams, each emitting a series of items over an infinite period of time (similar to how a DOM-based click Observable would behave). I know an item from Observable A (a$) will match an item from Observable B (b$), but need to do some custom logic to determine which items match.
I tried to make this work, but I could only get the first pair to match, and then subsequent items never emit again...
This is an extract from the code:
a$.pipe(
mergeMap(a => {
return b$.pipe(
filter(b => b.key.includes(a.subKey)), // custom matching logic goes here
take(1),
map(b => ({ a, b }))
);
})
)
.subscribe(({ a, b }) => {
console.log("do something with a and b", a, b);
});
Note that both Observables never complete, so if some item a from a$ emitted, its "pair" might not have been emitted from b yet. That's why I used filter and not find above. When I did find a matching item, I can complete the inner observable, since that pair has been matched & handled.
Please advise, what am I missing?
Have you looked at combine latest? It emits the latest value for both streams once both have emitted once.
combineLatest(a$, b$).pipe(filter(([a, b]) => b.key.includes(a.subKey)))
.subscribe(([a, b]) => {
// Do stuff with an and b here
});
I think one way to solve this is to first accumulate the values of each observable in a map, then to use the combineLatest operator so you can check for pairs on every emission.
const aMap$ = a$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
const bMap$ = b$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
combineLatest(aMap$, bMap$)
.pipe(
map(([aMap, bMap]) => {
let pair = null;
for (const bKey in bMap) {
const bVal = bMap[bKey];
const aPairKey = bVal.keys.find(k => !!aMap[k]);
if (aPairKey) {
pair = { a: aMap[aPairKey], b: bVal };
delete aMap[aPairKey];
delete bMap[bKey];
break;
}
}
return pair;
}),
filter(v => !!v)
)
I would accumulate the values from A and B to see which values happened yet. After you can create an intersection from those arrays
const keysOccuredInA$ = a$.pipe(
map(a => a.subKey),
scan((acc, curr) => ([...acc, curr]), []),
);
const keysOccuredInB$ = b$.pipe(
map(b => b.key), // b.key is an array, right?
scan((acc, curr) => ([...acc, ...curr]), []),
);
keysOccuredInBoth$ = combineLatest(keysOccuredInA$, keysOccuredInB$).pipe(
map(([keysOccuredInA, keysOccuredInB]) => _intersection(keysOccuredInA, keysOccuredInB)), // lodash intersection
)
Related
i'm kinda new to rxjs and can't get my head around this problem:
I have two streams:
one with incoming objects
---a----b----c----d----->
one with the selected object from a list
-------------------c---->
From the incoming objects stream make a stream of the list of objects (with scan operator)
incoming: ----a--------b-------c----------d----------------\>
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]---------\>
When a list object is selected (n), start a new stream
the first value of the new stream is the last value of the list sliced ( list.slice(n))
incoming: ----a--------b-------c----------d--------------------e-------->
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]--------->
selected object: ---------------------------------c------->
new stream of list: ------[c,d]-----[c,d,e]--->
i can't get the last value of the list stream when the object is selected,,,
made a marble diagram for better understanding,
selectedObject$ = new BehaviorSubject(0);
incomingObjects$ = new Subject();
list$ = incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
newList$ = selectedObject$.pipe(
withLastFrom(list$),
switchMap(([index,list])=> incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, list.slice(index))
))
)
A common pattern I use along with the scan operator is passing reducer functions instead of values to scan so that the current value can be used in the update operation. In this case you can link the two observables with a merge operator and map their values to functions that are appropriate - either adding to a list, or slicing the list after a selection.
// these are just timers for demonstration, any observable should be fine.
const incoming$ = timer(1000, 1000).pipe(map(x => String.fromCharCode(x + 65)), take(10));
const selected$ = timer(3000, 3000).pipe(map(x => String.fromCharCode(x * 2 + 66)), take(2));
merge(
incoming$.pipe(map(x => (s) => [...s, x])), // append to list
selected$.pipe(map(x => (s) => { // slice list starting from selection
const index = s.indexOf(x);
return (index !== -1) ? s.slice(index) : s;
}))
).pipe(
scan((list, reducer) => reducer(list), []) // run reducer
).subscribe(x => console.log(x)); // display list state as demonstration.
If I understand the problem right, you could follow the following approach.
The key point is to recognize that the list Observable (i.e. the Observable obtained with the use of scan) should be an hot Observable, i.e. an Observable that notifies independent on whether or not it is subscribed. The reason is that each new stream you want to create should have always the same source Observable as its upstream.
Then, as you already hint, the act of selecting a value should be modeled with a BehaviorSubject.
As soon as the select BehaviorSubject notifies a value selected, the previous stream has to complete and a new one has to be subscribed. This is the job of switchMap.
The rest is to slice the arrays of numbers in the right way.
This is the complete code of this approach
const selectedObject$ = new BehaviorSubject(1);
const incomingObjects$ = interval(1000).pipe(take(10));
const incomingObjectsHot$ = new ReplaySubject<number[]>(1);
incomingObjects$
.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
.subscribe(incomingObjectsHot$);
selectedObject$
.pipe(
switchMap((selected) =>
incomingObjectsHot$.pipe(
map((nums) => {
const selIndex = nums.indexOf(selected);
if (selIndex > 0) {
return nums.slice(selIndex);
}
})
)
),
filter(v => !!v)
)
.subscribe(console.log);
An example can be seen in this stackblitz.
Is there an RxJS factory function (or more generally a pattern) to merge several others together, but only emit once when all of them have completed?
The use case I want this for, is to wait for several parallel operations, and then do something else, once they complete. For promises this can be done like this:
Promise.all(A, B, C).then(() => console.log('done'));
For observables, the best I've come up with yet is
merge(A, B, C).pipe(takeLatest(1)).subscribe(() => console.log('done'));
This doesn't account for A, B, and C being empty, though. And it doesn't provide a deterministic value to the subscriber. Isn't there a simple built-in solution for this use-case?
You can use forkJoin. This operator emits once all its given observables are completed.
const { Observable, of, forkJoin } = rxjs;
const obs1$ = new Observable(subscriber => {
subscriber.next('obs1$ - value 1');
subscriber.next('obs1$ - value 2');
subscriber.complete();
})
const obs2$ = of('obs2$');
const obs3$ = of('obs3$');
const result$ = forkJoin(
obs1$,
obs2$,
obs3$
);
result$.subscribe(v => console.log(v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';
The following
Rx.Observable.zip(
Rx.Observable.of(1,2),
Rx.Observable.of("a"))
.subscribe(p => console.log(p))
produces
1,a
which makes sense, but what I want it to produce is
1,a
2,undefined
I want to pad the shorter observable with undefineds until the longer one completes. Any suggestions?
I realise adding delay can turn the .of operator async and with scan you can replace the same value with undefine
Rx.Observable.combineLatest(
Rx.Observable.of(1,2).delay(0),
Rx.Observable.of("a"))
.scan((acc,curr)=>{
acc[1]=acc[1]==curr[1]?undefined:curr[1]
acc[0]=acc[0]==curr[0]?undefined:curr[0]
return acc
},[])
.subscribe(p => console.log(p))
I think the key to this is to ensure that all of the source observables are the same length.
One solution would be to compose a counter observable that's as long as the longest source observable. It could then be concatenated to the shorter source observables, like this:
const pad = (...sources) => Rx.Observable.create(observer => {
// Publish the source observables to make them multicast
// and to allow the subscription order to be managed.
const publishedSources = sources.map(source => source.publish());
// Create an observable that emits an incremented index and
// is as long as the longest source observable.
const counter = Rx.Observable
.merge(...publishedSources.map(
source => source.map((unused, index) => index)
))
.scan((max, index) => Math.max(max, index), 0)
.distinctUntilChanged()
.publish();
// Zip the published sources, contatenating the counter so
// that they are all the same length. When the counter
// emissions are concatenated, they are mapped to undefined.
const subscription = Rx.Observable.zip(...publishedSources.map(
source => source.concat(counter.mapTo(undefined))
)).subscribe(observer);
// Connect the counter and the published sources.
subscription.add(counter.connect());
publishedSources.forEach(
source => subscription.add(source.connect())
);
return subscription;
});
pad(
Rx.Observable.of(1, 2),
Rx.Observable.of("a")
).subscribe(padded => console.log(padded));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
My own solution is below. The idea is that all the observables are converted into endless streams, starting with the original values, enclosed in objects, and then an infinite number of undefineds. These endless streams are concatted and the result is taken as long as any of the values are original.
const unending = (obs) =>
obs.map(value => ({value}))
.concat(Rx.Observable
.interval(0)
.mapTo(undefined));
const zipPad = (...obss) =>
Rx.Observable.zip(...obss.map(unending))
.takeWhile(p => p.find(v => v))
.map(p => p.map(v => v && v.value));
zipPad(Rx.Observable.of(1,2,3),
Rx.Observable.of("a"),
Rx.Observable.of("x", "y"))
.subscribe(p => console.log(p));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.min.js"></script>
Anyone is welcome to improve on this answer and post their variations here.
I need to wait on 2 observables to process a request, the types of which are different, e.g.
observable1 = Observable.create((observer: Subscriber<Type1> => ...)
and
observable2 = Observable.create((observer: Subscriber<Type2> => ...)
How can I avoid nested subscriptions such as
observable1.subscribe(result1 => {
observable2.subscribe(result2 => {
...
<do something with result1 and result2>
...
}
}
I tried
observable1.concat(observable2).subscribe(result => ...)
but that appears to read from both observables in turn.
I'm thinking along the lines of
observable1<???>observable2.subscribe((result1, result2) => ...)
How can I do that?
You're correct: concat will read from the observables in turn. To "join" them (by ordinal position), you will want to use zip:
const a$ = Rx.Observable.just(1);
const b$ = Rx.Observable.just(2);
const combined$ = a$.zip(b$, (x, y) => ({ a: x, b: y }));
const subscription = combined$.subscribe(x => console.log(x));
Note that the fact that the two streams are of different types causes no inconvenience when using zip, as a function must be provided to produce a new value from the two "zipped" values.