Skip to content

Commit

Permalink
feat(Resolve): implement NOWAIT policy: Do not wait for resolves befo…
Browse files Browse the repository at this point in the history
…re completing a transition.

- The injector returns a promise, instead of the unwrapped value
  (`Transition.injector().get()` returns the same value as `Transition.injector().getAsync()`)

Closes #3243
Closes #2691
  • Loading branch information
christopherthielen committed Jan 7, 2017
1 parent 849f84f commit 05d4c73
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 32 deletions.
18 changes: 18 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,35 @@ export interface UIInjector {
/**
* Gets a value from the injector
*
* #### Example:
* ```js
* var myResolve = injector.get('myResolve');
* ```
*
* #### ng1 Example:
* ```js
* // Fetch $state service
* injector.get('$state').go('home');
* ```
*
* #### ng2 Example:
* ```js
* import {StateService} from "ui-router-ng2";
* // Fetch StateService
* injector.get(StateService).go('home');
* ```
*
* #### Typescript Example:
* ```js
* var stringArray = injector.get<string[]>('myStringArray');
* ```
*
* ---
*
* ### `NOWAIT` policy
*
* When using [[ResolvePolicy.async]] === `NOWAIT`, the value returned from `get()` is a promise for the result.
*
* @param token the key for the value to get. May be a string or arbitrary object.
* @return the Dependency Injection value that matches the token
*/
Expand Down
68 changes: 43 additions & 25 deletions src/resolve/resolveContext.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/** @module resolve */ /** for typedoc */
/** @module resolve */
/** for typedoc */
import { find, tail, uniqR, unnestR, inArray } from "../common/common";
import {propEq} from "../common/hof";
import {trace} from "../common/trace";
import {services, $InjectorLike} from "../common/coreservices";
import {resolvePolicies, PolicyWhen} from "./interface";

import {PathNode} from "../path/node";
import {Resolvable} from "./resolvable";
import {State} from "../state/stateObject";
import {PathFactory} from "../path/pathFactory";
import {stringify} from "../common/strings";
import {Transition} from "../transition/transition";
import {UIInjector} from "../interface";
import { propEq, not } from "../common/hof";
import { trace } from "../common/trace";
import { services, $InjectorLike } from "../common/coreservices";
import { resolvePolicies, PolicyWhen, ResolvePolicy } from "./interface";
import { PathNode } from "../path/node";
import { Resolvable } from "./resolvable";
import { State } from "../state/stateObject";
import { PathFactory } from "../path/pathFactory";
import { stringify } from "../common/strings";
import { Transition } from "../transition/transition";
import { UIInjector } from "../interface";

const when = resolvePolicies.when;
const ALL_WHENS = [when.EAGER, when.LAZY];
Expand Down Expand Up @@ -46,12 +46,18 @@ export class ResolveContext {
* Throws an error if it doesn't exist in the ResolveContext
*/
getResolvable(token: any): Resolvable {
var matching = this._path.map(node => node.resolvables)
let matching = this._path.map(node => node.resolvables)
.reduce(unnestR, [])
.filter((r: Resolvable) => r.token === token);
return tail(matching);
}

/** Returns the [[ResolvePolicy]] for the given [[Resolvable]] */
getPolicy(resolvable: Resolvable): ResolvePolicy {
let node = this.findNode(resolvable);
return resolvable.getPolicy(node.state);
}

/**
* Returns a ResolveContext that includes a portion of this one
*
Expand Down Expand Up @@ -95,8 +101,8 @@ export class ResolveContext {
* @param state Used to find the node to put the resolvable on
*/
addResolvables(newResolvables: Resolvable[], state: State) {
var node = <PathNode> find(this._path, propEq('state', state));
var keys = newResolvables.map(r => r.token);
let node = <PathNode> find(this._path, propEq('state', state));
let keys = newResolvables.map(r => r.token);
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.token) === -1).concat(newResolvables);
}

Expand All @@ -117,19 +123,27 @@ export class ResolveContext {
// get the subpath to the state argument, if provided
trace.traceResolvePath(this._path, when, trans);

const matchesPolicy = (acceptedVals: string[], whenOrAsync: "when"|"async") =>
(resolvable: Resolvable) =>
inArray(acceptedVals, this.getPolicy(resolvable)[whenOrAsync]);

// Trigger all the (matching) Resolvables in the path
// Reduce all the "WAIT" Resolvables into an array
let promises: Promise<any>[] = this._path.reduce((acc, node) => {
const matchesRequestedPolicy = (resolvable: Resolvable) =>
inArray(matchedWhens, resolvable.getPolicy(node.state).when);
let nodeResolvables = node.resolvables.filter(matchesRequestedPolicy);
let subContext = this.subContext(node.state);
let nodeResolvables = node.resolvables.filter(matchesPolicy(matchedWhens, 'when'));
let nowait = nodeResolvables.filter(matchesPolicy(['NOWAIT'], 'async'));
let wait = nodeResolvables.filter(not(matchesPolicy(['NOWAIT'], 'async')));

// For the matching Resolvables, start their async fetch process.
var getResult = (r: Resolvable) => r.get(subContext, trans)
let subContext = this.subContext(node.state);
let getResult = (r: Resolvable) => r.get(subContext, trans)
// Return a tuple that includes the Resolvable's token
.then(value => ({ token: r.token, value: value }));
return acc.concat(nodeResolvables.map(getResult));
nowait.forEach(getResult);
return acc.concat(wait.map(getResult));
}, []);

// Wait for all the "WAIT" resolvables
return services.$q.all(promises);
}

