Why does assigning an observer to a subject in the subscribe break sometimes? - rxjs

Imagine the following simple case:
you get some data from a source, resulting in an observable, lets call it obs$
you need to transform it (frex a simple synchronous sort) and hand the result some kind of subject, lets call it subject$$
You now have several ways to implement it. The simplest might look like:
obs$
.pipe(
takeUntil(this.#destroy$$)
)
.subscribe(
result => {
this.subject$$.next(result.dSomething())
});
Works like a charm. A bit later you write some more similar functions, and thus would like to separate the transformation from the assignment. Basically you move the doSomething from the subscribe to a map in the pipe. This could look like:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(res => { this.subject$$.next(res) });
Works fine as well. Though the assignment now looks a bit clunky, surely there is a better way to do this? You might be tempted to go for something like:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(this.subject$$.next);
But quickly find that this fails, as you've got no access to your outer scopes this. The suggested solution looks like this:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(this.subject$$);
Which works just fine in many cases.
Imagine my surprise, when all of a sudden I stumbled into a waterfall of errors:
EmptyError: no elements in sequence
showing up in four completely unrelated places, bubbling up through the code, without an obvious place to even try catch it.
Now following the white rabbit, I searched for far to long until I finally managed to discover the place of origin. Still it doesn't make any sense to me.
Why is the observer "disconnecting" for a moment when there is no activity that could result in a complete? Funnily enough, the subject is catching back up shortly afterwards, so this is not completely breaking the assignment.
Of course, I can always use the slightly more verbose assignment, but understanding why this fails only in some cases would really help avoid those.

Its very hard to suggest anything because I don't know what part of your code and what RxJS chain gives you the EmptyError.
However, I'm suspicious that the problem comes from .subscribe(this.subject$$) because this is not the same as .subscribe(res => this.subject$$.next(res));
All Subjects behave like Observables and observers at the same time. Observers implement this interface that handles all three notification types. If you use .subscribe(this.subject$$) you're passing also error and complete notifications from the source Observable to subject$$ and this is very like your problem. When obs$ completes, the complete notification is passed to subject$$ that completes as well and since it's a Subject it will never ever emit any next again and to all its new observers it'll just pass complete notification and that's it. So this might explain why you're seeing EmptyError somewhere else in you chains that use subject$$.
With .subscribe(res => this.subject$$.next(res)); you're only passing next notifications and complete or error notifications are ignored.

Related

Need help understanding this Ngrx effect

The Ngrx documentation for using an effect with state selected from the store
states (no pun intended)
Note: For performance reasons, use a flattening operator like concatMap to prevent the
selector from firing until the correct action is dispatched.
...and provides the example:
addBookToCollectionSuccess$ = createEffect(() => this.actions$.pipe(
ofType(CollectionApiActions.addBookSuccess),
concatMap(action => of(action).pipe(
withLatestFrom(this.store.select(fromBooks.getCollectionBookIds))
)),
tap(([action, bookCollection]) => console.log('whatever'))
), { dispatch: false }
);
The example has since been changed to use concatLatestFrom, but that's not important here. What I'm interested in is understanding the original suggestion. I think I understand the intent, to avoid having withLatestFrom processing results from getCollectionBookIds apart from when the source action triggers, but I don't understand why the solution needs to involve wrapping withLatestFrom in a manually constructed Observable using of(action). Wouldn't something like this be just as effective and a lot more readable?
concatMap(() => this.store.select(selectSelectedBookIds).pipe(first())),
My understanding of Rxjs is not that good and after a few days of trying to working this out I suspect I'm still missing something.
Your suggestions should work fine, if you just need the selected state:
concatMap(() => this.store.select(selectSelectedBookIds).pipe(first())),
However, should you also need the action itself, the following will pass along an array containing both the action and your selected state:
concatMap(action => of(action).pipe(
withLatestFrom(this.store.select(fromBooks.getCollectionBookIds))
)),

How can I use an observable to perform a recursive search without going over duplicates?

I'm going to phrase the question in rxjs, but I suppose it's similar for any Rx or observable library.
Say I have an observable of users, and a function getAssociates(user) that returns another observable of users. I want to use the getAssociates function on every user in the observable and return an observable of those associates. flatMap is enough for this.
But I also want to run getAssociates on each associate that comes back, but without ever running it twice on any given user (since two users might share an associate, and if A has B as an associate, then B also has A as an associate).
Something like the expand operator is what I think I'm looking for:
seedUsers.pipe(
expand(user => getAssociates(user)),
);
but how can I get in the bit about not running twice on any given user? I could maintain a list of seen users, but I'd like to achieve it in a functional style.
Conceptually, you need to :
Keep track of the known users, for example using a Set
Filter the known users before making a request, for example using filter operator
Here is a suggestion :
let knownIds = new Set();
getAllItems(Ids){
return from(Ids).pipe(
filter(id => ! knownIds.has(id)),
concatMap(id => getAllItems(id)),
map( id => knownIds.add(id))
)
}
getAllItems([originalId]).subscribe( allItems=> console.log)
Notes:
I guess you could manage to do it using rxjs, but none of the solutions I can think of is simpler than using a set + filter.
I used concatMap to ensure you don't run the request twice. Using mergeMap (flatMap), you could have a scenario like this :
---------Req(user1)----------------------------resp(user1)-------------------------
---Req(user2)--------resp(user2)--Req(user1)-----------------resp(user1)-
But if you accept having eventually more than 2 requests per user, you can use flatMap to gain speed.

When should we use the RxJS tap operator?

I do not understand from the docs. Could anyone explain it to me?
Most of the operators are working in streamed sequence, for example:
source$.pipe(
map((a: string) => changeAndReturnArray(a)),
filter((b: string[]) => giveMeOnlySymbolsThatAreAfterNInAlphabet(b)),
switchMap((c: string[]) => putToSomeObservable(c))
....
);
In that example you are not 'breaking' the stream, or jumping outside of it to do some external action.
Jumping outside of stream is possible with 'tap' operator, where you can:
call functions that will cause some side effect, that might be visible to end user (for example - display dialog, show snackbar, redirect to different route (but in my opinion it's not recommended to use tap in that way))
dispatch actions for store (if you are using any - for example ngrx store)
debug you're code -> console.log()
anything what can be considered as 'side effect' for your stream.
My personal opinion - use 'tap' only if you can't find any better solution. Jumping outside of stream and calling some side effect can be double edged sword, especially when your dealing with some bigger application. Side effect are always harder to maintain, and you can finish with application that is doing magic stuff without any reason.
You can use it to perform a side effect for example. Or you can use it to see what's the current value that is being passed around without affecting/modifying the Observable. So something like a console.log() but inside the stream.
Decalration
public tap(nextOrObserver: Observer | function, error: function, complete: function): Observable
tap is replacement of do operator which returns observable identical to your source observable. for each value emitted, perform a side-effect. it has 3 optional parameters.
nextOrObserver: A normal Observable object to perform side effect.
error: Callback for errors in source Observable.
complete: Callback for completion of the source.
Recommended for debugging purpose.

withLatestFrom using two Redux actions

I have been trying to use rxjs properly with Redux observable and Redux actions. I've tried many different combinations of calling the below functions and every possible combination has given me different errors.
Basically, I have two actions that can be dispatched in my app: SET_CIRCULAR_DATA and SET_MODAL_OVER_DEEP_LINK_FLAG.
I'd like another action to be dispatched, determineFoundInProducts() when SET_MODAL_OVER_DEEP_LINK_FLAG happens followed by the first (successful) SET_CIRCULAR_DATA action. Any subsequent SET_CIRCULAR_DATA actions should be ignored until SET_MODAL_OVER_DEEP_LINK_FLAG happens again. Because of this need, I felt withLatestFrom() made the most sense. I also thought that takeUntil() and repeat() might be helpful to help me accomplish what I need, though I'm not including them in this code example because I'm stuck at the withLatestFrom() part.
I've tried different combinations of piping, not piping, using switchMap, map, mapTo, but nothing seems to work. I am importing all the used rxjs functions and I'm combining epics at the root, so it couldn't be that. I just can't seem to get the right combination to not get this to error.
Some examples of errors are
Actions must be plain objects. Use custom middleware for async actions.
You provided 'undefined' where a stream was expected
I am using RXJS ^5.5.6 and Redux Observable ^0.17.0.
import 'rxjs';
import { Observable } from 'rxjs/Observable';
import { withLatestFrom, map, tap, mergeMap } from 'rxjs/operators';
import ....all the types and actions
export const handleProductDetailsModalOnLoginRedirect = (action) =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA).pipe(
withLatestFrom(
action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG)
),
mergeMap(([first, second]) => {
console.log(first, second);
return determineFoundInProducts();
})
);
EDIT
I was struggling in my original post to determine what part of the code was breaking. What I had trouble with was that the redux observable action could not communicate with the rxjs functions. What I found was that piping appropriately piped (pun intended) the action to be workable with rxjs functions. Here is what I did.
action.ofType(navigationTypes.types.INIT_REDIRECT_MODAL_LOGIC)
.switchMap(() =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA)
.withLatestFrom(action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG))
.take(1)
.pipe(
map(([first]) => {
// Do stuff
return determineFoundInProducts();
}))
)
The answer depends on what you want to happen if multiple SET_MODAL_OVER_DEEP_LINK_FLAG actions are dispatched before any SET_CIRCULAR_DATA action is, as well as whether they should some how be paired together with some sort of unique ID or not.
It sounds most likely like you're looking for exhaustMap.
The exhaustMap operator is basically the opposite behavior of switchMap. It maps inputs to inner observable and flattens its output, ignore other input values until that observable completes. It exhausts the inner Observable.
In your use case it means that we first listen for SET_MODAL_OVER_DEEP_LINK_FLAG, then we listen for a single SET_CIRCULAR_DATA, but while we're waiting for that subsequent SET_CIRCULAR_DATA we ignore all possibly incoming SET_MODAL_OVER_DEEP_LINK_FLAG actions. Note that the take(1) is important, because otherwise we'll listen for a stream of every SET_CIRCULAR_DATA, not just one.
export const handleProductDetailsModalOnLoginRedirect = (action) =>
action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG)
.exhaustMap(() =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA)
.take(1)
.map(() => determineFoundInProducts())
);
Btw it's common to ask Redux Observable questions that are actually just RxJS questions. This is a cool thing to note because the RxJS community is massive so you'll find a TON more resources on it, as well as have a better chance of getting help if you're able to rephrase questions to be agnostic of Redux Observable--e.g. change ofType('TYPE') to a filter(d => d.type === 'TYPE')
The above answer will definitely help others. However, what I was struggling with was having redux observable action communicate with RXJS functions. Please see my edit to my original post for solution.

RxJS 5.5.0-beta : mergeMap behavior differing from map + mergeAll

I am having trouble creating a reproducible example so bear with me.
I have a library I created that wraps async API responses in an Observable. Simplifying greatly, the library has functions like lib.ask("end-point") that return an Observable with the response of the API call to that end point.
I want to run that API call every time another event happens. I'm attempting to model that with some version of mergeMap/switchMap/concatMap, depending on the situation. Let's use mergeMap as an example.
If I use map + mergeAll, it works just fine. I am writing it like so:
var responses$ = event$.pipe(
map(()=>lib.ask("end-point")),
mergeAll()
);
When I subscribe to responses$, I get my response!
However, if I rewrite the same chain with mergeMap instead, it does not work:
var responses$ = event$.pipe(
mergeMap(()=>lib.ask("end-point"))
);
In this case, when I subscribe I do not get any results. It almost appears as if the inner subscription never gets made.
Like I said, I am struggling to create a reproducible example. I have attempted to debug what RxJS does to try to find where this thing is failing, but have not been able to figure it out. If anyone can think of why or in what situations mergeMap would behave differently than map + mergeAll, that might help me figure out where my problem is.

Resources