Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RXJS Stop propagation of Observable chain if certain condition is met

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.

like image 362
maljukan Avatar asked Oct 16 '18 23:10

maljukan


2 Answers

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);
})
like image 126
martin Avatar answered Sep 19 '22 02:09

martin


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;
        });
}
like image 23
danday74 Avatar answered Sep 18 '22 02:09

danday74