switchMap distincted by property in RxJs - javascript

Let's say, I have a stream of actions. Each action is assigned some id. Like this:
const actions$ = of({ id: 1 }, { id: 2 }, { id: 1 });
Now, for each action, I want to perform some logic in switchMap:
actions$.pipe(switchMap(a => /* some cancellable logic */)).subscribe(...);
The problem is that each emitted action cancels previous 'some cancellable logic'.
Is it possible to cancel 'some cancellable logic' based on action id, preferably an operator? Something like:
actions$.pipe(switchMapBy('id', a => /*some cancellable logic */)).subscribe(...)
Essentially, current behaviour with switchMap:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap unsubscribes from previous nested observable. Subscribes to new one.
3. actions$ emits id #1. switchMap again unsubscribes from previous nested observable. Subscribes to new one.
Expected behaviour:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap again subscribes to nested observable (this time with #2). And here's the difference, it doesn't cancel the one from #1.
3. actions$ emits id #1. switchMap unsubscribes from nested observable for #1. Subscribes again, for #1.

this seems to be a use case for the mergeMap operator. The use case of switchMap is to only maintain one inner subscription and cancel previous ones, which is not what you're after here. You want multiple inner subscriptions and you want them to cancel when a new value of the same id comes through, so implement some custom logic to do that.
something along the lines of:
action$.pipe(
mergeMap(val => {
return (/* your transform logic here */)
.pipe(takeUntil(action$.pipe(filter(a => a.id === val.id)))); // cancel it when the same id comes back through, put this operator at the correct point in the chain
})
)
you can turn this into something resuable by writing a custom operator:
import { OperatorFunction, Observable, from } from 'rxjs';
import { takeUntil, filter, mergeMap } from 'rxjs/operators';
export function switchMapBy<T, R>(
key: keyof T,
mapFn: (val: T) => Observable<R> | Promise<R>
): OperatorFunction<T, R> {
return input$ => input$.pipe(
mergeMap(val =>
from(mapFn(val)).pipe(
takeUntil(input$.pipe(filter(i => i[key] === val[key])))
)
)
);
}
and use it like:
action$.pipe(
switchMapBy('id', (val) => /* your transform logic here */)
);
here's a blitz of it in action: https://stackblitz.com/edit/rxjs-x1g4vc?file=index.ts

use filter operation before switchMap to exclude canceled ids, like this
of({ id: 1 }, { id: 2 }, { id: 1 }).pipe(
filter(id => ![1,2].includes(id)), // to exclude ids (1,2)
switchMap(id => /*some cancellable logic */ )
).subscribe(...)

Related

How to map over an observed array to combine multiple obervables latest values

I am piping into an observable, that streams an array of let's say food. I need to get the latest cook, the latest kitchen, and the latest knife used to prepare each of these food items. These three values are all values got by an observable like getCookById(food.cook.id).
How could i map it so:
I'm waiting for the array of food
When i receive it, I map on every food item to:
get latest cook, kitchen and knife that are all from Observables (combineLatest?)
combine all of that on an object like
{
foodItem,
cook,
kitchen,
knife
}
return the array of all these transformed items
Currently, I get an array of empty items doing it like so :
const foodObservable = new Subject();
foodObservable.pipe(
map(foodItems => {
return foodItems.map((foodItem: any)=>{
const cookObservable = new Subject();
const kitchenObservable = new Subject();
const knifeObservable = new Subject();
combineLatest(cookObservable, kitchenObservable, knifeObservable).subscribe(([cook, kitchen, knife]) => {
return {
foodItem,
cook,
kitchen,
knife
};
});
})
})
).subscribe(foodPreparationArray=>{
console.log(foodPreparationArray) // array of empty values :(
})
Based on your condition and your comment, I'd suggest the following changes
You'd need to use a higher order mapping operator like switchMap to map from one observable to another. map operator is used to transform the emitted data.
Using Firebase get() instead of valueChanges() would be a better fit. I assume you need the value at the moment and do not need to observe any changes to the property. See here for more info.
Using get() instead of valueChanges() provides us with us observables that completes instead of streaming. So you could replace the combineLatest with forkJoin.
You'd need two forkJoins. First forkJoin is to trigger requests for each element of the foodItems array and second forkJoin is to trigger requests for cook, knife, and kitchen simultaneously.
Finally you could use the map operator to combining everything.
Try the following
foodObservable.pipe(
switchMap((foodItems: any) => // <-- RxJS `switchMap` operator
forkJoin(
foodItems.map((foodItem: any) => // <-- JS `Array#map` function
forkJoin({
cook: db.collection('cook').doc(foodItem.cookId).get(),
kitchen: db.collection('kitchen').doc(foodItem.kitchenId).get(),
knife: db.collection('knife').doc(foodItem.knifeId).get()
}).pipe(
map(({cook, kitchen, knife}) => ({ // <-- RxJS `map` operator
...foodItem, // <-- spread operator
cook: cook,
kitchen: kitchen,
knife: knife
)})
)
)
)
)
).subscribe({
next: (foodPreparationArray: any) => {
console.log(foodPreparationArray);
},
error: (error: any) => {
// handle error
}
});

Refactoring chained RxJs subscriptions

I have a piece of code that I need to refactor because it's a hell of chained subscriptions.
ngOnInit(): void {
this.dossierService.getIdTree()
.subscribe(idTree => {
this.bootstrappingService.refreshObligations(idTree)
.subscribe(() => {
this.dossierPersonsService.retrieveDossierPersons(idTree)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
});
});
}
The first call dossierService.getIdTree() retrieves idTree which is used by other services except obligationsService.retrieveObligations().
All service methods should be executed in the order they executed now. But retrieveDossierPersons and retrieveObligations can be executed in parallel.
retrieveObligations() is a method that subscribes to another observable. This method is used in a few other methods.
I've refactored it and it seems to work. But did I refactor it in a proper way or my code can be improved?
this.dossierService.getIdTree()
.pipe(
map(idTree => {
this.idTree = idTree;
}),
switchMap(() => {
return this.bootstrappingService.refreshObligations(this.idTree)
}),
switchMap(
() => {
return this.dossierPersonsService.retrieveDossierPersons(this.idTree)
},
)
)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
Something like this (not syntax checked):
ngOnInit(): void {
this.dossierService.getIdTree().pipe(
switchMap(idTree =>
this.bootstrappingService.refreshObligations(idTree)).pipe(
switchMap(() => this.dossierPersonsService.retrieveDossierPersons(idTree).pipe(
tap(debtors => this.debtors = debtors)
)),
switchMap(() => this.retrieveObligations())
)
).subscribe();
}
Using a higher-order mapping operator (switchMap in this case) will ensure that the inner observables are subscribed and unsubscribed.
In this example, you don't need to separately store idTree because you have access to it down the chained pipes.
You could try something like:
ngOnInit(): void {
const getIdTree$ = () => this.dossierService.getIdTree();
const getObligations = idTree => this.bootstrappingService.refreshObligations(idTree);
const getDossierPersons = idTree => this.dossierPersonsService.retrieveDossierPersons(idTree);
getIdTree$().pipe(
switchMap(idTree => forkJoin({
obligations: getObligations(idTree)
debtors: getDossierPersons(idTree),
}))
).subscribe(({obligations, debtors}) => {
// this.retrieveObligations(); // seems like duplicate of refreshObligations?
this.debtors = debtors;
});
}
Depending on the rest of the code and on the template, you might also want to avoid unwrapping debtors by employing the async pipe instead
forkJoin will only complete when all of its streams have completed.
You might want also want to employ some error handling by piping catchError to each inner observable.
Instead of forkJoin you might want to use mergeMap or concatMap (they take an array rather than an object) - this depends a lot on logic and the UI. concatMap will preserve the sequence, mergeMap will not - in both cases, data could be display accumulatively, as it arrives. With forkJoin when one request gets stuck, the whole stream will get stuck, so you won't be able to display anything until all streams have completed.
You can use switchMap or the best choice is concatMap to ensure orders of executions
obs1$.pipe(
switchMap(data1 => obs2$.pipe(
switchMap(data2 => obs3$)
)
)

How to subscribe to an observable once?

My function access() needs to subscribe once, each and every call.
In the snippet below, $valueChanges emits data to each change made. Calling access() without const $ = ... and $.unsubscribe(), $valueChanges observable emits unnecessary streams of values.
Is there an rxjs operator/function that emits once in subscription inside a function? Even if the function is called multiple times the subscription emits once?
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
).subscribe((res) => {
...
$.unsubscribe();
});
}
You can consider using the take() operator, and emit only the first value before completing.
According to the documentation, the take operator
Emit provided number of values before completing.
This is how you can use it:
access() {
valueChanges
.pipe(
map((res) =>
...
),
take(1),
).subscribe((res) => {
...
});
}
Try shareReply(1). Then the original stream will be called only once and its emit will be shared with all subscribers. If the stream emits 2nd time - the update will go to all subscribers too.
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
// take(1), // in case if you need just 1 emit from it.
shareReply(1), // in case if you don't want to trigger `$valueChanges` on every subscription.
).subscribe((res) => {
...
// $.unsubscribe(); // it's useless in case of `take(1)`.
});
}

