RxJS: conditional throttling - rxjs

i want to have a stream that emits while the user is typing. It should emit at most once per second, unless the either the before or the after is an empty input box. In other words, it must always emit immediately if the user types (or pastes) the first character, or if he empties his input. I now have this, but it isn't exactly what I'm looking for:
this.chatForm.get('message')
.valueChanges
.pipe(
startWith(this.chatForm.get('message').value),
pairwise(),
throttle(([a, b]) => {
if (!a || !b) {
return timer(0);
}
return timer(1000);
}),
map(([, b]) => b),
withLatestFrom(this.user$)
)
.subscribe(([ message, user ]) => this.updateUserIsTyping(user, !!message));
with this code it will emit immediately for the first two times that the user starts typing and starts throttling it then, and if you time it right, you will not get an emission when the user clears the input.
What do I need to change to make this work?

Here's my approach:
const input$ = this.chatForm.get('message').valuesChanges;
const emptyInput$ = input$.pipe(filter(v => !v));
const src$ = input$.pipe(
observeOn(asyncScheduler),
throttle(() => timer(1000).pipe(takeUntil(emptyInput$))),
/* ... */
).subscribe(/* ... */)
if the user is continuously typing, a single value per 1000ms will be emitted
if the user empties the input, emptyInput$ will emit, meaning that the throttle's inner observable will complete.
observeOn(asyncScheduler) is used because the valuesChanges Subject has 2 subscribers(input$.pipe(...) and the one from takeUntil) and we want to make sure that the throttle's inner obs. is completed before the empty value is passed along.
Without doing this, the empty value will arrive while the throttling is still happening(the observable is still active) and it wouldn't be emitted, since throttle has leading: true and trailing: false by default.

I actually managed to do it by creating two streams and merging them like this:
const stream1 = this.chatForm.get('message')
.valueChanges
.pipe(
throttleTime(1000)
);
const stream2 = this.chatForm.get('message')
.valueChanges
.pipe(
startWith(this.chatForm.get('message').value),
pairwise(),
flatMap(pair => {
return pair.every(x => x) ? EMPTY : of(pair[1]);
})
);
merge(stream1, stream2)
.pipe(
// when both streams emit at the same time we only want one result
debounceTime(0),
withLatestFrom(this.user$)
)
.subscribe(([ message, user ]) => this.updateUserIsTyping(user, !!message))
I'm still curious to learn if there's a single stream solution though

Related

RxJS: execute concatMap i parallel

Is it possible to execute a high-order observable in parallel, but still preserve the order when merging the results?
I have something looking like this:
invoker$: Observable<void>;
fetch: (index: number) => Observable<T[]>;
invoker$
.pipe(
concatMap((_, index) => fetch(index)),
scan((acc: T[], values) => [...acc, ...values], [])
)
.subscribe(/* Do something with the array */);
The idea is having an observable that invokes a callback (e.g. backend call that takes a considerable amount of time) generating a new observable that emits a single value (array of some generic type). The returned values should be concatenated in another array while preserve their original fetch order.
I would, however, like the requests to be fired in parallel. So if the invoker$ is called rapidly, the requests are made in parallel and the results are merged as they complete.
My understanding is that the concatMap will wait for one observable to complete, before starting the next one. mergeMap will do it parallel, but won't do anything to preserve the order.
You can do it using mergeMap.
First, you need to pass the index together with the async response down the stream.
Then you can sort based on the index from the previous step.
Then you have two choices:
if the stream needs to end once all the requests are made and handle only once all the responses you can use reduce https://rxmarbles.com/#reduce
if the stream needs to continue for another batch of requests you need to use scan and later filter until you reach the needed event count. https://rxmarbles.com/#scan and https://rxmarbles.com/#filter
I am going to give you some pseudo-code for both examples:
In the reduce case, the stream ends once all requests are sent:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
reduce((acc: T[], singleValue) => [...acc, ...singleValue], []),
map(array => array.sort(/*Sort on index here*/).map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
In the multiple-use case, I am assuming the size of the batch to be constant:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
scan((acc: T[], singleValue) => {
let resp = [...acc, ...singleValue];
// The scan can accumulate more than the batch size,
// so we need to limit it and restart for the new batch
if(resp.length > BATCH_SIZE) {
resp = [singleValue];
}
return resp;
}, []),
filter(array => array.length == BATCH_SIZE),
map(array =>
array
.sort(/*Sort on index here*/)
.map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
2.1. In case the batch size is dynamic:
invoker$
.pipe(
mergeMap((_, index) => fetch(index).then(value => {value, index})),
withLatestFrom(batchSizeStream),
scan((acc: [T[], number], [singleValue, batchSize]) => {
let resp = [[...acc[0], ...singleValue], batchSize];
// The scan can accumulate more than the batch size,
// so we need to limit it and restart for the new batch
// NOTE: the batch size is dynamic and we do not want to drop data
// once the buffer size changes, so we need to drop the buffer
// only if the batch size did not change
if(resp[0].length > batchSize && acc[1] == batchSize) {
resp = [[singleValue], batchSize];
}
return resp;
}, [[],0]),
filter(arrayWithBatchSize =>
arrayWithBatchSize[0].length >= arrayWithBatchSize[1]),
map(arrayWithBatchSize =>
arrayWithBatchSize[0]
.sort(/*Sort on index here*/)
.map(valueWithIndex => valueWithIndex.value))
)
.subscribe(/* Do something with the array */);
EDIT: optimized sorting, added dynamic batch size case
I believe that the operator you are looking for is forkJoin.
This operator will take as input a list of observables, fire them in parallel and will return a list of the last emitted value of each observable once they all complete.
forkJoin({
invoker: invoker$,
fetch: fetch$,
})
.subscribe(({invoker, fetch}) => {
console.log(invoker, fetch);
});
Seems like this behavior is provided by the concatMapEager operator from the cartant/rxjs-etc library - written by Nicholas Jamieson
(cartant) who's a developer on the core RxJS team.

How do I create an observable which emits a value from an ngrx selector, then another value after a delay?

Given an ngrx selector:
store.select('count')
I want to create an observable that will emit values emitted by the selector, then emit another specific value after a delay.
Using concat doesn't work as (I assume) the selector doesn't complete, so the 0 is never emitted:
this.count$ = concat(store.select('count'), of(0).pipe(delay(2000)));
Stackblitz: https://stackblitz.com/edit/so-selector-delay?file=src/app/my-counter/my-counter.component.ts
- click 'Increment' button - Current Count should change to 1 then back to 0 after 2 seconds.
If you want to emit the store.select('count') value, then essentially reset it to 0 after not receiving an emission for 2 seconds, you can use a switchMap to create a source that emits two values:
The emitted count
The "default" value of 0 after 2000ms
The trick here is that the second value (0) will NOT be emitted if another store.select('count') value is received, because switchMap will create a new source and "switch" to that:
this.count$ = store.select('count').pipe(
switchMap(count => concat(
of(count),
of(0).pipe(delay(2000))
))
);
Here's a working StackBlitz demo.
It might even be worth creating a custom operator:
this.count$ = this.store.select('count').pipe(
resetAfterDelay(0, 2000)
);
export function resetAfterDelay<T>(defaultValue: T, delayMs: number) {
return switchMap((value: T) => concat(
of(value),
of(defaultValue).pipe(delay(delayMs))
));
}
StackBlitz
Below is an approach using combineLatest and BehaviorSubject
We are hold a value in a subject and create a timer that emits value 0 after 2s. So we have two Observables, one emits immediately and the other after 2s. We combine this two and the effect is a single observable as desired
valueHolderSubject$ = new BehaviorSubject(0);
...
this.count$ = combineLatest([
store.select("count").pipe(
tap(x => this.valueHolderSubject$.next(x)),
tap(() =>
timer(2000).subscribe({
next: () => {
this.valueHolderSubject$.next(0);
}
})
),
map(() => this.valueHolderSubject$.value),
distinctUntilChanged()
),
this.valueHolderSubject$
]).pipe(map(([, x]) => x));
Demo Here
A per my comments on the answer from BizzyBob, I was getting unreliable results. But I refined it to work using:
this.count$ = merge(
store.select('count'),
store.select('count').pipe(delay(2000), map(_ => 0))
);
See stackblitz: https://stackblitz.com/edit/so-selector-delay-merge-working?file=src/app/my-counter/my-counter.component.ts

Cancel repeated subscription in mergeMap

How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).

RxJS design pattern for multiple async calls

I am new to RxJS and haven't been able to find clear answers on the following use case:
In a mobile app (Angular/Ionic), I need to (1) make simultaneous HTTP calls and only return data when all have completed (like $q.all). I want to (2) throw an error if the calls work correctly but there is a nested value in one of the responses that meets a certain criteria (ie user is not authenticated properly). Because it's a mobile app, I want to (3) build in a few retry attempts if the calls don't work correctly (for any reason). If after a certain number of retry attempts the calls still fail, I want to (4) throw an error.
Based on my research seems like forkJoin works the same as q.all. I have a provider that returns the following (observableArray holds the http calls).
return Observable.forkJoin(observableArray)
And then I can pipe in some operators, which is where I'm starting to struggle. For checking the nested value (2), I am using an underscore method to iterate over each response in my response array. This doesn't seem clean at all.
For retrying the calls (3), I am using retryWhen and delayWhen. But I am unsure how to limit this to 3 or 4 attempts.
And if the limit is hit, how would I throw an error back to the subscribers (4)?
.pipe(
map(
res => {
_.each(res, (obs) => {
if (!obs['success']) throw new Error('success was false')
})
}
),
retryWhen(attempts =>
attempts.pipe(
tap(err => console.log('error:', err),
delayWhen(() => timer(1000))
)
)
))
There are couple of tricks here to make your code clean.
1. Use Observable.iif():
iif accepts a condition function and two Observables. When an Observable returned by the operator is subscribed, condition function will be called. Based on what boolean it returns at that moment, consumer will subscribe either to the first Observable (if condition was true) or to the second (if condition was false).
2. Use JavaScript array native's every():
The every() method tests whether all elements in the array pass the test implemented by the provided function.
3. Use take() to terminate your retryWhen
Emits only the first count values emitted by the source Observable.
So your code boils down to:
.pipe(
switchMap((res) => iif(
() => res.every(({success}) => success),
res,//if every element in res is successful, return it
throwError('success was false') //else if any of it is false, throw error.
)
),
retryWhen(err => err.delay(1000).take(4))
)
Edit:
If you want to catch the error at your subscribe, you will need to rethrow the error. .take() will actually just terminate the sequence, aka completing it:
.pipe(
switchMap((res) => iif(
() => res.every(({success}) => success),
res,//if every element in res is successful, return it
throwError('success was false') //else if any of it is false, throw error.
)
),
retryWhen(err => {
return errors.scan((errorCount, err) =>
(errorCount >= 4 ? //is retried more than 4 times?
throwError(err) : //if yes, throw the error
errorCount++ //else just increment the count
), 0);
})
)
A possibility to consider is to pipe the various operators you need, e.g. retry and map, into each Observable contained in the observableArray you pass to forkJoin.
The code could look like something similar to this
const observableArray = new Array<Observable<any>>();
const maxRetries = 4;
function pipeHttpObservable(httpObs: Observable<any>): Observable<any> {
return httpObs
.pipe(
map(data => data.success ? data : throwError('success was false')),
retryWhen(err => err.delay(1000).take(maxRetries))
)
}
observableArray.push(pipeHttpObservable(httpObs1));
observableArray.push(pipeHttpObservable(httpObs2));
.....
observableArray.push(pipeHttpObservable(httpObsN));
forkJoin(observableArray).subscribe(result => do stuff with the results)

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