forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(router): add prioritizedGuardValue operator optimization and all…
…owing UrlTree return from guard (angular#26478) * If all guards return `true`, operator returns `true` * `false` and `UrlTree` are now both valid returns from a guard * Both these values wait for higher priority guards to resolve * Highest priority `false` or `UrlTree` value will be returned PR Close angular#26478
- Loading branch information
Showing
3 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Observable, OperatorFunction, combineLatest} from 'rxjs'; | ||
import {filter, scan, startWith, switchMap, take} from 'rxjs/operators'; | ||
|
||
import {UrlTree} from '../url_tree'; | ||
|
||
const INITIAL_VALUE = Symbol('INITIAL_VALUE'); | ||
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree; | ||
|
||
export function prioritizedGuardValue(): | ||
OperatorFunction<Observable<boolean|UrlTree>[], boolean|UrlTree> { | ||
return switchMap(obs => { | ||
return combineLatest( | ||
...obs.map(o => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES)))) | ||
.pipe( | ||
scan( | ||
(acc: INTERIM_VALUES, list: INTERIM_VALUES[]) => { | ||
let isPending = false; | ||
return list.reduce((innerAcc, val, i: number) => { | ||
if (innerAcc !== INITIAL_VALUE) return innerAcc; | ||
|
||
// Toggle pending flag if any values haven't been set yet | ||
if (val === INITIAL_VALUE) isPending = true; | ||
|
||
// Any other return values are only valid if we haven't yet hit a pending call. | ||
// This guarantees that in the case of a guard at the bottom of the tree that | ||
// returns a redirect, we will wait for the higher priority guard at the top to | ||
// finish before performing the redirect. | ||
if (!isPending) { | ||
// Early return when we hit a `false` value as that should always cancel | ||
// navigation | ||
if (val === false) return val; | ||
|
||
if (i === list.length - 1 || val instanceof UrlTree) { | ||
return val; | ||
} | ||
} | ||
|
||
return innerAcc; | ||
}, acc); | ||
}, | ||
INITIAL_VALUE), | ||
filter(item => item !== INITIAL_VALUE), take(1)) as Observable<boolean|UrlTree>; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ ts_library( | |
"//packages/router/testing", | ||
"@rxjs", | ||
"@rxjs//operators", | ||
"@rxjs//testing", | ||
], | ||
) | ||
|
||
|
182 changes: 182 additions & 0 deletions
182
packages/router/test/operators/prioritized_guard_value.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
|
||
import {TestBed} from '@angular/core/testing'; | ||
import {Observable, Observer, of } from 'rxjs'; | ||
import {every, mergeMap} from 'rxjs/operators'; | ||
import {TestScheduler} from 'rxjs/testing'; | ||
|
||
import {prioritizedGuardValue} from '../../src/operators/prioritized_guard_value'; | ||
import {Router} from '../../src/router'; | ||
import {UrlTree} from '../../src/url_tree'; | ||
import {RouterTestingModule} from '../../testing/src/router_testing_module'; | ||
|
||
|
||
describe('prioritizedGuardValue operator', () => { | ||
let testScheduler: TestScheduler; | ||
let router: Router; | ||
const TF = {T: true, F: false}; | ||
|
||
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); }); | ||
beforeEach(() => { testScheduler = new TestScheduler(assertDeepEquals); }); | ||
beforeEach(() => { router = TestBed.get(Router); }); | ||
|
||
it('should return true if all values are true', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ----------(T|)', TF); | ||
const c = cold(' ------(T|)', TF); | ||
const source = hot('---o--', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------T--'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, TF, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should return false if observables to the left of false have produced a value', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ----------(T|)', TF); | ||
const c = cold(' ------(F|)', TF); | ||
const source = hot('---o--', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------F--'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, TF, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should ignore results for unresolved sets of Observables', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' -------------(T|)', TF); | ||
const c = cold(' ------(F|)', TF); | ||
|
||
const z = cold(' ----(T|)', TF); | ||
|
||
const source = hot('---o----p----', {o: [a, b, c], p: [z]}); | ||
|
||
const expected = ' ------------T---'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, TF, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should return UrlTree if higher priority guards have resolved', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const urlTree = router.parseUrl('/'); | ||
|
||
const urlLookup = {U: urlTree}; | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ----------(U|)', urlLookup); | ||
const c = cold(' ------(T|)', TF); | ||
|
||
const source = hot('---o---', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------U---'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, urlLookup, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should return false even with UrlTree if UrlTree is lower priority', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const urlTree = router.parseUrl('/'); | ||
|
||
const urlLookup = {U: urlTree}; | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ----------(F|)', TF); | ||
const c = cold(' ------(U|)', urlLookup); | ||
|
||
const source = hot('---o---', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------F---'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, TF, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should return UrlTree even after a false if the false is lower priority', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const urlTree = router.parseUrl('/'); | ||
|
||
const urlLookup = {U: urlTree}; | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ----------(U|)', urlLookup); | ||
const c = cold(' ------(F|)', TF); | ||
|
||
const source = hot('---o---', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------U----'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, urlLookup, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should return the highest priority UrlTree', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const urlTreeU = router.parseUrl('/u'); | ||
const urlTreeR = router.parseUrl('/r'); | ||
const urlTreeL = router.parseUrl('/l'); | ||
|
||
const urlLookup = {U: urlTreeU, R: urlTreeR, L: urlTreeL}; | ||
|
||
const a = cold(' ----------(U|)', urlLookup); | ||
const b = cold(' -----(R|)', urlLookup); | ||
const c = cold(' --(L|)', urlLookup); | ||
|
||
const source = hot('---o---', {o: [a, b, c]}); | ||
|
||
const expected = ' -------------U---'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, urlLookup, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
it('should propagate errors', () => { | ||
testScheduler.run(({hot, cold, expectObservable}) => { | ||
|
||
const a = cold(' --(T|)', TF); | ||
const b = cold(' ------#', TF); | ||
const c = cold(' ----------(F|)', TF); | ||
const source = hot('---o------', {o: [a, b, c]}); | ||
|
||
const expected = ' ---------#'; | ||
|
||
expectObservable(source.pipe(prioritizedGuardValue())) | ||
.toBe(expected, TF, /* an error here maybe */); | ||
}); | ||
}); | ||
|
||
|
||
}); | ||
|
||
|
||
|
||
function assertDeepEquals(a: any, b: any) { | ||
return expect(a).toEqual(b); | ||
} |