How canActivate works for multiple guards

Angular 2 lets developers forbid routes activation by means of a list of guards assigned to the canActivate property of a route definition.

{ 
  path: 'vip-lounge', 
  component: 'VipLoungeComponent', 
  canActivate: [ IfUserIsLoggedIn, 
    AndSpendsEnough,
    AndMembershipIsCurrent ]
}

Those guards are classes which implement the CanActivate interface, and the implementation can be a promise, for example when it entails a roundtrip to the server.

The way Angular 2 treats this list of guards is by applying .every(...) to the values of a merge-mapped list of observables.

  checkGuards(): Observable<boolean> {
    if (this.checks.length === 0) return of (true);
    const checks$ = from(this.checks);
    const runningChecks$ = mergeMap.call(checks$, (s: any) => {
      if (s instanceof CanActivate) {
        return andObservables(
            from([this.runCanActivateChild(s.path), this.runCanActivate(s.route)]));
      } else if (s instanceof CanDeactivate) {
        // workaround https://github.com/Microsoft/TypeScript/issues/7271
        const s2 = s as CanDeactivate;
        return this.runCanDeactivate(s2.component, s2.route);
      } else {
        throw new Error('Cannot be reached');
      }
    });
    return every.call(runningChecks$, (result: any) => result === true);
  }

The list of guards is conceptually the same as a list of promises, and in fact all the guards could be promises because, as we see above, .from(...) is used to initialise check$.

Merge-mapping is exactly what we did in How to convert promises to an observable.

var source = Rx.Observable.from(promises.map(function (promise) { 
  return Rx.Observable.from(promise); 
})).mergeAll(); 
/* 
 
true 
"Next: true" 
true 
"Next: true" 
true 
"Next: true" 
false 
"Next: false" 
true 
"Next: true" 
"Completed" 
 
*/

var source = Rx.Observable.from(promises).mergeMap(function (promise) {
  return Rx.Observable.from(promise);
});
/* 
 
true 
"Next: true" 
true 
"Next: true" 
true 
"Next: true" 
false 
"Next: false" 
true 
"Next: true" 
"Completed" 
 
*/

If we now apply .every(...) to that merge-mapped list of promises, we can see how canActivate works for multiple guards.

var source = Rx.Observable
  .from(promises)
  .mergeMap(promise => Rx.Observable.from(promise))
  .every(value => value === true);
/*

true
true
true
false
"Next: false"
"Completed"
true

*/

Notice that the subscription is notified only once, as soon as the resolved promises make it clear what the result would be: the observable emits a false soon after getting the first false, or a true after getting all true-s. So, as expected, canActivate works as a short-circuit-ed AND of each guard of the list, according to the arrival order of values.

Also notice that the values of all the promises keep arriving as promises get resolved, even after the observable has completed. This means that guards intended to be used together in a list of guards should never have side effects (to avoid race conditions). If guard G1 had a side effect of activating route R1, and G2 had another for R2, then canActivate: [G1, G2] would unpredictably activate R1 or R2, depending on which guard completes later.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.