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.

maljukan
  • 2,148
  • 2
  • 25
  • 35
  • 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. – maljukan Oct 17 '18 at 00:39

3 Answers3

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);
})
martin
  • 93,354
  • 25
  • 191
  • 226
  • 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 further – maljukan Oct 17 '18 at 12:47
  • I see. Well, then `Observable.empty()` is really not an option. – martin Oct 17 '18 at 12:49
  • 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. – maljukan Oct 17 '18 at 12:52
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;
        });
}
Community
  • 1
  • 1
danday74
  • 52,471
  • 49
  • 232
  • 283
  • 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. – maljukan Oct 17 '18 at 00:02
  • 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 :) – danday74 Oct 17 '18 at 00:06
  • 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 eggs – danday74 Oct 17 '18 at 00:10
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.

Xinan
  • 3,002
  • 1
  • 16
  • 18
  • Unfortunately i need to return `boolean true` in this specific case because `canActivate` needs it otherwise that would be the solution – maljukan Oct 17 '18 at 17:54