RXJS Stop propagation of Observable chain if certain condition is met
Asked Answered
S

3

8

Introduction

I'm trying to create a route guard in Angular2+ using Observable from the shared service which holds the string value of current user role.
The problem is obviously in shifting my mind from Promises to Observables.

What I made so far is based on heuristics and try & error approach but I hit the wall by killing the browser Solved thanks to danday74

.

Attempt (improved thanks to @danday74)

With the help of RxJS sequence equvalent to promise.then()? i've translated what i want to do into this chain:

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
        return this.auth.isRoleAuthenticated(route.data.roles)
            .mergeMap((isRoleAuthenticated: boolean) => {
                return isRoleAuthenticated ? Observable.of(true) : this.auth.isRole(Roles.DEFAULT_USER);
            })
            .do((isDefaultUser: boolean) => {
                const redirectUrl: string = isDefaultUser ? 'SOMEWHERE' : 'SOMEWHERE_ELSE';
                this.router.navigate([redirectUrl]);
            })
            .map((isDefaultUser: boolean) => {
                return false;
            });
    }

Question

How to stop further propagation of observable chain if isRoleAuthenticated = true? I need to return that boolean value if such condition is met and make sure .do operator block is not called after.
Limitation is that boolean value must be returned from canActivate guard.

Scorcher answered 16/10, 2018 at 23:16 Comment(1)
Browser was actually just loading forever not crashing, sorry for bad expression. Tried with setTimeout() and take(1), still i can't debug and browser loads forever until the page becomes unresponsive.Scorcher
T
2

Whatever you return from the first mergeMap is passed to your second mergeMap so it won't stop further propagation. If you want to stop propagation use filter (although that may cause a hang in this scenario).

You only use mergeMap when you are returning an Observable but there is no need to return an Observable from the 2nd mergeMap. Just do:

  .mergeMap((isRoleAuthenticated: boolean) => {
    if (isRoleAuthenticated) {
      return Observable.of(true)
    }
    return this.auth.isRole(Roles.DEFAULT_USER)
  })
  .tap((isDefaultUser: boolean) => {
    if (isDefaultUser) {
      this.router.navigate(['SOMEWHERE'])
    } else {
      this.router.navigate(['SOMEWHERE_ELSE'])
    }
  })
  .map((isDefaultUser: boolean) => {
    return false
  })

Also, you are using RxJs v5 syntax but should be using v6. In v6 the operators - mergeMap, tap, map - are comma separated in a pipe.

Possibly the router navigation is preventing the final return and causing the hang? Comment that out and see if it stops the hang.

Not sure this will fully fix your prob but hopefully some useful insights for 1am

I am assuming that these return Observables:

  • this.auth.isRoleAuthenticated
  • this.auth.isRole(Roles.DEFAULT_USER)

If they don't you will get problems.

Solution

Instead of focusing on stopping the chain you can create an object consisted of results of collected Observables along the way and propagate it further which solves the problem:

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
    return this.auth.getRole()
        .mergeMap((role: string) => {
            return this.auth.isRoleAuthorized(route.data.roles)
                .map((authorized: boolean) => ({
                    role: role,
                    authorized: authorized
                }));
        })
        .do((markers: { role: string, authorized: boolean }) => {
            const redirectUrl: string = markers.role === Roles.DEFAULT_USER ? 'SOMEWHERE' : 'SOMEWHERE_ELSE';
            this.router.navigate([redirectUrl]);
        })
        .map((markers: { role: string, authorized: boolean }) => {
            return markers.authorized;
        });
}
Tumefy answered 16/10, 2018 at 23:50 Comment(3)
Thanks it's impossible to debug at the moment, hope your insights will give me at least a step closer to revive chrome i'm using RxJs V5 and Angular 5. Yes, auth methods are returning Observables correctly, i'll update the question with more details about it just to try changing according to your advises. Yeah i've already spotted i don't need the second mergeMap as no there are no inner Observables there.Scorcher
worth knowing, use .... .tap(x => console.log(x)) at every stage of the chain so you can see what values are being passed down - good for debugging - all the best :)Tumefy
also worth knowing, in Chrome, make sure the "Preserve log" checkbox is checked so that it does not clear the log on navigation else you'll lose all your useful debug info - apologies if I'm teaching you to suck eggsTumefy
M
3

