Skip to content

Commit 05d4c73

Browse files
feat(Resolve): implement NOWAIT policy: Do not wait for resolves before 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
1 parent 849f84f commit 05d4c73

File tree

3 files changed

+138
-32
lines changed

3 files changed

+138
-32
lines changed

src/interface.ts

+18
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,35 @@ export interface UIInjector {
2727
/**
2828
* Gets a value from the injector
2929
*
30+
* #### Example:
31+
* ```js
32+
* var myResolve = injector.get('myResolve');
33+
* ```
34+
*
3035
* #### ng1 Example:
3136
* ```js
37+
* // Fetch $state service
3238
* injector.get('$state').go('home');
3339
* ```
3440
*
3541
* #### ng2 Example:
3642
* ```js
3743
* import {StateService} from "ui-router-ng2";
44+
* // Fetch StateService
3845
* injector.get(StateService).go('home');
3946
* ```
4047
*
48+
* #### Typescript Example:
49+
* ```js
50+
* var stringArray = injector.get<string[]>('myStringArray');
51+
* ```
52+
*
53+
* ---
54+
*
55+
* ### `NOWAIT` policy
56+
*
57+
* When using [[ResolvePolicy.async]] === `NOWAIT`, the value returned from `get()` is a promise for the result.
58+
*
4159
* @param token the key for the value to get. May be a string or arbitrary object.
4260
* @return the Dependency Injection value that matches the token
4361
*/

src/resolve/resolveContext.ts

+43-25
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
/** @module resolve */ /** for typedoc */
1+
/** @module resolve */
2+
/** for typedoc */
23
import { find, tail, uniqR, unnestR, inArray } from "../common/common";
3-
import {propEq} from "../common/hof";
4-
import {trace} from "../common/trace";
5-
import {services, $InjectorLike} from "../common/coreservices";
6-
import {resolvePolicies, PolicyWhen} from "./interface";
7-
8-
import {PathNode} from "../path/node";
9-
import {Resolvable} from "./resolvable";
10-
import {State} from "../state/stateObject";
11-
import {PathFactory} from "../path/pathFactory";
12-
import {stringify} from "../common/strings";
13-
import {Transition} from "../transition/transition";
14-
import {UIInjector} from "../interface";
4+
import { propEq, not } from "../common/hof";
5+
import { trace } from "../common/trace";
6+
import { services, $InjectorLike } from "../common/coreservices";
7+
import { resolvePolicies, PolicyWhen, ResolvePolicy } from "./interface";
8+
import { PathNode } from "../path/node";
9+
import { Resolvable } from "./resolvable";
10+
import { State } from "../state/stateObject";
11+
import { PathFactory } from "../path/pathFactory";
12+
import { stringify } from "../common/strings";
13+
import { Transition } from "../transition/transition";
14+
import { UIInjector } from "../interface";
1515

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

55+
/** Returns the [[ResolvePolicy]] for the given [[Resolvable]] */
56+
getPolicy(resolvable: Resolvable): ResolvePolicy {
57+
let node = this.findNode(resolvable);
58+
return resolvable.getPolicy(node.state);
59+
}
60+
5561
/**
5662
* Returns a ResolveContext that includes a portion of this one
5763
*
@@ -95,8 +101,8 @@ export class ResolveContext {
95101
* @param state Used to find the node to put the resolvable on
96102
*/
97103
addResolvables(newResolvables: Resolvable[], state: State) {
98-
var node = <PathNode> find(this._path, propEq('state', state));
99-
var keys = newResolvables.map(r => r.token);
104+
let node = <PathNode> find(this._path, propEq('state', state));
105+
let keys = newResolvables.map(r => r.token);
100106
node.resolvables = node.resolvables.filter(r => keys.indexOf(r.token) === -1).concat(newResolvables);
101107
}
102108

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

126+
const matchesPolicy = (acceptedVals: string[], whenOrAsync: "when"|"async") =>
127+
(resolvable: Resolvable) =>
128+
inArray(acceptedVals, this.getPolicy(resolvable)[whenOrAsync]);
129+
130+
// Trigger all the (matching) Resolvables in the path
131+
// Reduce all the "WAIT" Resolvables into an array
120132
let promises: Promise<any>[] = this._path.reduce((acc, node) => {
121-
const matchesRequestedPolicy = (resolvable: Resolvable) =>
122-
inArray(matchedWhens, resolvable.getPolicy(node.state).when);
123-
let nodeResolvables = node.resolvables.filter(matchesRequestedPolicy);
124-
let subContext = this.subContext(node.state);
133+
let nodeResolvables = node.resolvables.filter(matchesPolicy(matchedWhens, 'when'));
134+
let nowait = nodeResolvables.filter(matchesPolicy(['NOWAIT'], 'async'));
135+
let wait = nodeResolvables.filter(not(matchesPolicy(['NOWAIT'], 'async')));
125136

126137
// For the matching Resolvables, start their async fetch process.
127-
var getResult = (r: Resolvable) => r.get(subContext, trans)
138+
let subContext = this.subContext(node.state);
139+
let getResult = (r: Resolvable) => r.get(subContext, trans)
128140
// Return a tuple that includes the Resolvable's token
129141
.then(value => ({ token: r.token, value: value }));
130-
return acc.concat(nodeResolvables.map(getResult));
142+
nowait.forEach(getResult);
143+
return acc.concat(wait.map(getResult));
131144
}, []);
132145

146+
// Wait for all the "WAIT" resolvables
133147
return services.$q.all(promises);
134148
}
135149

