I have the following stream:
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
})))
)
})
)
When the inner stream throws, the fromEvent source is also stopped. How can I prevent this and keep the source stream alive?
I think you could try the following:
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
}))),
catchError(() => EMPTY)
)
})
)
where EMPTY is nothing but this:
export const EMPTY = new Observable<never>(subscriber => subscriber.complete());
The reason this is happens is that when an error occurs, even in a next callback(e.g tap's first argument), it will be passed through as:
this.destination.error(caughtError);
where destination is the next subscriber in the chain(downwards). Eventually catchError will be reached and this prevents the error from affecting the outermost stream(whose source is fromEvent).
You can try with retry operator after the first switchMap, like this
const source = fromEvent(document.querySelector('h1'), 'click').pipe(
switchMap(() => {
return timer(500).pipe(
switchMap(() => timer(500).pipe(tap(() => {
throw new Error('error')
})))
)
}),
retry()
)
If no parameter is passed to retry, then it will resubscribe to the source observable an infinite amount of times. Otherwise you can pass an integer which represents the number of times it will retry before eventually error.
Here a stackblitz.
Related
I have an effect in NgRX effect, as follows:
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data)),
map((response) => fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
return of(fromOrganisationActions.createOrganisationError(error));
})
)
);
However, my stream never seems to end when the catchError is triggered, i.e. in the instance of this.organisation.createOrganisation returning a 400 error.
The action, fromOrganisationActions.createOrganisationError(error) is triggered and my reducer is triggered from this... but if I re-trigger the fromOrganisationActions.createOrganisation effect, this effect runs but the API call is never made a second time.
If I configure it as follows, and dispatch it manually, it works:
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data)),
map((response) => fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
this.store.dispatch(fromOrganisationActions.createOrganisationError(error));
return throwError(error);
})
)
);
But other examples online suggest the first way should work, but it is not for me, and I do not understand why my stream never ends.
Can somebody please advise and tell me why my stream never ends in the first instance?
The catchError should be added to the inner observable.
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data).pipe (
map((response) =>
fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
return of(fromOrganisationActions.createOrganisationError(error));
})
)),
)
);
These mistakes can be caught with eslint-plugin-ngrx, more details about the rule here.
How to pause observable every 15 seconds then wait for 5s and then continue emitting?
I made this example:
const digits = interval(1000);
const pauser$ = interval(20000).pipe(mapTo(true));
pauser$.subscribe(item =>
console.log(`${new Date().toISOString()} pause fired`)
);
const pauseStopper$ = pauser$.pipe(
concatMap(_ => timer(5000)),
tap(() => console.log(`${new Date().toISOString()} pause stopped`)),
mapTo(false)
);
const observable = merge(pauser$, pauseStopper$).pipe(
startWith(false),
switchMap(paused => (paused ? NEVER : digits))
);
observable.subscribe(
item => console.log(`${new Date().toISOString()}: ${item}`),
console.error,
() => console.log("complete")
);
After first pause cycle it behaves nearly close to what I need. Any ideas on how to make this more clear?
stackblitz example
I created a player function that gets three params:
source$: Your source observable that should be paused/played
play$: Continues to emit your source$ observable
pause$: Pauses your source$ observable
const player = <T>(source$: Observable<T>, play$: Observable<void>, pause$: Observable<void>): Observable<T> =>
merge(
play$.pipe(
switchMap(() => of(source$))
),
pause$.pipe(
switchMap(() => of(NEVER))
)
).pipe(
switchMap(stream => stream)
)
This function currently throws an error as far as I can see. In the short time I have atm I could not fix the error. The error is caused by of(void 0) in the pause$ switchMap. Later I will try to fix this error. Forfeit the error the function works.
You can now use a play$ and pause$ Observable to emit the play/pause:
const play$: Observable<void> = interval(20000).pipe(
startWith(void 0),
mapTo(void 0)
);
const pause$: Observable<void> = play$.pipe(
switchMap(() => interval(15000).pipe(
take(1)
)),
mapTo(void 0)
);
const player$ = player(source$, play$, pause$);
Does this solve your problem? If there are any issues with the solution let me know and I try to adapt.
I have an epic, that listens for a certain action.
Once it gets the action, it should do a ajax.post
Branch
If status code is good, then emit YES
If status bad, then emit pre, wait 1s, emit post
I am struggling mightily with the last bullet, here is my code in a playground - https://rxviz.com/v/WJxGMl4O
Here is my pipeline part:
action$.pipe(
flatMap(action =>
defer(() => ajax.post('foo foo foo')).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
retryWhen(error$ =>
error$.pipe(
tap(error => console.log('got error:', error)),
merge(of('pre')), // this isnt emiting
delay(1000),
merge(of('post')) // this isnt emitting
)
)
)
)
)
I think you can achieve what you want by using catchError instead of retryWhen because retryWhen only reacts to next notifications but won't propagate them further. With catchError you get also the source Observable which you can return and thus re-subscribe. concat subscribes to all its source one after another only after the previous one completed so it'll first send the two messages pre and post and after that retry.
action$.pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
catchError((error, source$) => {
console.log('retrying, got error:', error);
return staticConcat(
of('pre'),
of('post').pipe(delay(1000)),
source$,
);
}),
)
),
//take(4)
)
Your updated demo: https://rxviz.com/v/A8D7BzyJ
Here is my approach:
First, I created 2 custom operators, one that will handle 'pre' & 'post'(skipValidation) and one that will handle the logic(useValidation).
const skipValidation = src => of(src).pipe(
concatMap(
v => of('post').pipe(
startWith('pre'),
delay(1000),
),
),
);
What's important to notice in the snippet below is action$.next({ skip: true }). With that, we are emitting new values that will go through the iif operator so that we can emit 'pre' & 'post';
const useValidation = src => of(src).pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
delay(1000),
retryWhen(error$ =>
error$.pipe(
tap(error => { console.log('retrying, got error:', error); action$.next({ skip: true })}),
delay(1000),
)
)
)
)
);
action$.pipe(
tap(v => console.log('v', v)), // Every emitted value will go through the `iif ` operator
mergeMap(v => iif(() => typeof v === 'object' && v.skip, skipValidation(v), useValidation(v))),
)
Here is your updated demo.
In the code snippet below, I want to emit a GetData action if the update operation was successful or a BackendError action if unsuccessful.
#Effect()
updateData$ = this.actions$.pipe(
ofType(MyActionType.UPDATE_DATA),
map((action: UpdateData) => action.payload),
combineLatest(this.authService.getUser(), (myData, user) => this.dataService.updateData(myData, user)),
map(() => new GetData()),
catchError((err) => { of(new BackendError(err)))
);
The above code does not seem to be working. Even though the update operation fails due to permission error, the BackendError action is not emitted.
Any help is appreciated.
Your current implementation will swallow errors on the updateData operation, because the action$ (the outer observable) gets mapped to a new GetData() action, regardless of the result of the updateData operation (success or failure).
In the implementation below, the catchError operator will runif the updateData operation (dataUpdate$ -- the inner observable) throws an error..
#Effect()
updateData$ = this.actions$.pipe(
ofType(MyActionType.UPDATE_DATA),
mergeMap((action: UpdateData) => {
const user$ = this.authService.getUser();
const dataUpdate$ = user$.pipe(
mergeMap(user => this.dataService.updateData(action.payload, user));
);
return dataUpdate$.pipe(
map(() => new GetData()),
catchError(err => of(new BackendError(err)))
);
})
);
Additional Resources
Here's some more information on the difference between mergeMap
and map.
Official docs for mergeMap.
Example of #Effect using this approach in the #ngrx/effects README.
Consider the following code:
this.msgService.getUserChatList()
.do( (list) => {
this.unread = false;
console.log(list);
} )
.mergeMap( chats => Observable.from(chats) )
.mergeMap( chat => this.msgService.getLastMessage(chat['id']).map( lastMessage => this.containsUnreadMessages(lastMessage, chat['lastPresence']) ) )
.filter( state => state === true )
.subscribe( (unread) => {
this.unread = true;
console.log('result ', res);
} );
getUserChatList():
- emits an element each time one of the chat changes
- an element is a raw array containing all chats meta-data
- never completes
getLastMessage():
- is an Observable that never completes
In the second mergeMap I am calling the function getLastMessage().
I need to be observe this observable only until a new item is emitted by the getUserChatList() otherwise I would multiple observations on last message of the same chat.
Illustration :
getUserChatList emits : [chatMetaA:{}, chatMetaB:{}]
code go through getLastMessage and start to observe lastMessage of chatA and chatB
one of the chat change so a new item is emitted by getUserChatList containing the new version of the meta-data of the chats: [chatMetaA:{}, chatMetaB:{}]
code go through getLastMessage and start to observe lastMessage of chatA and chatB. So we now observe twice last message of chatA and chatB
And it will go on and on...
My question is, how could I cancel observation on getLastMessage() once a new item is emitted by getUserChatList()? I tried using switch but couldn't manage to make it work
Solution was indeed to use switchMap:
this.msgService.getUserChatList()
.do( () => { this.unread = false } )
.switchMap(
chats => Observable.from(chats)
.mergeMap( chat => this.msgService.getLastMessage(chat['id'])
.map( lastMessage => this.containsUnreadMessages(lastMessage, chat['lastPresence']) ) )
)
.filter( state => state === true )
.subscribe( (unread) => {
this.unread = true;
console.log('result ', res);
} );