RxJS: merge with single emit on completion - rxjs

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>

Related

Chaining two dependent observables in rxjs

I have an observable that emits a series of messages, say obs1. Then a second observable, obs2, that needs some data from the last message emitted by obs1 and emits another series of messages. I would like to "chain" these 2 observables to produce an observable obs3 that serially emits ALL messages from obs1 and obs2.
The solution I came up with so far is:
obs3 = concat(
obs1,
obs1.pipe(
last(),
concatMap(lastMessage => obs2(lastMessage)),
);
But this has the flaw that obs1 is executed (subscribed to) 2 times.
Is there a more direct way to achieve this? Something like a concatMapWithSelf() operator that would work like this:
obs3 = obs1.pipe(
concatMapWithSelf(lastMessage => obs2(lastMessage)),
);
Thank you!
Sounds like you could use ConnectableObservable. In RxJS 7 I believe it would be even easier and better readable with multicast() but that's going to be deprecated in RxJS 8 so the only option is probably wrapping the source Observable with connectable() and then manually calling connect().
const obs1 = connectable(
defer(() => {
console.log('new subscription');
return of('v1', 'v2', 'v3', 'v4');
})
);
const obs2 = msg => of(msg);
const obs3 = merge(
obs1,
obs1.pipe(
last(),
concatMap(lastMessage => obs2(lastMessage))
)
);
obs3.subscribe(console.log);
obs1.connect();
Live demo: https://stackblitz.com/edit/rxjs-2uheg4?devtoolsheight=60
If obs1 is always asynchronous then probably you could use share() but that would behave differently with synchronous sources so using ConnectableObservable should be more safe.

How to recompute observable once for a single event loop tick?

I have a use case where I have one observable shared across multiple observables where finally those 2 observable merged into a single one.
The issue is, whenever I click the document, I get subscription callback called twice, but I need it to be called once containing the last value:
import { fromEvent, of, combineLatest } from "rxjs";
import { map, switchMap, startWith } from "rxjs/operators";
const sharedState$ = fromEvent(document, "click").pipe(
map((event) => event.clientX),
startWith(0)
);
const stateA$ = of(5).pipe(
switchMap((a) => sharedState$.pipe(map((x) => x + a)))
);
const stateB$ = of(7).pipe(
switchMap((b) => sharedState$.pipe(map((x) => x + b)))
);
const result$ = combineLatest([stateA$, stateB$]).pipe(map(([a, b]) => a + b));
result$.subscribe(console.log);
Here it is the codesandbox link for the convenience:
Technically, I understand why it's happening. First sharedState$ update is propagated to stateA$ and therefore the whole result$ is recomputed. And next the same thing is happening with stateB$.
But both these updates are happening synchronously one immediately after another in a single event loop tick, which make sense to result in a single subscriber notification of result$ stream.
Finally, I came up applying debounceTime(0) operator on the result$ stream, which solved the problem. But I have a feeling that it's more a workaround than the actual solution:
// works, but I'm not sure whether it's the right way
const result$ = combineLatest([stateA$, stateB$]).pipe(
map(([a, b]) => a + b),
debounceTime(0)
);
What's the idiomatic way of handling such things?
PS. Obviously, for that specific use-case it can be solved differently, which will result in a single subscriber call, but my real use case is quite complicated and unfortunately the pieces being merged together into a final observable composed dynamically and can include the same observable inside.
This will solve the issue you mentioned:
import { zip } from "rxjs";
import { distinctUntilChanged } from "rxjs/operators";
const result$ = zip([stateA$, stateB$]).pipe(map(([a, b]) => a + b), distinctUntilChanged());
For the cases where we need to trigger when any observable emits:
Just adding an operator to your initial solution, so it will not emit multiple times on clicking the same point.
const result$ = combineLatest([stateA$, stateB$]).pipe(
map(([a, b]) => a + b),
debounceTime(0),
distinctUntilChanged()
);

In rxjs how can I create groups of sequential values?

I have a stream of values in this form:
[1,2,3,1,2,1,1,1,2...]
I would like to convert it to a stream of groups in the form:
[[1,2,3],[1,2],[1],[1],[1,2]...]
New group should be created every time value becomes 1.
You can use the bufferWhen() operator to collect emitted values until an observable emits a value. In this example I use a subject to emit a shallow copy of the stream to the buffer.
The buffer will emit when ever the number 1 is emitted. If the stream starts with 1 then an empty array is emitted. So I filter that away.
const {from, Subject} = rxjs;
const {filter, bufferWhen, tap, skip} = rxjs.operators;
const stream = from([1,2,3,1,2,1,1,1,2]);
const trigger = new Subject();
stream.pipe(
tap(v => trigger.next(v)),
bufferWhen(() => trigger.pipe(filter(v => v === 1))),
filter(v => v.length)
).subscribe(x => console.log(x));
<script src="https://unpkg.com/#reactivex/rxjs#6.x/dist/global/rxjs.umd.js"></script>
You can use the scan() if you wanted to emit the trigger when the value decreases from the previous. Which is maybe a little better in logic, but using 1 as the trigger fits with the question.
For the record, I'm posting a solution alternative to the accepted answer.
NOTE: For finite streams concat(of(1)) is required before scan(), for distinctUntilChanged to emit the last observable.
const {of, from, ReplaySubject} = rxjs;
const {map, concat, scan, distinctUntilChanged, concatAll, toArray, delay} = rxjs.operators;
//considering infinite stream
const stream = from([1,2,3,1,2,1,1,1,2]);
stream.pipe(
scan( (acc, val) => {
if(val===1){
acc.complete()
acc=new ReplaySubject();
}
acc.next(val);
return acc;
}, new ReplaySubject()),
distinctUntilChanged(),
map(toArray()),
concatAll() );
It would be nice to collect some feedback regarding preferred solution.

rxjs concatMap with array

There is Higher Order Observable observable
const obs1 = interval(1000).pipe(take(5));
const obs2 = interval(500).pipe(take(2));
const obs3 = interval(2000).pipe(take(1));
//emit three observables
const source = of(obs1, obs2, obs3);
How to use concatMap in order to as soon as the previous observable is completed, the next one immediately begins. After it will return result array of Observable.Need to use concatMap.
You'll need to use toArray() to collect all emissions from all source Observables.
source.pipe(
concatMap(observable => observable),
toArray(),
);

Have withLatestFrom wait until all sources have produced one value

I'm making use of the withLatestFrom operator in RxJS in the normal way:
var combined = source1.withLatestFrom(source2, source3);
...to actively collect the most recent emission from source2 and source3 and to emit all three value only when source1 emits.
But I cannot guarantee that source2 or source3 will have produced values before source1 produces a value. Instead I need to wait until all three sources produce at least one value each before letting withLatestFrom do its thing.
The contract needs to be: if source1 emits then combined will always eventually emit when the other sources finally produce. If source1 emits multiple times while waiting for the other sources we can use the latest value and discard the previous values. Edit: as a marble diagram:
--1------------2---- (source)
----a-----b--------- (other1)
------x-----y------- (other2)
------1ax------2by--
--1------------2---- (source)
------a---b--------- (other1)
--x---------y------- (other2)
------1ax------2by--
------1--------2---- (source)
----a-----b--------- (other1)
--x---------y------- (other2)
------1ax------2by--
I can make a custom operator for this, but I want to make sure I'm not missing an obvious way to do this using the vanilla operators. It feels almost like I want combineLatest for the initial emit and then to switch to withLatestFrom from then on but I haven't been able to figure out how to do that.
Edit: Full code example from final solution:
var Dispatcher = new Rx.Subject();
var source1 = Dispatcher.filter(x => x === 'foo');
var source2 = Dispatcher.filter(x => x === 'bar');
var source3 = Dispatcher.filter(x => x === 'baz');
var combined = source1.publish(function(s1) {
return source2.publish(function(s2) {
return source3.publish(function(s3) {
var cL = s1.combineLatest(s2, s3).take(1).do(() => console.log('cL'));
var wLF = s1.skip(1).withLatestFrom(s2, s3).do(() => console.log('wLF'));
return Rx.Observable.merge(cL, wLF);
});
});
});
var sub1 = combined.subscribe(x => console.log('x', x));
// These can arrive in any order
// and we can get multiple values from any one.
Dispatcher.onNext('foo');
Dispatcher.onNext('bar');
Dispatcher.onNext('foo');
Dispatcher.onNext('baz');
// combineLatest triggers once we have all values.
// cL
// x ["foo", "bar", "baz"]
// withLatestFrom takes over from there.
Dispatcher.onNext('foo');
Dispatcher.onNext('bar');
Dispatcher.onNext('foo');
// wLF
// x ["foo", "bar", "baz"]
// wLF
// x ["foo", "bar", "baz"]
I think the answer is more or less as you described, let the first value be a combineLatest, then switch to withLatestFrom. My JS is hazy, but I think it would look something like this:
var selector = function(x,y,z) {};
var combined = Rx.Observable.concat(
source1.combineLatest(source2, source3, selector).take(1),
source1.withLatestFrom(source2, source3, selector)
);
You should probably use publish to avoid multiple subscriptions, so that would look like this:
var combined = source1.publish(function(s1)
{
return source2.publish(function(s2)
{
return source3.publish(function(s3)
{
return Rx.Observable.concat(
s1.combineLatest(s2, s3, selector).take(1),
s1.withLatestFrom(s2, s3, selector)
);
});
});
});
or using arrow functions...
var combined = source1.publish(s1 => source2.publish(s2 => source3.publish(s3 =>
Rx.Observable.concat(
s1.combineLatest(s2, s3, selector).take(1),
s1.withLatestFrom(s2, s3, selector)
)
)));
EDIT:
I see the problem with concat, the withLatestFrom isn't getting the values. I think the following would work:
var combined = source1.publish(s1 => source2.publish(s2 => source3.publish(s3 =>
Rx.Observable.merge(
s1.combineLatest(s2, s3, selector).take(1),
s1.skip(1).withLatestFrom(s2, s3, selector)
)
)));
...so take one value using combineLatest, then get the rest using withLatestFrom.
I wasn't quite satisfied with the accepted answer, so I ended up finding another solution. Many ways to skin a cat!
My use-case involves just two streams - a "requests" stream and a "tokens" stream. I want requests to fire as soon as they are received, using the whatever the latest token is. If there is no token yet, then it should wait until the first token appears, and then fire off all the pending requests.
I wasn't quite satisfied with the accepted answer, so I ended up finding another solution. Essentially I split the request stream into two parts - before and after first token arrives. I buffer the first part, and then re-release everything in one go once I know that the token stream is non-empty.
const first = token$.first()
Rx.Observable.merge(
request$.buffer(first).mergeAll(),
request$.skipUntil(first)
)
.withLatestFrom(token$)
See it live here: https://rxviz.com/v/VOK2GEoX
For RxJs 7:
const first = token$.first()
merge(
request$.pipe(
buffer(first),
mergeAll()
),
request$.pipe(
skipUntil(first)
)
).pipe(
withLatestFrom(token$)
)
I had similar requirements but for just 2 observables.
I ended up using switchMap+first:
observable1
.switchMap(() => observable2.first(), (a, b) => [a, b])
.subscribe(([a, b]) => {...}));
So it:
waits until both observables emit some value
pulls the value from second observable only if the first one has changed (unlike combineLatest)
doesn't hang subscribed on second observable (because of .first())
In my case, second observable is a ReplaySubject. I'm not sure if it will work with other observable types.
I think that:
flatMap would probably work too
it might be possible to extend this approach to handle more than 2 observables
I was surprised that withLatestFrom will not wait on second observable.
In my mind, the most elegant way to achieve the different behavior of an existing RxJS operator is to wrap it into a custom operator. So that from the outside it looks just like any regular operator and doesn't require you to restructure your code each time you need this behavior.
Here is how you can create your own operator which behaves just like withLatestFrom, except that at the very beginning it will emit as soon as the first value of the target observable is emitted (unlike standard withLatestFrom, which will ignore the first emission of the source if the target hasn't yet emitted once). Let's call it delayedWithLatestFrom.
Note that it's written in TypeScript, but you can easily transform it to plain JS. Also, it's a simple version that supports only one target observable and no selector function - you can extend it as needed from here.
export function delayedWithLatestFrom<T, N>(
target$: Observable<N>
): OperatorFunction<T, [T, N]> {
// special value to avoid accidental match with values that could originate from target$
const uniqueSymbol = Symbol('withLatestFromIgnore');
return pipe(
// emit as soon target observable emits the first value
combineLatestWith<T, [N]>(target$.pipe(first())),
// skip the first emission because it's handled above, and then continue like a normal `withLatestFrom` operator
withLatestFrom(target$.pipe(skip(1), startWith(uniqueSymbol))),
map(([[rest, combineLatestValue], withLatestValue]) => {
// take combineLatestValue for the first time, and then always take withLatestValue
const appendedValue =
withLatestValue === uniqueSymbol ? combineLatestValue : withLatestValue;
return [rest, appendedValue];
})
);
}
// SAMPLE USAGE
source$.pipe(
delayedWithLatestFrom(target$)
).subscribe(console.log);
So if you compare it with the original marble diagram for withLatestFrom, it will differ only in one fact: while withLatestFrom ignores the first emissions and produces b1 as the first value, the delayedWithlatestFrom operator will emit one more value a1 at the beginning, as soon as the second observable emits 1.
a) Standard withLatestFrom:
b) Custom delayedWithLatestFrom:
Use combineLatest and filter to remove tuples before first full set is found then set a variable to stop filtering. The variable can be within the scope of a wrapping defer to do things properly (support resubscription). Here it is in java (but the same operators exist in RxJs):
Observable.defer(
boolean emittedOne = false;
return Observable.combineLatest(s1, s2, s3, selector)
.filter(x -> {
if (emittedOne)
return true;
else {
if (hasAll(x)) {
emittedOne = true;
return true;
} else
return false;
}
});
)
I wanted a version where tokens are fetched regularly - and where I want to retry the main data post on (network) failure. I found shareReplay to be the key. The first mergeWith creates a "muted" stream, which causes the first token to be fetched immediately, not when the first action arrives. In the unlikely event that the first token will still not be available in time, the logic also has a startWith with an invalid value. This causes the retry logic to pause and try again. (Some/map is just a Maybe-monad):
Some(fetchToken$.pipe(shareReplay({refCount: false, bufferSize: 1})))
.map(fetchToken$ =>
actions$.pipe(
// This line is just for starting the loadToken loop immediately, not waiting until first write arrives.
mergeWith(fetchToken$.pipe(map(() => true), catchError(() => of(false)), tap(x => loggers.info(`New token received, success: ${x}`)), mergeMap(() => of()))),
concatMap(action =>
of(action).pipe(
withLatestFrom(fetchToken$.pipe(startWith(""))),
mergeMap(([x, token]) => (!token ? throwError(() => "Token not ready") : of([x, token] as const))),
mergeMap(([{sessionId, visitId, events, eventIds}, token]) => writer(sessionId, visitId, events, token).pipe(map(() => <ISessionEventIdPair>{sessionId, eventIds}))),
retryWhen(errors =>
errors.pipe(
tap(err => loggers.warn(`Error writing data to WG; ${err?.message || err}`)),
mergeMap((_error: any, attemptIdx) => (attemptIdx >= retryPolicy.retryCount ? throwError(() => Error("It's enough now, already")) : of(attemptIdx))), // error?.response?.status (int, response code) error.code === "ENOTFOUND" / isAxiosError: true / response === undefined
delayWhen(attempt => timer(attempt < 2 ? retryPolicy.shortRetry : retryPolicy.longRetry, scheduler))
)
)
)
),
)
)
Thanks to everyone on this question-page for good inputs.
Based on the answer from #cjol
Here's a RxJs 7 implementation of a waitFor operator that will buffer the source stream until all input observables have emitted values, then emit all buffered events on the source stream. Any subsequent events on the source stream are emitted immediately.
// Copied from the definition of withLatestFrom() operator.
export function waitFor<T, O extends unknown[]>(
inputs: [...ObservableInputTuple<O>]
): OperatorFunction<T, [T, ...O]>;
/**
* Buffers the source until every observable in "from" have emitted a value. Then
* emit all buffered source values with the latest values of the "from" array.
* Any source events are emitted immediately after that.
* #param from Array of observables to wait for.
* #returns Observable that emits an array that concatenates the source and the observables to wait.
*/
export function waitFor(
from: Observable<unknown>[]
): (source$: Observable<unknown>) => Observable<unknown> {
const combined$ = combineLatest(from);
// This served as a conditional that switched on and off the streams that
// wait for the the other observables, or emits the source right away because
// the other observables have emitted.
const firstCombined$ = combined$.pipe(first());
return function (source$: Observable<unknown>): Observable<unknown> {
return merge(
// This stream will buffer the source until the other observables have all emitted.
source$.pipe(
takeUntil(firstCombined$), // without this it continues to buffer new values forever
buffer(firstCombined$),
mergeAll()
),
// This stream emits the source straight away and will take over when the other
// observables have emitted.
source$.pipe(skipUntil(firstCombined$))
).pipe(
withLatestFrom(combined$),
// Flatten it to behave like withLatestFrom() operator.
map(([source, combined]) => [source, ...combined])
);
};
}
All of the above solutions are not really on the point, therefore I made my own. Hope it helps someone out.
import {
combineLatest,
take,
map,
ObservableInputTuple,
OperatorFunction,
pipe,
switchMap
} from 'rxjs';
/**
* ### Description
* Works similar to {#link withLatestFrom} with the main difference that it awaits the observables.
* When all observables can emit at least one value, then takes the latest state of all observables and proceeds execution of the pipe.
* Will execute this pipe only once and will only retrigger pipe execution if source observable emits a new value.
*
* ### Example
* ```ts
* import { BehaviorSubject } from 'rxjs';
* import { awaitLatestFrom } from './await-latest-from.ts';
*
* const myNumber$ = new BehaviorSubject<number>(1);
* const myString$ = new BehaviorSubject<string>("Some text.");
* const myBoolean$ = new BehaviorSubject<boolean>(true);
*
* myNumber$.pipe(
* awaitLatestFrom([myString$, myBoolean$])
* ).subscribe(([myNumber, myString, myBoolean]) => {});
* ```
* ### Additional
* #param observables - the observables of which the latest value will be taken when all of them have a value.
* #returns a tuple which contains the source value as well as the values of the observables which are passed as input.
*/
export function awaitLatestFrom<T, O extends unknown[]>(
observables: [...ObservableInputTuple<O>]
): OperatorFunction<T, [T, ...O]> {
return pipe(
switchMap((sourceValue) =>
combineLatest(observables).pipe(
take(1),
map((values) => [sourceValue, ...values] as unknown as [T, ...O])
)
)
);
}
Actually withLatestFrom already
waits for every source
emits only when source1 emits
remembers only the last source1-message while the other sources are yet to start
// when source 1 emits the others have emitted already
var source1 = Rx.Observable.interval(500).take(7)
var source2 = Rx.Observable.interval(100, 300).take(10)
var source3 = Rx.Observable.interval(200).take(10)
var selector = (a,b,c) => [a,b,c]
source1
.withLatestFrom(source2, source3, selector)
.subscribe()
vs
// source1 emits first, withLatestFrom discards 1 value from source1
var source1 = Rx.Observable.interval(500).take(7)
var source2 = Rx.Observable.interval(1000, 300).take(10)
var source3 = Rx.Observable.interval(2000).take(10)
var selector = (a,b,c) => [a,b,c]
source1
.withLatestFrom(source2, source3, selector)
.subscribe()

Resources