Skip to content

Instantly share code, notes, and snippets.

@zzpmaster
Created September 13, 2021 09:27
Show Gist options
  • Save zzpmaster/f156eb80e5d1d9309a41ca37b049eccd to your computer and use it in GitHub Desktop.
Save zzpmaster/f156eb80e5d1d9309a41ca37b049eccd to your computer and use it in GitHub Desktop.
Angular multiple guards async.
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { tap, delay } from 'rxjs/operators';
import { of } from 'rxjs';
import { SequentialRoutingGuardService } from './sequential-routing-guard.service';
@Injectable({
providedIn: 'root'
})
export class Guard1Service implements CanActivate {
constructor(private sequentialRoutingGuardService: SequentialRoutingGuardService) { }
canActivate(state) {
console.log('Guard1 canActivate()');
return this.sequentialRoutingGuardService.queue(
state,
of(true).pipe(delay(500)).pipe(
tap(() => console.log('Guard1 emits false via observable'))
)
);
}
}
import {BehaviorSubject, concat, EMPTY, Observable, race} from 'rxjs';
import {switchMap, take, tap} from 'rxjs/operators';
/**
* any observable wrapped with queue() on one instance of ObservableQueue will never run
* at the same time and will run in order of invocation.
*
* E.g.
*
* const observableQueue = new ObservableQueue();
*
* of('first').pipe(flatMap((value) => observableQueue.queue(delay(1000), mapTo(value))))
* .subscribe(console.log)
* of('second').pipe(flatMap((value) => observableQueue.queue(delay(10), mapTo(value))))
* .subscribe(console.log)
*
* // will log:
* // 'first' // after 1000 ms
* // 'second' // after 1010 ms
*/
export class ObservableQueue<VALUE> {
private active = false;
private pipeline: (() => void)[] = [];
private stopped$ = new BehaviorSubject<{replacement$?: Observable<VALUE>} | undefined>(undefined);
private queue$ = new Observable<never>(subscriber => {
const next = () => {
this.active = true;
subscriber.complete();
};
if (!this.active) {
next();
return;
}
this.pipeline.push(next);
return () => {
const index = this.pipeline.indexOf(next);
if (index >= 0) {
this.pipeline.splice(index, 1);
}
};
});
constructor(private config?: {queueMiddleware?(currentValue: VALUE, queue: ObservableQueue<VALUE>): void}) {}
private dequeue$ = new Observable<never>(() => {
return () => {
this.active = false;
this.pipeline.shift()?.();
};
});
/**
* stops the queue
*
* @param replacement$ - by default all remaining streams in the queue will just unsubscribe.
* With replacement you can switch to another stream instead (like emitting a value before completing)
*/
public stop(replacement$?: Observable<VALUE>): void {
this.stopped$.next({replacement$});
}
public queue<T>(source$: Observable<T>): Observable<T>;
public queue(source$: Observable<VALUE>): Observable<VALUE> {
const queueMiddleware = this.config?.queueMiddleware;
if (queueMiddleware) {
source$ = source$.pipe(tap(value => queueMiddleware(value, this)));
}
/**
* concat(queue$, ...)
* concat only proceeds if queue$ completes. This is used to achieve the queue behavior
*
* race(dequeue$, ...)
* race will subscribe to both, and call the unsubscribe of dequeue$ when source$ is done.
* This is used to start the next task on the queue
*/
return concat(
this.queue$,
this.stopped$.pipe(
take(1),
switchMap(stopped => race(this.dequeue$, stopped ? stopped.replacement$ || EMPTY : source$)),
),
);
}
public getState(): {active: boolean; queued: number} {
return {active: this.active, queued: this.pipeline.length};
}
}
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot} from '@angular/router';
import {Observable, of} from 'rxjs';
import { ObservableQueue } from './observable-queue';
@Injectable({
providedIn: 'root',
})
export class SequentialRoutingGuardService {
private queued = new WeakMap<ActivatedRouteSnapshot, ObservableQueue<boolean>>();
private cannotActivate$ = of(false);
/**
* angular routing guard are all executed, regarding of result. So if you attach 3 routing guards, and two of them deny and do a redirect,
* you get two redirects
*
* With this method you can queue them up and the queue will automatically break after one of the guards returned false
* Pipe all your canActivate(state) observables through this method and succeeding guards won't execute if one returns `false`
*/
public queue(snapshot: ActivatedRouteSnapshot | undefined, canActivate$: Observable<boolean>): Observable<boolean> {
if (!snapshot) {
return canActivate$;
}
let queue = this.queued.get(snapshot);
if (!queue) {
queue = new ObservableQueue<boolean>({
queueMiddleware: (currentValue, currentQueue) => {
if (!currentValue) {
// the last guard in the queue returned `false` for canActivate. So we stop the queue and prevent them from doing anything
// we have to provide a result anyway for every guard, otherwise the angular router will throw an error
// so return `of(false)` for all remaining guards
currentQueue.stop(this.cannotActivate$);
}
},
});
this.queued.set(snapshot, queue);
}
return queue.queue(canActivate$);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment