RxJs race function - make sure that the first to occur wins - rxjs

I have 2 different Observables, let's call them racer_1 and racer2, which emit when 2 different events occur.
If racer_1 occurs I have to perform the logic of function doStuff_1(), if racer_2 occurs I have to perform doStuff_2().
racer_1 and racer_2 are not mutually exclusive and they can occur one after the other, but what I want to achieve is that if any of them occur, I process just that one with its associated function.
It seems clearly the case of using the race function of RxJs.
race(
racer_1.pipe(
tap(() => doStuff_1()),
finalize(() => console.log('racer 1 completed'))
),
racer_2.pipe(
tap(() => doStuff_2()),
finalize(() => console.log('racer 2 completed'))
)
)
Unfortunately though it may happen that racer_1 emits first at t0 and doStuff_1() starts a synchronous execution which ends at t0+1000.
In the meantime, at t0+200 racer_2 emits and the execution of doStuff_2() is much shorter so that it ends at t0+300.
In this case what I see is that racer_2 "wins the race" and its emissions continue to get processed, while racer_1 gets completed.
On the contrary what I would like to see is that racer_1 wins since it occurred first, even if its processing can get long to be completed.
Is there a way to obtain such behavior?

You can just reorder operators and wrap the initial emissions from both source Observables and perform doStuff_1 or doStuff_2 later:
race( // wrap emissions with tmp objects
racer_1.pipe(map(result => { type: 'racer1', result })),
racer_2.pipe(map(result => { type: 'racer2', result })),
)
.tap(({ type, result }) => type === 'racer1'
? doStuff_1()
: doStuff_2()
)
map(({ type, result }) => result) // unwrap the object

Related

RxJS: conditional throttling

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

How to combine a parent and a dependent child observable

There is a continuous stream of event objects which doesn't complete. Each event has bands. By subscribing to events you get an event with several properties, among these a property "bands" which stores an array of bandIds. With these ids you can get each band. (The stream of bands is continuous as well.)
Problem: In the end you'd not only like to have bands, but a complete event object with bandIds and the complete band objects.
// This is what I could come up with myself, but it seems pretty ugly.
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(of(event), ...band$Array);
})),
map(combined => {
const newEvent = combined[0];
combined.forEach((x, i) => {
if (i === 0) return;
newEvent.bands = {...newEvent.bands, ...x};
})
})
)
Question: Please help me find a cleaner way to do this (and I'm not even sure if my attempt produces the intended result).
ACCEPTED ANSWER
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
ORIGINAL ANSWER
You may want to try something along these lines
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return forkJoin(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
The Observable returned by this transformation emits an object with 2 properties: bandArray holding the array of bands retrieved with the getBand service and event which is the object emitted by the Observable returned by getEvents.
Consider also that you are using switchMap, which means that as soon as the Observable returned by getEvents emits you are going to switch to the last emission and complete anything which may be on fly at the moment. In other words you can loose some events if the time required to exectue the forkJoin is longer than the time from one emission and the other of getEvents.
If you do not want to loose anything, than you better use mergeMap rather than switchMap.
UPDATED ANSWER - The Band Observable does not complete
In this case I understand that getBand(bandId) returns an Observable which emits first when the back end is queried the first time and then when the band data in the back end changes.
If this is true, then you can consider something like this
getEvents().pipe(
switchMap(event => {
return from(event.bands).pipe(
switchMap(bandId => getBand(bandId)).pipe(
map(bandData => ({event, bandData}))
)
);
})
)
This transformation produces an Observable which emits either any time a new event occurs or any time the data of a band changes.

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)

RxJS Observables and a wheel event

I was wondering if anyone could help me theorize a solution. I create an Observable from a wheel event, prevent the default action, throttle it by 200ms, map deltaY (which I can use to determine direction), and then I share it.
My problem is that it emits more values than I need creating a situation where my subscribers continue to fire even after the desired action has occurred. I'm new to RxJS so bear with me but... Is there a way for me to take the "first" value emitted in a series of values within say X amount of time passed and not have the observable complete?
Below is the code.
import { fromEvent } from 'rxjs';
const wheel$ = fromEvent(document, 'wheel')
.pipe(
tap((event) => event.preventDefault()),
// throttleTime(200), /* I have tried throttling and debouncing but that doesn't work - values will continue to be emitted */
map((event) => event.deltaY),
share()
)
// handles scrolling down //
wheel$.pipe(filter((val) => val > 0))
.subscribe((event) => {
if (this.props.isScrolling) return
this.scrollDown();
})
One solution would be "bufferCount()"
of(1,2,3,4,5,6,7,8,9).pipe(
bufferCount(3)
).subscribe(data => console.log(data) )
would create packages of 3 signals. So the Events would be
[1,2,3]
[4,5,6]
[7,8,9]
Or "throttleTime(xy)", than it will put the first signal through, will ignore for "xy" milliseconds every other signal, and then give the next signal a chance.
interval(500).pipe(
throttleTime(2000)
).subscribe(data => console.log(data) )
will result in something like
1 // ignore everything the next 2 seconds
5 // ignore everything the next 2 seconds
9 // ignore everything the next 2 seconds
...
warm regards
Found this question while looking for a solution.
I went with a switchMap
fromEvent(el, 'wheel').pipe(
switchMap(e =>
concat(
//wrap the original event, which fires immediately
of({kind: 'ON_WHEEL', e}),
// fire a onWheelEnd after a 200ms delay
// this event is unsubscribed by the switchMap if a new event comes in
// before the delay
of({kind: 'ON_WHEEL_END}).pipe(delay(200))
)
)
)

why concatMap is not subscribing to all groupBy Observables rxjs?

I have a question why is this not writing to console the numbers 2,4,6? what is the explanation?
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
// this is the output
1 -
3 -
5 -
complete concatMap
The basic problem is that you're using concatMap that subscribes to the next Observable only when the previous one completed. groupBy emits two GroupedObservables so it subscribes to the first one and I think before it can subscribe to the second one the chain completes. This means the observer receives the complete notification from the first GroupedObservable and therefore you never see values from the second GroupedObservable (to be honest I'm not 100% sure it really happens like this but that makes sense to be without further investigating you example).
So if you want only the second group you could do:
import { Observable } from 'rxjs';
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.filter(o => o.key === true)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
See live demo (open console): https://stackblitz.com/edit/rxjs5-sfused
I checked the source code and groupBy completes all groups after receiving the complete notification (which it does after receiving all values from range) and therefore there's never space for concatMap to subscribe to the second Observable.
See this: https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/groupBy.ts#L200-L210
Problem is that groupBy operator emits Subjects for each key.
Concat map subscribes to subsequent Subject only after the first one is completed, i.e. it misses a chance to catch items from subsequent Subject because all of the sub-streams emit values in the same time.
Kudos to: https://blog.angularindepth.com/those-hidden-gotchas-within-rxjs-7d5c57406041
TL;DR:
GroupBy receives subjectSelector as a 4th argument. You can use it to force using ReplaySubject instead of Subject (default).
Observable.range(1, 6)
.groupBy(
n => n % 2 === 0,
null,
null,
() => new ReplaySubject() // <-- Here we go
)
.concatMap(obs => obs)
.subscribe((n) => console.log(n))
Demo on RxViz

Resources