RxJS share parent observable among partitioned child observables - javascript

I'm coding a game in which the character can fire their weapon.
I want different things to happen when the player tries to fire, depending on whether they have ammo.
I reduced my issue down to the following code (btw I'm not sure why SO's snippet feature does not work, so I made CodePen where you can try out my code).
const { from, merge } = rxjs;
const { partition, share, tap } = rxjs.operators;
let hasAmmo = true;
const [ fire$, noAmmo$ ] = from([true]).pipe(
share(),
partition(() => hasAmmo),
);
merge(
fire$.pipe(
tap(() => {
hasAmmo = false;
console.log('boom');
}),
),
noAmmo$.pipe(
tap(() => {
console.log('bam');
}),
)
).subscribe({
next: val => console.log('next', val),
error: val => console.log('error', val),
complete: val => console.log('complete', val),
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>
When I run this code I get the following:
"boom"
"next" true
"bam"
"next" true
"complete" undefined
I don't understand why I get a "bam".
The first emission goes to fire$ (I get a "boom"), which makes sense because hasAmmo is true. But as a side-effect of fire$ emitting is that the result of the partition condition changes, which I guess is causing me to get a "bam".
Am I not supposed to cause side-effects that affect partition()?
Or maybe is there an issue with the way I share() my parent observable? I may be wrong but I would intuitively think the fire$ and noAmmo$ internally subscribe to the parent in order to split it, in which case share() should work?

It actually works correctly. The confusion comes from the partition operator which is basically just two filter operators.
If you rewrite it without partition it looks like this:
const fire$ = from([true]).pipe(
share(),
filter(() => hasAmmo),
);
const noAmmo$ = from([true]).pipe(
share(),
filter(() => !hasAmmo),
);
Be aware that changing hasAmmo has no effect on partition itself. partition acts only when it receives a value from its source Observable.
When you later use merge() it makes two separate subscriptions to two completely different chains with two different from([true])s. This means that true is passed to both fire$ and noAmmo$.
So share() has no effect here. If you want to share it you'll have to wrap from before using it on fire$ and noAmmo$. If the source Observable is just from it's unfortunately going to be even more confusing because the initial emission will arrive only to the first subscriber which is fire$ later when used in merge:
const shared$ = from([true]).pipe(
share(),
);
const fire$ = shared$.pipe(...);
const noAmmo$ = shared$.pipe(...);
The last thing why you're receiving both messages is that partition doesn't modify the value that goes through. It only decides which one of the returned Observable will reemit it.
Btw, rather avoid partition completely because it's probably going to be deprecated and just use filter which is more obvious:
https://github.com/ReactiveX/rxjs/issues/3797
https://github.com/ReactiveX/rxjs/issues/3807

Related

Struggling with flatMap vs concatMap in rxJs

I am struggling to understand the difference between the flatMap and concatMap in rxJs.
The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap
So I went and tried things out by my self.
import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";
function createObs1() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(1);
subscriber.complete();
}, 900);
});
}
function createObs2() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(2);
//subscriber.next(22);
//subscriber.next(222);
subscriber.complete();
}, 800);
});
}
function createObs3() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(3);
//subscriber.next(33);
//subscriber.next(333);
subscriber.complete();
}, 700);
});
}
function createObs4() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 600);
});
}
function createObs5() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(5);
subscriber.complete();
}, 500);
});
}
createObs1()
.pipe(
flatMap((resp) => {
console.log(resp);
return createObs2();
}),
flatMap((resp) => {
console.log(resp);
return createObs3();
}),
flatMap((resp) => {
console.log(resp);
return createObs4();
}),
flatMap((resp) => {
console.log(resp);
return createObs5();
})
)
.subscribe((resp) => console.log(resp));
console.log("hellooo");
I have used that playground here playground example
Questions
1)
From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)
What am I doing wrong or have undestood wrong?
2)
If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. Even if you change to concatMap it messes things and results come mixed. Multiple numbers that I expect only once come multiple times. The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?
How I test the example on playground. I just remove only 1 letter from the last console.log("hello"). Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.
Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.
createObs1().subscribe( (resp1) => {
console.log(resp1);
createObs2().subscribe( (resp2) => {
console.log(resp2);
createObs3().subscribe( (resp3) => {
console.log(resp3);
createObs4().subscribe( (resp4) => {
console.log(resp4);
createObs5().subscribe( (resp5) => {
console.log(resp5);
})
})
})
})
})
Your test scenario is not really sufficient to see the differences between these two operators. In your test case, each observable only emits 1 time. If an observable only emits a single value, there is really no different between concatMap and flatMap (aka mergeMap). The differences can only be seen when there are multiple emissions.
So, let's use a different scenario. Let's have a source$ observable that simply emits an incrementing integer every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits a variable number of times every 1 second, then completes.
// emit number every second
const source$ = interval(1000).pipe(map(n => n+1));
// helper to return observable that emits the provided number of times
function inner$(max: number, description: string): Observable<string> {
return interval(1000).pipe(
map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
take(max),
);
}
Then let's define two separate observables based on the source$ and the inner$; one using concatMap and one using flatMap and observe the output.
const flatMap$ = source$.pipe(
flatMap(n => inner$(n, 'flatMap$'))
);
const concatMap$ = source$.pipe(
concatMap(n => inner$(n, 'concatMap$'))
);
Before looking the differences in the output, let's talk about what these operators have in common. They both:
subscribe to the observable returned by the passed in function
emit emissions from this "inner observable"
unsubscribe from the inner observable(s)
What's different, is how they create and manage inner subscriptions:
concatMap - only allows a single inner subscription at a time. As it receives emissions, it will only subscribe to one inner observable at a time. So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2". This is consistent with how the concat static method behaves.
flatMap (aka mergeMap) - allows many inner subscriptions. So, it will subscribe to the inner observables as new emissions are received. This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit. This is consistent with how the merge static method behaves (which is why I personally prefer the name "mergeMap").
Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$:
Hopefully, the above explanation helps to clear up your questions!
#1 - "use of flatMap should mix the outputs"
The reason this wasn't working as you expected was because only one emission was going through the flatMap, which means you only ever had a single "inner observable" emitting values. As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.
#2 - "...and include the code with multiple emitted events then things get messy."
The "things get messy" is due to having multiple inner subscription that emit values.
For the part you mention about using concatMap and still getting "mixed" output, I would not expect that. I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled (seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output). Maybe code sandbox has a similar problem.
#3 - "The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library"
This makes sense. You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.
In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about. Imagine you have a component that reads an id from a route param. It uses this id to make an http call to fetch data.
itemId$ = this.activeRoute.params.pipe(
map(params => params['id']),
distinctUntilChanged()
);
item$ = this.itemId$.pipe(
switchMap(id => http.get(`${serverUrl}/items/${id}`)),
map(response => response.data)
);
We want item$ to emit only the "current item" (corresponds to the id in the url). Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.
If we chose mergeMap, we would end up with many inner observables that would emit the results of all of those http calls. At best, the screen will flicker as all those different calls come back. At worst (if the calls came back out of order) the UI would be left displaying data that isn't in sync with the id in the url :-(
If we chose concatMap, the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.
But, with switchMap, whenever a new emission (itemId) is received, it will unsubscribe from the previous inner observable and subscribe to the new one. This means it will not ever emit the results from the old http calls that are no longer relevant. :-)
One thing to note is that since http observables only emit once, the choice between the various operators (switchMap, mergeMap, concatMap) may not seem to make a difference, since they all perform the "inner observable handling" for us. However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.
Every time the first observable emits, a second observable is created in the flatMap and starts emitting. However, the value from the first observable is not passed along any further.
Every time that second observable emits, the next flatMap creates a third observable, and so on. Again, the original value coming into the flatMap is not passed along any further.
createObs1()
.pipe(
flatMap(() => createObs2()), // Merge this stream every time prev observable emits
flatMap(() => createObs3()), // Merge this stream every time prev observable emits
flatMap(() => createObs4()), // Merge this stream every time prev observable emits
flatMap(() => createObs5()), // Merge this stream every time prev observable emits
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
So, it's only the values emitted from createObs5() that actually get emitted to the observer. The values emitted from the previous observables have just been triggering the creation of new observables.
If you were to use merge, then you would get what you may have been expecting:
createObs1()
.pipe(
merge(createObs2()),
merge(createObs3()),
merge(createObs4()),
merge(createObs5()),
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
// 4
// 3
// 2
// 1

Is it possible to wait for all actions in the ofType operator to be called before calling next?

I have an array of redux actions which when ALL have been called I want to call another action in my epic. The actions in the array are changeable, dynamic. What I'm hoping to find would be the equivalent of the ofType operator working like this:
ofType(FETCH_USER_PROFILE_SUCCESS && FETCH_USER_PREFERENCES_SUCCESS)
When both actions have been called, another is called further down the pipe. Of course the ofType operator doesn't work like this. It only needs one of the actions to be called to continue.
I have read that combineLatest might be the solution here but I've had no luck. Below is what I imagine the solution could look like but obviously this doesn't work for me. Any suggestions would be appreciated.
const actions = ['FETCH_USER_PROFILE_SUCCESS', 'FETCH_USER_PREFERENCES_SUCCESS'];
const asTypes = actions.map((action) => ofType(action);
const userEpic = (action$, state$) =>
action$.pipe(
combineLatest(asTypes);
mergeMap(() =>
of({
type: 'SET_USER_READY'
})
)
);
Following on from replies below:
Just to clear what I meant by dynamic. The array of actions might include more actions depending on a user's profile (e.g. 'FETCH_USER_POSTS_SUCCESS'), essentially an unknown number of actions depending on the user.
So, is possible to generate this dynamically:
zip(
action$.pipe(ofType('FETCH_USER_PROFILE_SUCCESS')),
action$.pipe(ofType('FETCH_USER_PREFERENCES_SUCCESS')),
)
const actions = ['FETCH_USER_PROFILE_SUCCESS', 'FETCH_USER_PREFERENCES_SUCCESS', unknown number of more actions];
const actions = dynamic.forEach((action) => action$.pipe(ofType(action))
zip(
actions
)
why not making one general action like executeAllActions and inside this action payload you pass an array which contain all the references for all the actions you want like
executeAllActions = {
type: 'exectueAllActions',
actionsList: [ fetchCategories, fetchProducts, ...],
}
then you listen for this action
this.actions$.pipe(
ofType(executeAllActions),
concatMap(action => [ ...actions.actionsList ])
)
where we used concatMap to return a list of actoins that will all be executed.
I wish I can try this idea now, but I think it somehow possible.
If it works for you please let me know.
There's numerous ways of approaching this because there are some important missing details. But I'll make some guesses :)
The biggest is whether the order in which they arrive matters. If it does not, using zip() is probably what you want. It will pair emissions 1:1 (actually any number of observables, not just two, e.g. 1:1:1 for three, etc)
Even in that case, there's the question of what you want to do if the rate you receive the actions varies between them. What happens if you receive FETCH_USER_PROFILE_SUCCESS's twice as fast as often as you receive FETCH_USER_PREFERENCES_SUCCESS's? Should you continue to buffer them? Do you drop them? Do you error?
If buffering them is fine or you otherwise provide guarantees they will always happen at the same rate, zip() is works.
Now if you want to make sure they come in some sequence, things get more complicated still. You'd likely have a chain utilizing mergeMap, switchMap, exhaustMap, or concatMap. Deciding which depends again on what you want to do if the rate you get the actions isn't always the same.
import { zip } from 'rxjs';
import { map, take, switchMap } from 'rxjs/operators';
import { ofType } from 'redux-observable';
// Pair up the emissions 1:1 but without regard to the sequence
// in which they are received i.e. they can be in either order
const userEpic = (action$, state$) =>
zip(
action$.pipe(ofType('FETCH_USER_PROFILE_SUCCESS')),
action$.pipe(ofType('FETCH_USER_PREFERENCES_SUCCESS')),
// ...etc more observables
).pipe(
map(() => ({
type: 'SET_USER_READY'
}))
);
// FETCH_USER_PROFILE_SUCCESS must come before FETCH_USER_PREFERENCES_SUCCESS
// and if we receive another FETCH_USER_PROFILE_SUCCESS before we get
// the FETCH_USER_PREFERENCES_SUCCESS, start over again (switchMap)
const userEpic = (action$, state$) =>
action$.pipe(
ofType('FETCH_USER_PROFILE_SUCCESS'),
switchMap(() =>
action$.pipe(
ofType('FETCH_USER_PREFERENCES_SUCCESS'),
take(1) // important! we only want to wait for one
)
),
map(() => ({
type: 'SET_USER_READY'
}))
)
The actions in the array are changeable, dynamic
It's not clear how you want them to change, so I didn't include a mechanism for doing so.
--
There's also a similar question here: https://stackoverflow.com/a/45554647/1770633

How to explain that Observable.of(Math.random()) always returns the same value?

I'm not really sure how to explain it. The observable returned by of() method is not multicast.
If we'd do sth like the following:
const obs = of(Math.random()).pipe(tap(()=>console.log('side effect'));
obs.subscribe(console.log);
obs.subscribe(console.log);
obs.subscribe(console.log);
We'd see 3 side effects, but 3 also equal values. Why? Normally observables are recalculated on each subscription.
e.g.
new Observable(observer => observer.next(Math.random()))
would return 3 different values.
Why does an observable created with a static 'of' method behave differently? I'm not sure how to explain it. Does it have sth to do with observables caching?
EDIT
Here's the small example that helped me prove, that 'of' observables are indeed loaded on demand.
const obs = of(undefined).pipe(
map(() => Math.random())
);
or even
const obs = of(Math.random).pipe(
map(random => random())
);
If you were to call, say, of(0.5), then of would see that you passed it 0.5 and creates an observable based on that. Each new subscription will cause the observable to emit the value (0.5) and then complete.
If you changed that to of(0.25 * 2), then of still sees that you passed it 0.5, and so it behaves exactly the same. A calculation did happen before hand to create the 0.5, but of knows nothing about that.
And if you change it to of(Math.random()) and by chance the calculated value is 0.5, the behavior is again exactly the same. of got passed a 0.5, so it creates an observable that spits out 0.5 and then completes. It has no idea how that 0.5 was calculated.
The value passed to of is prepared when the code is read. This is only a JS thing nothing related to RxJs in particular. Even if you don't subscribe to the observable. Ex:
const MyMathRandom = () => {
console.log('MyMathRandom has been run');
return Math.random();
}
const test = of(MyMathRandom())
Will display in the console MyMathRandom has been run.
In your case, you want to function to be executed lazily and there's an operator for that: defer
If you update your code to:
const source = defer(() => of(Math.random()))
source.subscribe(x => console.log(x));
source.subscribe(x => console.log(x));
source.subscribe(x => console.log(x));
The output will be what you're looking for. Ex:
0.20757387233599833
0.6417609881625241
0.09756371489129778
Live demo here: https://stackblitz.com/edit/rxjs-jymadr

Rxjs nested subscribe with multiple inner subscriptions

Original promise based code I'm trying to rewrite:
parentPromise
.then((parentResult) => {
childPromise1
.then(child1Result => child1Handler(parentResult, child1Result));
childPromise2
.then(child1Result => child2Handler(parentResult, child2Result));
childPromise3
.then(child1Result => child3Handler(parentResult, child3Result));
});
I'm trying to figure a way how to avoid the nested subscriptions anti-pattern in the following scenario:
parent$
.pipe(takeUntil(onDestroy$))
.subscribe((parentResult) => {
child1$
.pipe(takeUntil(onDestroy$))
.subscribe(child1Result => child1Handler(parentResult, child1Result));
child2$
.pipe(takeUntil(onDestroy$))
.subscribe(child2Result => child2Handler(parentResult, child2Result));
child3$
.pipe(takeUntil(onDestroy$))
.subscribe(child3Result => child3Handler(parentResult, child3Result));
});
What would be the correct 'RxJS way' to do this?
That seems pretty strange to me. You're creating new subscription for each child every time parentResult arrives. Even though those eventually indeed will be destroyed (assuming onDestroy$ implementation is correct), seems wrong.
You probably want withLatestFrom(parent$) and three separate pipes for each child.
It might look something like:
child1$.pipe(takeUntil(globalDeath$), withLatestFrom(parent$)).subscribe(([childResult, parentResult]) => ...). Not sure if my JS is correct, can't test it at the moment; but the point is: you're getting the latest result from the parent$ every time child1$ fires. Note that you can reverse the direction if necessary (withLatestFrom(child1$)).
You can: 1) pass parent$ through share, and 2) use flatMap three times, something like:
const sharedParent$ = parent$.pipe(share());
sharedParent$.pipe(
flatMap(parentResult => forkJoin(of(parentResult), child1$)),
takeUntil(onDestroy$)),
.subscribe((results) => child1Handler(...results)); // repeat for all children
(If there's more than 2 children, extracting that into a function with child stream and handler as parameters is a good idea).
That's following the original behavior of waiting with subscribing children until parent$ emits. If you don't need that, you can skip flatMap and just forkJoin sharedParent$ and children.
How about using higher order observables? Something like this:
const parentReplay$ = parent$.pipe(shareReplay(1));
of(
[child1$, child1Handler],
[child2$, child2Handler],
[child3$, child3Handler]
).pipe(
mergeMap([child$, handler] => parentReplay$.pipe(
mergeMap(parentResult => child$.pipe(
tap(childResult => handler(parentResult, childResult))
)
)
).subscribe();
If you were using Promises then the corresponding Observables emit only once and then complete.
If this is the case, you can use forkJoin to execute in parallel the child Observables.
So the code could look like
parent$.pipe(
takeUntil(onDestroy$),
// wait for parent$ to emit and then move on
// the following forkJoin executes the child observables in parallel and emit when all children complete - the value emitted is an array with the 3 notifications coming from the child observables
concatMap(parentResult => forkJoin(child1$, child2$, child3$)).pipe(
// map returns both the parent and the children notificiations
map(childrenResults => ({parentResult, childrenResults})
)
).subscribe(
({parentResult, childrenResults}) => {
child1Handler(parentResult, childrenResults[0]);
child1Handler(parentResult, childrenResults[1]);
child1Handler(parentResult, childrenResults[2]);
}
)

RxJs operator with unconditional execution in the end of each iteration

Is there a Rx operator or composition to guarantee some logic execution last per each observable emission?
Let's assume the following context:
endless or continuous sequence
conditional logic like filter() to skip some emissions
some logic in the end of each iteration in a doAlways()-like operator
Please refer to numbered comments in the code-sample below
Notes:
finalize() would require the sequence to terminate (violates p.1)
iif() or regular if inside switchMap() is an option but makes the code more unreadable
Code snippet to illustrate: Step (3) should execute always, per-iteration, last, i.e. we want always start and finish log in a doAlways()-like operator instead of tap()
import { of, interval } from 'rxjs';
import { tap, filter, switchMap } from 'rxjs/operators';
const dataService = { update: (z) => of(z /*posts data to back-end*/) };
const sub = interval(1000).pipe( // <-- (1)
tap(a => console.log('starting', a)),
filter(b => b % 100 === 0), // <-- (2)
switchMap(c => dataService.update(c)),
tap(d => console.log('finishing', d)) // <-- (3) should execute always even (2)
)
.subscribe(x => console.log(x));
No such operator exists, and that's because it can't exist. Once you filtered out a value, the resulting observable just doesn't emit it anymore. Any operator "downstream" just simply doesnt know about its existence.
To illustrate, you included a switchMap to some service, which depends on the emitted value. For obvious reasons that operator that cannot logically be applied if there is no value to switch on.
You would have to "tag" each value instead of filtering it and defer the filter to after the tap call, but even then scenarios like switching to another observable would require more detailed requirements.
Think of the observable as a conveyor belt on which you place items. Each operator is a room through which the belt leads. Inside each room a worker can decide what to do with each item: modify it, take it away, put new items in instead etc. However, each worker only sees what the conveyor belt brings along — they don't know what other rooms came before them or what has been done there.
To achieve what you want, the worker in the last room would have to know about an item that he never received, which would require additional knowledge they don't have.
Nope, what you ask is not possible due to the fact that filtered notifications won't ever be passed on. You would need a totally new type of notification that is not consumed by any other operator than the one you describe.
Now, here is an idea that is probably not recommended. You can misuse the error notification to skip some operators, but that will interfere with any other error handling so that's not something you should do...
const sub = interval(1000).pipe(
tap(a => console.log('starting', a)),
mergeMap(b => b % 100 === 0 ? of(b) : throwError(b)),
switchMap(c => dataService.update(c)),
catchError(b => of(b)),
tap(d => console.log('finishing', d))
)
Note that we don't use filter but map to either a next or an error notification depending on the condition. Errors will naturally be ignored by most operators and consumed by tap, subscribe or catchError. This is a way to put a tag on the item so the workers know they shouldn't touch it (in the analogy described by Ingo)
I would split it for 2 streams with partition, and them would merge them back.
https://stackblitz.com/edit/rxjs-8z9jit?file=index.ts
import { of, interval, merge } from 'rxjs';
import { tap, switchMap, partition, share } from 'rxjs/operators';
const dataService = { update: z => of(z /*posts data to back-end*/) };
const [sub, rest] = interval(1000).pipe(
tap(a => console.log('starting', a)),
share(),
partition(b => b % 2 === 0)
);
merge(
sub.pipe(
switchMap(c => dataService.update(c)),
tap(() => console.log('Ok'))
),
rest
)
.pipe(tap(d => console.log('finishing', d)))
.subscribe(x => console.log(x));
A possible work-around is to eliminate the .filter() of course.
Given:
observable.pipe(
filter(() => condition),
switchMap(p => service.otherObservable(p)),
tap(d => console.log('not triggered when condition is false'))
)
Can be rewritten as:
observable.pipe(
switchMap(p =>
(!condition) ?
of(null) :
service.otherObservable(p)),
tap(d => console.log('always triggered'))
)

Categories

Resources