@@ -179,8 +193,12 @@ class UIInjectorImpl implements UIInjector {
179193
}
180194

181195
get(token: any) {
182-
var resolvable = this.context.getResolvable(token);
196+
let resolvable = this.context.getResolvable(token);
183197
if (resolvable) {
198+
if (this.context.getPolicy(resolvable).async === 'NOWAIT') {
199+
return resolvable.get(this.context);
200+
}
201+
184202
if (!resolvable.resolved) {
185203
throw new Error("Resolvable async .get() not complete:" + stringify(resolvable.token))
186204
}
@@ -190,12 +208,12 @@ class UIInjectorImpl implements UIInjector {
190208
}
191209

192210
getAsync(token: any) {
193-
var resolvable = this.context.getResolvable(token);
211+
let resolvable = this.context.getResolvable(token);
194212
if (resolvable) return resolvable.get(this.context);
195213
return services.$q.when(this.native.get(token));
196214
}
197215

198216
getNative(token: any) {
199-
return this.native.get(token);
217+
return this.native && this.native.get(token);
200218
}
201219
}

test/resolveSpec.ts

+77-7
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import { UIRouter } from "../src/router";
66

77
import Spy = jasmine.Spy;
88
import { TestingPlugin } from "./_testingPlugin";
9+
import { StateService } from "../src/state/stateService";
10+
import { TransitionService } from "../src/transition/transitionService";
11+
import { StateRegistry } from "../src/state/stateRegistry";
12+
import { tail } from "../src/common/common";
913

1014
///////////////////////////////////////////////
1115

1216
let router: UIRouter, states, statesMap: { [key:string]: State } = {};
17+
let $state: StateService;
18+
let $transitions: TransitionService;
19+
let $registry: StateRegistry;
1320
let vals, counts, expectCounts;
1421
let asyncCount;
1522

@@ -78,13 +85,16 @@ describe('Resolvables system:', function () {
7885
beforeEach(function () {
7986
router = new UIRouter();
8087
router.plugin(TestingPlugin);
88+
$state = router.stateService;
89+
$transitions = router.transitionService;
90+
$registry = router.stateRegistry;
8191

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

86-
tree2Array(getStates(), false).forEach(state => router.stateRegistry.register(state));
87-
statesMap = router.stateRegistry.get()
96+
tree2Array(getStates(), false).forEach(state => $registry.register(state));
97+
statesMap = $registry.get()
8898
.reduce((acc, state) => (acc[state.name] = state.$$state(), acc), statesMap);
8999
});
90100

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

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

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

374379
$registry.register({
@@ -396,6 +401,71 @@ describe('Resolvables system:', function () {
396401
done();
397402
});
398403
});
404+
405+
describe('NOWAIT Resolve Policy', () => {
406+
it('should allow a transition to complete before the resolve is settled', async (done) => {
407+
let resolve, resolvePromise = new Promise(_resolve => { resolve = _resolve; });
408+
409+
$registry.register({
410+
name: 'nowait',
411+
resolve: {
412+
nowait: () => resolvePromise
413+
},
414+
resolvePolicy: { async: 'NOWAIT' }
415+
});
416+
417+
$transitions.onSuccess({ }, trans => {
418+
expect(trans.injector().get('nowait') instanceof Promise).toBeTruthy();
419+
expect(trans.injector().getAsync('nowait') instanceof Promise).toBeTruthy();
420+
expect(trans.injector().getAsync('nowait')).toBe(trans.injector().get('nowait'));
421+
422+
let resolvable = tail(trans.treeChanges('to')).resolvables[0];
423+
expect(resolvable.token).toBe('nowait');
424+
expect(resolvable.resolved).toBe(false);
425+
expect(resolvable.data).toBeUndefined();
426+
427+
trans.injector().get('nowait').then(result => {
428+
expect(result).toBe('foobar');
429+
done();
430+
});
431+
432+
resolve('foobar')
433+
});
434+
435+
$state.go('nowait');
436+
});
437+
438+
it('should wait for WAIT resolves and not wait for NOWAIT resolves', async (done) => {
439+
let resolve, resolvePromise = new Promise(_resolve => { resolve = _resolve; });
440+
441+
$registry.register({
442+
name: 'nowait',
443+
resolve: [
444+
{ token: 'nowait', policy: { async: 'NOWAIT' }, resolveFn: () => resolvePromise },
445+
{ token: 'wait', policy: { async: 'WAIT' }, resolveFn: () => new Promise(resolve => resolve('should wait')) },
446+
]
447+
});
448+
449+
$transitions.onSuccess({ }, trans => {
450+
expect(trans.injector().get('nowait') instanceof Promise).toBeTruthy();
451+
expect(trans.injector().get('wait')).toBe('should wait');
452+
453+
let resolvable = tail(trans.treeChanges('to')).resolvables[0];
454+
expect(resolvable.token).toBe('nowait');
455+
expect(resolvable.resolved).toBe(false);
456+
expect(resolvable.data).toBeUndefined();
457+
458+
trans.injector().get('nowait').then(result => {
459+
expect(result).toBe('foobar');
460+
done();
461+
});
462+
463+
resolve('foobar')
464+
});
465+
466+
$state.go('nowait');
467+
});
468+
});
399469
});
400470

401471

0 commit comments

Comments
 (0)