Rxjs, Combine last query result for multiple group (combine switchMap with groupBy) - rxjs

I have an issue using the operator. Basically I have multiple payslips and I want to keep a debounce for each payslip and trigger a query. I want to subscribe to only the last query that succeed for a specific payslip and if a new request is triggered for this payslip before the previous one finished, I want to cancel the previous one.
Here's a sample marble diagram for what i'm looking for:
-----1----1-----1----3----3----3----3-----1---3---1---3---1------>
(magic operators for which I'm unclear)
-------------------1-------------------3-----1---3---1---3---1--->
I have debounce and query, which I like, but it does this:
-----1----1-----1----3----3----3----3-----1---3---1---3---1------>
debounce
-------------------1-------------------3--------------------1---->
.pipe(
groupBy(payslip => payslip._id),
map(group =>
group.pipe(
debounceTime(200),
switchMap(payslip => httpQuery)
)
),
mergeAll()
)
With the current solution, the merge all is grouping the switch map thus canceling even for other group. Is there a way to do what I want ?
Thanks !

I am struggling to find anything wrong with your code or it's outcomes.
I have put the following test code in the ThinkRx playground
const { rxObserver } = require('api/v0.3');
const { zip, timer, from, of } = require('rxjs');
const { take, map, groupBy, mergeAll, debounceTime, delay, switchMap } =
require('rxjs/operators');
zip(
timer(0, 50),
from([1,1,1,3,3,3,3,1,3,1,3,1]),
).pipe(
map(([_,i])=>i),
groupBy(i=>i),
map(group => group.pipe(
debounceTime(0),
switchMap(i=>of(i).pipe(delay(0))), // simulates http request with know duration
)),
mergeAll(),
).subscribe(rxObserver());
With debounce and time-for-http-request both 0 the result is:
With time-for-http-request = 60 (note the time scale is longer):
And then with debounce = 10:
Is it possible in your code that the http request simply takes longer than the time between the requests, so the switchMap is correctly cancelling the earlier one?

Related

Is it possible to write an rxjs operator that controls subscription to it's source?

Let's say I have an observable source that has the following properties:
It makes a network request the first time it's subscribed to
It's idempotent, so it will always emit the same value after it's first subscribed to
It's low priority, so we don6t6 want to be too eager in subscribing to it
What would be ideal is if there were some sort of operator delaySubscriptionUntil that would delay subscription until some other observable s emits a value.
So for example:
const s = new Subject<void>();
const l = source
.pipe(
delaySubscriptionUntil(s));
l.subscribe(console.log);
// The above won't print anything until this line executes
s.next();
I looked through the documentation to see if there's an existing operator like this, but haven't found one.
You just put the subject first and switchMap
const l = s.pipe(
switchMap(() => source)
);
Once the subject emits then the source will be subscribed to.
Any thing that is after wont work as it relies on the previous observable emitting a value. You can have a filter in the chain that stops the previous observable's emission being emitted but there is nothing you can pass back up the chain to control outer subscriptions.
You could use a takeWhile
let allow = false;
const l = source.pipe(
takeWhile(allow)
);
but here the subscription to source is active, it is emitting values, they are just stopped from being passed through.
So you could make a similar operator that keeps an internal flag and is flipped by a subject but source is still going to be emitting, you are just filtering values. You could buffer up the values if you don't want to lose them.
You could use share() which will share the result of anything that happened before it until you call sub.next() with a new url then the request will happen again.
const sub = new BehaviorSubject<string>('http://example.com/api');
const result$ = sub.pipe(
exhaustMap(url => this.http.get(url)),
share()
)
// Each one of these subscriptions will share the result.
// The http request will be called only once
// until you send a new url to the BehaviorSubject.
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));

What is the difference between merge and mergeAll?

What is the difference between merge and mergeAll? They both seem identical to me:
http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeAll
http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-merge
merge is a static creation method that flattens group of observable.
according to the docs
Flattens multiple Observables together by blending their values into one Observable.
simply it will take a group of observables, and flattens them within one, so whenever any observable emits a value, the output will emit a value.
mergeAll However is different, it is an instance method that works with higher order observables (an observable that emits observables), according to docs
Converts a higher-order Observable into a first-order Observable which concurrently delivers all values that are emitted on the inner Observables.
I think that sums it up, but mergeAll can be confusing, so let's look at this example provided by rxjs docs
import { fromEvent, interval } from 'rxjs';
import { take, map, mergeAll } from 'rxjs/operators';
const higherOrder = fromEvent(document, 'click').pipe(
map((ev) => interval(1000).pipe(take(10))),
);
const firstOrder = higherOrder.pipe(mergeAll(2));
firstOrder.subscribe(x => console.log(x));
you have a document click observable (higher order) which return an interval observable (inner observable) that emits a value every second, it will complete after 10 intervals emits, which means every time you click on the document, a new interval will be returned, here where merge all comes in, it will subscribe to these intervals returned by the higher order observable, and flattens them into one observable, the first order observable, the argument 2, is to limit to 2 concurrent intervals at a time, so if you clicked 3 times, only 2 will run, but since these 2 intervals will complete after 10 seconds, then you can click again and mergeAll will subscribe to the new intervals.
Both merge and mergeAll inherit from mergeMap !
mergeAll
mergeAll is the same as calling mergeMap with an identity function(const identity = x => x)
mergeAll() === mergeMap(obs$ => obs$)
Example:
of(a$, b$, c$)
.pipe(
mergeAll(),
)
.subscribe()
// Same as
of(a$, b$, c$)
.pipe(
mergeMap(obs$ => obs$)
)
.subscribe()
Both will subscribe to the incoming observables(a$, b$ and c$) and will pass along to their values to the data consumer. Thus, a$, b$ and c$ are considered inner observables.
merge
Armed with the knowledge from the previous section, understanding merge should not be difficult.
merge(a$, b$, c$).subscribe() is essentially the same as
const observables = [a$, b$, c$];
new Observable(subscriber => {
for (let i = 0; i < observables.length; i++) {
subscriber.next(observables[i]);
}
subscriber.complete();
}).pipe(
mergeAll()
).subscribe();

Initialize observable with the result of other observable

I have 2 requests.
getCurrentBook(): Observable<Book>
getDetailedInfo(bookId): Observable <BookDetailed>
They both return observables with information, however to use second request I have to make sure that I received the information from the first one since bookId is in the response.
I understand that I could subscribe inside other subscribe, however this solution doesn't seem appealing to me. There must be a much more elegant way.
The existing solution
getCurrentBook().subscribe(res => {
getDetailedInfo(res.id).subscribe(...);
})
I get that it should look something like:
booksSubs = getCurrentBook().pipe(
map(res =>
{this.currentBook = res}
)
)
detailedSubs = getDetailedInfo(this.currentBook.id).pipe(
map(res =>
{this.detailed = res}
)
)
this.subscriptions.push(SOME OPERATOR(booksSubs, detailedSubs).subscribe();
But the option higher won't work since I need result of first observable to initialize second.
You can achieve it using some of "flattening" operators, for example mergeMap:
const currentBookDetails$ = getCurrentBook().pipe(
mergeMap(book => getDetailedInfo(book.id))
);

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 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))
)
)
)

Resources