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.
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.
I am using NGRX and Angular 11.
I am trying to, when an action is executed, call another action, listen to the success of it, and if it succeeded dispatch a final action:
Here my code a bit simplified :
#Effect()
getUserSettingSuccess$ = this.actions$.pipe(
// When this action `GetUserSettingsSuccess` is executed
ofType<featureActions.GetUserSettingsSuccess>(featureActions.ActionTypes.GetUserSettingsSuccess),
concatMap((action) =>
of(action).pipe(withLatestFrom(this.store$.pipe(select(ProjectsStoreSelectors.selectedProjectId))))
),
// I want to dispatch a new action
tap(([action, projectId]) => new ModelsStoreAction.GetAllModelsRequest({ projectId })),
// and listen to the success / failure of that action.
// If success dispatch `SetModelSelection` else do nothing.
map(([action]) =>
this.actions$.pipe(
ofType(ModelsStoreAction.ActionTypes.GetAllModelsSuccess),
takeUntil(this.actions$.pipe(ofType(ModelsStoreAction.ActionTypes.GetAllCesiumModelsFailed))),
first(),
map(() => new ModelsStoreAction.SetModelSelection())
)
)
The problem I have is that, the above code do not dispatch a valid action. But I am getting a bit lost with all those rxjs operator.
Here would be my approach:
#Effect()
getUserSettingSuccess$ = this.actions$.pipe(
// When this action `GetUserSettingsSuccess` is executed
ofType<featureActions.GetUserSettingsSuccess>(featureActions.ActionTypes.GetUserSettingsSuccess),
concatMap((action) =>
of(action).pipe(withLatestFrom(this.store$.pipe(select(ProjectsStoreSelectors.selectedProjectId))))
),
// I want to dispatch a new action
tap(([action, projectId]) => new ModelsStoreAction.GetAllModelsRequest({ projectId })),
// and listen to the success / failure of that action.
// If success dispatch `SetModelSelection` else do nothing.
switchMap(
([action]) => this.actions$.pipe(
// adding all the possibilities here
ofType(ModelsStoreAction.ActionTypes.GetAllModelsSuccess, ModelsStoreAction.ActionTypes.GetAllCesiumModelsFailed),
first(),
filter(a => a.type === ModelsStoreAction.ActionTypes.GetAllModelsSuccess.type),
map(() => new ModelsStoreAction.SetModelSelection()),
)
)
)
It didn't work before because this.actions$ is essentially a type of Subject and an effect is not expected to return an Observable-like value. In order to solve that, I used switchMap, which will handle the inner subscription automatically.
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.
I need to invoke multiple web request in declared order. Every consecutive request depends on the outcome of previous one. I would like to emit event after each request is completed that will go to the subscriber.
Right now I am "tapping" value of each request and emitting result using separate Subject. Is it possible to do this using single pipe with operators?
Here is code example
fromEvent(pauseButton, "click")
.pipe(
tap(()=>{
subscribedLabel.innerHTML="";
tapedLabel.innerHTML="";
}),
tap(v => (tapedLabel.innerHTML = "started")),
concatMapTo(of("phase 1 completed").pipe(delay(1000))),
tap(v => (tapedLabel.innerHTML = v)),
concatMapTo(of("phase 2 completed").pipe(delay(1000))),
tap(v => (tapedLabel.innerHTML = v))
)
.subscribe(v => {
console.log(v);
subscribedLabel.innerHTML = v;
});
https://stackblitz.com/edit/typescript-6jza7h?file=index.ts
The expected outcome is that subscribedLabel.innerHTML will change the same way as tapedLabel.innerHTML
It not clear what you're after, but this is a way you can use 4 consecutive calls and accumulate all their responses into one object.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
concatMap(_ =>
fakeHTTP(1).pipe(
map(res => ({first: res}))
)
),
tap(_ => console.log("First Request Complete")),
concatMap(first =>
fakeHTTP(2).pipe(
map(res => ({...first, second: res}))
)
),
tap(_ => console.log("Second Request Complete")),
concatMap(second =>
fakeHTTP(3).pipe(
map(res => ({...second, third: res}))
)
),
tap(_ => console.log("Third Request Complete")),
concatMap(third =>
fakeHTTP(4).pipe(
map(res => ({...third, fourth: res}))
)
),
tap(_ => console.log("Fourth Request Complete"))
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
First Request Complete
// Wait 1s
Second Request Complete
// Wait 1s
Third Request Complete
// Wait 1s
Fourth Request Complete
{"first":1,"second":2,"third":3,"fourth":4} // <- Value sent to subscribe
Update #1: Pass Values Up the Call Chain
You can pass values up the call chain, but it gets a bit more complicated. You want each step only to work on values from the previous step, but to ignore (emit unaltered) the values from further up the chain.
One way you can do this is to tag each response. I do this with a pass flag that can be true or false. The final operation is to remove the flag.
Here is what that looks like:
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
concatMap(_ =>
fakeHTTP(1)
),
tap(_ => console.log("First Request Complete")),
concatMap(first =>
fakeHTTP(2).pipe(
map(res => ({pass: false, payload: res})),
startWith({pass: true, payload: first})
)
),
tap(({pass}) => {
if(!pass) console.log("Second Request Complete")
}),
concatMap(second => second.pass ?
of(second) :
fakeHTTP(3).pipe(
map(res => ({pass: false, payload: res})),
startWith({...second, pass: true})
)
),
tap(({pass}) => {
if(!pass) console.log("Third Request Complete")
}),
concatMap(third => third.pass ?
of(third) :
fakeHTTP(4).pipe(
map(res => ({pass: false, payload: res})),
startWith({...third, pass: true})
)
),
tap(({pass}) => {
if(!pass) console.log("Second Request Complete")
}),
map(({payload}) => payload)
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
First Request Complete // <- console log from tap
1 // <- console log from subscribe
// Wait 1s
Second Request Complete // <- console log from tap
2 // <- console log from subscribe
// Wait 1s
Third Request Complete // <- console log from tap
3 // <- console log from subscribe
// Wait 1s
Second Request Complete // <- console log from tap
4 // <- console log from subscribe
Update #2: When recursion is possible
You can also make recursive calls where each new call depends on the previous call and some base-case ends the recursion. RxJS jas expand as a built-in way to recurse.
In this example, each new call to fakeHTTP uses the value emitted by the previous call directly.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
map(_ => 1),
expand(proj => proj < 4 ?
fakeHTTP(++proj) :
EMPTY
)
).subscribe(console.log);
The output of this is as follows:
// Wait 1s
1
// Wait 1s
2
// Wait 1s
3
// Wait 1s
4
Update #3: Separate observables
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
const first$ = fromEvent(button, "click").pipe(
concatMap(_ => fakeHTTP(1)),
share()
);
const second$ = first$.pipe(
concatMap(first => fakeHTTP(2)),
share()
);
const third$ = second$.pipe(
concatMap(second => fakeHTTP(3)),
share()
);
const fourth$ = third$.pipe(
concatMap(third => fakeHTTP(4))
);
merge(
first$,
second$,
third$,
fourth$
).subscribe(console.log);
Here's another, more annoying way to write almost the exact same thing.
function fakeHTTP(resW): Observable<string> {
return of(resW).pipe(delay(1000))
}
fromEvent(button, "click").pipe(
map(_ => fakeHTTP(1).pipe(
share(),
)),
map(first$ => ([first$.pipe(
concatMap(firstR => fakeHTTP(2)),
share()
), first$])),
map(([second$, ...tail]) => ([second$.pipe(
concatMap(secondR => fakeHTTP(3)),
share()
),second$, ...tail])),
map(([third$, ...tail]) => ([third$.pipe(
concatMap(thirdR => fakeHTTP(4))
),third$, ...tail])),
concatMap(calls => merge(...calls))
).subscribe(console.log);
I am working on this sandbox here - https://stackblitz.com/edit/rxjs-g7msgv?file=index.ts
What I am trying to do is:
1) Wait for onLogin event
2) While logged in, I want to connectSocket(), and whenever the socket gets disconnected, and the app is in the foreground, I want to re-connectSocket(). (in the sandbox I have stubbed out connectSocket() to a promise that just waits 5 sec)
3) I want to repeat step 2, until onLogout event comes in
I wrote this code here, please see the sandbox and start things off by pressing the "onLogin" button.
fromEvent(document, 'onLogin')
.pipe(
switchMap(() =>
of({ isDisconnected: true, isInForeground: true }).pipe(
mergeMap(data =>
concat(
fromEvent(document, 'onDisconnect').pipe(
mergeMap(() =>
data.isDisconnected = true
)
),
fromEvent(document, 'onAppStateChange').pipe(
mergeMap(({ detail:{ state } }) =>
data.isInForeground = state === 'foreground'
)
),
).pipe(
mergeMap(({ isDisconnected, isInForeground }) => {
if (isDisconnected && isInForeground) {
return flatMap(() => connectSocket());
} else {
return EMPTY;
}
})
)
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)
.subscribe(console.log);
I use switchMap because while its running, I don't want any other login events to restart another flow.
I'm not able to get this working. I am new to rxjs.
Use startWith to Init the value and combineLatest will fire when either one of the event is triggered.
fromEvent(document, 'onLogin').pipe(
switchMap(() =>
combineLatest(
fromEvent(document, 'onDisconnect').pipe(
mapTo(true),
startWith(true)
),
fromEvent(document, 'onAppStateChange').pipe(
map(e => e.detail === 'foreground'),
startWith(true),
)
).pipe(
mergeMap(([isDisconnected, isInForeground]) =>
isDisconnected && isInForeground ? connectSocket() : EMPTY
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)