Skip to content

Commit

Permalink
feat(router): add prioritizedGuardValue operator optimization and all…
Browse files Browse the repository at this point in the history
…owing UrlTree return from guard (#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 #26478
  • Loading branch information
jasonaden authored and mhevery committed Oct 18, 2018
1 parent 0ae3518 commit d5fa4dc
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 0 deletions.
52 changes: 52 additions & 0 deletions packages/router/src/operators/prioritized_guard_value.ts
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>;
});
}
1 change: 1 addition & 0 deletions packages/router/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ts_library(
"//packages/router/testing",
"@rxjs",
"@rxjs//operators",
"@rxjs//testing",
],
)

Expand Down
182 changes: 182 additions & 0 deletions packages/router/test/operators/prioritized_guard_value.spec.ts
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);
}

1 comment on commit d5fa4dc

@IgorMinar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this commit is not supposed to be on the patch branch

Please sign in to comment.