The do() operator works only with next notifications so if you don't want to process something that comes after .mergeMap you can filter it out with filter():

return this.auth.isRoleAuthenticated(route.data.roles)
  .mergeMap((isRoleAuthenticated: boolean) => {
    return isRoleAuthenticated ? Observable.of(true) : this.auth.isRole(Roles.DEFAULT_USER);
  })
  .filter(authenticated => authenticated !== true)

However, it looks like you could just return Observable.empty() instead of Observable.of(true) because that will only emit the complete notification and no next items so there will be nothing to be passed to do():

.mergeMap((isRoleAuthenticated: boolean) => {
  return isRoleAuthenticated ? Observable.empty() : this.auth.isRole(Roles.DEFAULT_USER);
})
Marnimarnia answered 17/10, 2018 at 12:33 Comment(3)
Thanks @martin, i thought about using Observable.empty() but the problem is i must return boolean true value to the canActivate guard as an output of the chain in that specific case. It seems that i can't do it without propagating anything furtherScorcher
I see. Well, then Observable.empty() is really not an option.Marnimarnia
ATM i'm trying to propagate a new merged object and then do the predication in further steps. I'll post that when i finish and then we can improve it.Scorcher
T
2

Whatever you return from the first mergeMap is passed to your second mergeMap so it won't stop further propagation. If you want to stop propagation use filter (although that may cause a hang in this scenario).

You only use mergeMap when you are returning an Observable but there is no need to return an Observable from the 2nd mergeMap. Just do:

  .mergeMap((isRoleAuthenticated: boolean) => {
    if (isRoleAuthenticated) {
      return Observable.of(true)
    }
    return this.auth.isRole(Roles.DEFAULT_USER)
  })
  .tap((isDefaultUser: boolean) => {
    if (isDefaultUser) {
      this.router.navigate(['SOMEWHERE'])
    } else {
      this.router.navigate(['SOMEWHERE_ELSE'])
    }
  })
  .map((isDefaultUser: boolean) => {
    return false
  })

Also, you are using RxJs v5 syntax but should be using v6. In v6 the operators - mergeMap, tap, map - are comma separated in a pipe.

Possibly the router navigation is preventing the final return and causing the hang? Comment that out and see if it stops the hang.

Not sure this will fully fix your prob but hopefully some useful insights for 1am

I am assuming that these return Observables:

  • this.auth.isRoleAuthenticated
  • this.auth.isRole(Roles.DEFAULT_USER)

If they don't you will get problems.

Solution

Instead of focusing on stopping the chain you can create an object consisted of results of collected Observables along the way and propagate it further which solves the problem:

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
    return this.auth.getRole()
        .mergeMap((role: string) => {
            return this.auth.isRoleAuthorized(route.data.roles)
                .map((authorized: boolean) => ({
                    role: role,
                    authorized: authorized
                }));
        })
        .do((markers: { role: string, authorized: boolean }) => {
            const redirectUrl: string = markers.role === Roles.DEFAULT_USER ? 'SOMEWHERE' : 'SOMEWHERE_ELSE';
            this.router.navigate([redirectUrl]);
        })
        .map((markers: { role: string, authorized: boolean }) => {
            return markers.authorized;
        });
}
Tumefy answered 16/10, 2018 at 23:50 Comment(3)
Thanks it's impossible to debug at the moment, hope your insights will give me at least a step closer to revive chrome i'm using RxJs V5 and Angular 5. Yes, auth methods are returning Observables correctly, i'll update the question with more details about it just to try changing according to your advises. Yeah i've already spotted i don't need the second mergeMap as no there are no inner Observables there.Scorcher
worth knowing, use .... .tap(x => console.log(x)) at every stage of the chain so you can see what values are being passed down - good for debugging - all the best :)Tumefy
also worth knowing, in Chrome, make sure the "Preserve log" checkbox is checked so that it does not clear the log on navigation else you'll lose all your useful debug info - apologies if I'm teaching you to suck eggsTumefy
G
1

Instead of returning Observable.of(true), you can return Observable.empty(), which will just complete, without emitting any values. Thus following chains won't be executed.

Graner answered 17/10, 2018 at 17:2 Comment(1)
Unfortunately i need to return boolean true in this specific case because canActivate needs it otherwise that would be the solutionScorcher

© 2022 - 2024 — McMap. All rights reserved.