How to create an redux-observable epic that waits for 2 actions before doing anything

I would like to create an epic that listens for a explicit sequence of actions before doing work.
This epic also does not need to exist after it completes the first time.
I'm picturing something like:
function doTheThing(action$) {
return action$
// The start of the sequence
.ofType(FIRST_ACTION)
// Do nothing until the second action occurs
.waitForAnotherAction(SECOND_ACTION)
// the correct actions have been dispatched, do the thing!
.map(() => ({ type: DO_THE_THING_ACTION }))
.destroyEpic();
}
Is something like this possible with redux-observable?
As #jayphelps pointed out in the comments there are a couple variants depending on whether you need access to the various events and if the events must be strictly ordered. So the following should all fit:
1) Strictly ordered don't care about events:
action$
.ofType(FIRST_ACTION)
.take(1)
.concat(action$.ofType(SECOND_ACTION).take(1))
.mapTo({ type: DO_THE_THING_ACTION })
2) Strictly ordered do care about events
action$
.ofType(FIRST_ACTION)
.take(1)
.concatMap(
a1 => action$.ofType(SECOND_ACTION).take(1),
(a1, a2) => ({type: DO_THE_THING_ACTION, a1, a2})
)
3) Non-strictly ordered (do or don't) care about events
Observable.forkJoin(
action$.ofType(FIRST_ACTION).take(1),
action$.ofType(SECOND_ACTION).take(1),
// Add this lambda if you *do* care
(a1, a2) => ({type: DO_THE_THING_ACTION, a1, a2})
)
// Use mapTo if you *don't* care
.mapTo({type: DO_THE_THING_ACTION})
This is how it looks with redux observables:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/zip';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
function doTheThing(action$) {
return Observable
// waits for all actions listed to complete
.zip(action$.ofType(FIRST_ACTION).take(1),
action$.ofType(SECOND_ACTION).take(1),
)
// do the thing
.map(() => ({ type: DO_THE_THING_ACTION }));
}

Rx BehaviorSubject + scan pushing prior event to new subscriber?

I want to have an stream to which I can push reducer functions. Each time a reducer function is pushed, a state object should be passed to the reducer, the reducer should return a modified state value, and the updated state should be pushed to subscribers. I'm hoping my code can explain:
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.BehaviorSubject(Rx.helpers.identity);
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);
Here's the output I see:
subscriptionA 1
subscriptionA 2
subscriptionB 1
while I was hoping to see:
subscriptionA 1
subscriptionA 2
subscriptionB 2
Obviously I'm missing something fundamental here. It seems the BehaviorSubject is supposed to retain the latest value for new subscribers which would make me think that when subscriptionB subscribes to downstream that it would get the latest reduced value, but it looks like having the .scan in the middle fouls things up...or something.
What's going on here and how to I accomplish what I'm trying to accomplish? Thanks!
Can you try to see if everything is conform to your expectations if you replace
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
by
export let downstream = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState).shareReplay(1);
jsfiddle here : http://jsfiddle.net/cqaumutp/
If so, you are another victim of the hot vs. cold nature of Rx.Observable, or maybe more accurately the lazy instantiation of observables.
In short (not so short), what happens everytime you do a subscribe, is that a chain of observables is created by going upstream the chain of operators. Each operator subscribes to its source, and returns another observable up to the starting source. In your case, when you subscribe to scan, scan subscribes to upstream which is the last one. upstream being a subject, on subscription it just registers the subscriber. Other sources would do other things (like register a listener on a DOM node, or a socket, or whatever).
The point here is that every time you subscribe to the scan, you start anew, i.e. with the initialState. If you want to use the values from the first subscription to the scan, you have to use the share operator. On the first subscription to the share, it will pass your subscription request on to the scan. On the second and subsequent ones, it will not, it will register it, and forward to the associated observer all the values coming from the scan firstly subscribed to.
I have a solution that seems to be giving me the results I'm looking for. I would appreciate it if others could verify that it's an appropriate solution.
import Rx from 'rx';
import { Map } from 'immutable';
let initialState = Map({ counter: 0 });
export let upstream = new Rx.Subject();
let downstreamSource = upstream.scan((state, reducer) => {
return reducer(state);
}, initialState);
export let downstream = new Rx.BehaviorSubject(initialState);
downstreamSource.subscribe(downstream);
let increment = state => {
return state.update('counter', counter => counter + 1);
};
upstream.onNext(increment);
downstream.subscribe(state => {
console.log('subscriptionA', state.get('counter'));
});
upstream.onNext(increment);
setTimeout(() => {
downstream.subscribe(state => {
console.log('subscriptionB', state.get('counter'));
});
}, 3000);

Categories

Resources