Expand Down Expand Up @@ -179,8 +193,12 @@ class UIInjectorImpl implements UIInjector {
}

get(token: any) {
var resolvable = this.context.getResolvable(token);
let resolvable = this.context.getResolvable(token);
if (resolvable) {
if (this.context.getPolicy(resolvable).async === 'NOWAIT') {
return resolvable.get(this.context);
}

if (!resolvable.resolved) {
throw new Error("Resolvable async .get() not complete:" + stringify(resolvable.token))
}
Expand All @@ -190,12 +208,12 @@ class UIInjectorImpl implements UIInjector {
}

getAsync(token: any) {
var resolvable = this.context.getResolvable(token);
let resolvable = this.context.getResolvable(token);
if (resolvable) return resolvable.get(this.context);
return services.$q.when(this.native.get(token));
}

getNative(token: any) {
return this.native.get(token);
return this.native && this.native.get(token);
}
}
84 changes: 77 additions & 7 deletions test/resolveSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import { UIRouter } from "../src/router";

import Spy = jasmine.Spy;
import { TestingPlugin } from "./_testingPlugin";
import { StateService } from "../src/state/stateService";
import { TransitionService } from "../src/transition/transitionService";
import { StateRegistry } from "../src/state/stateRegistry";
import { tail } from "../src/common/common";

///////////////////////////////////////////////

let router: UIRouter, states, statesMap: { [key:string]: State } = {};
let $state: StateService;
let $transitions: TransitionService;
let $registry: StateRegistry;
let vals, counts, expectCounts;
let asyncCount;

Expand Down Expand Up @@ -78,13 +85,16 @@ describe('Resolvables system:', function () {
beforeEach(function () {
router = new UIRouter();
router.plugin(TestingPlugin);
$state = router.stateService;
$transitions = router.transitionService;
$registry = router.stateRegistry;

counts = { _J: 0, _J2: 0, _K: 0, _L: 0, _M: 0, _Q: 0 };
vals = { _Q: null };
expectCounts = copy(counts);

tree2Array(getStates(), false).forEach(state => router.stateRegistry.register(state));
statesMap = router.stateRegistry.get()
tree2Array(getStates(), false).forEach(state => $registry.register(state));
statesMap = $registry.get()
.reduce((acc, state) => (acc[state.name] = state.$$state(), acc), statesMap);
});

Expand Down Expand Up @@ -347,8 +357,6 @@ describe('Resolvables system:', function () {

// Test for #2641
it("should not re-resolve data, when redirecting to a child", (done) => {
let $state = router.stateService;
let $transitions = router.transitionService;
$transitions.onStart({to: "J"}, ($transition$) => {
var ctx = new ResolveContext($transition$.treeChanges().to);
return invokeLater(function (_J) {}, ctx).then(function() {
Expand All @@ -366,9 +374,6 @@ describe('Resolvables system:', function () {

// Test for #2796
it("should not re-resolve data, when redirecting to self with dynamic parameter update", (done) => {
let $registry = router.stateRegistry;
let $state = router.stateService;
let $transitions = router.transitionService;
let resolveCount = 0;

$registry.register({
Expand Down Expand Up @@ -396,6 +401,71 @@ describe('Resolvables system:', function () {
done();
});
});

describe('NOWAIT Resolve Policy', () => {
it('should allow a transition to complete before the resolve is settled', async (done) => {
let resolve, resolvePromise = new Promise(_resolve => { resolve = _resolve; });

$registry.register({
name: 'nowait',
resolve: {
nowait: () => resolvePromise
},
resolvePolicy: { async: 'NOWAIT' }
});

$transitions.onSuccess({ }, trans => {
expect(trans.injector().get('nowait') instanceof Promise).toBeTruthy();
expect(trans.injector().getAsync('nowait') instanceof Promise).toBeTruthy();
expect(trans.injector().getAsync('nowait')).toBe(trans.injector().get('nowait'));

let resolvable = tail(trans.treeChanges('to')).resolvables[0];
expect(resolvable.token).toBe('nowait');
expect(resolvable.resolved).toBe(false);
expect(resolvable.data).toBeUndefined();

trans.injector().get('nowait').then(result => {
expect(result).toBe('foobar');
done();
});

resolve('foobar')
});

$state.go('nowait');
});

it('should wait for WAIT resolves and not wait for NOWAIT resolves', async (done) => {
let resolve, resolvePromise = new Promise(_resolve => { resolve = _resolve; });

$registry.register({
name: 'nowait',
resolve: [
{ token: 'nowait', policy: { async: 'NOWAIT' }, resolveFn: () => resolvePromise },
{ token: 'wait', policy: { async: 'WAIT' }, resolveFn: () => new Promise(resolve => resolve('should wait')) },
]
});

$transitions.onSuccess({ }, trans => {
expect(trans.injector().get('nowait') instanceof Promise).toBeTruthy();
expect(trans.injector().get('wait')).toBe('should wait');

let resolvable = tail(trans.treeChanges('to')).resolvables[0];
expect(resolvable.token).toBe('nowait');
expect(resolvable.resolved).toBe(false);
expect(resolvable.data).toBeUndefined();

trans.injector().get('nowait').then(result => {
expect(result).toBe('foobar');
done();
});

resolve('foobar')
});

$state.go('nowait');
});
});
});


0 comments on commit 05d4c73

Please sign in to comment.