Skip to content

Commit 93dd696

Browse files
ksperlingnateabele
authored andcommitted
$resolve service (tested) and integration into $state (only partially tested)
1 parent 8a6f2ee commit 93dd696

File tree

5 files changed

+558
-61
lines changed

5 files changed

+558
-61
lines changed

src/common.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ function inherit(parent, extra) {
1515
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
1616
}
1717

18-
/**
19-
* Extends the destination object `dst` by copying all of the properties from the `src` object(s)
20-
* to `dst` if the `dst` object has no own property of the same name. You can specify multiple
21-
* `src` objects.
22-
*
23-
* @param {Object} dst Destination object.
24-
* @param {...Object} src Source object(s).
25-
* @see angular.extend
26-
*/
2718
function merge(dst) {
2819
forEach(arguments, function(obj) {
2920
if (obj !== dst) {

src/resolve.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* Service (`ui-util`). Manages resolution of (acyclic) graphs of promises.
3+
* @module $resolve
4+
* @requires $q
5+
* @requires $injector
6+
*/
7+
$Resolve.$inject = ['$q', '$injector'];
8+
function $Resolve( $q, $injector) {
9+
10+
var VISIT_IN_PROGRESS = 1,
11+
VISIT_DONE = 2,
12+
NOTHING = {},
13+
NO_DEPENDENCIES = [],
14+
NO_LOCALS = NOTHING,
15+
NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING });
16+
17+
18+
/**
19+
* Studies a set of invocables that are likely to be used multiple times.
20+
* $resolve.study(invocables)(locals, parent, self)
21+
* is equivalent to
22+
* $resolve.resolve(invocables, locals, parent, self)
23+
* but the former is more efficient (in fact `resolve` just calls `study` internally).
24+
* See {@link module:$resolve/resolve} for details.
25+
* @function
26+
* @param {Object} invocables
27+
* @return {Function}
28+
*/
29+
this.study = function (invocables) {
30+
if (!isObject(invocables)) throw new Error("'invocables' must be an object");
31+
32+
// Perform a topological sort of invocables to build an ordered plan
33+
var plan = [], cycle = [], visited = {};
34+
function visit(value, key) {
35+
if (visited[key] === VISIT_DONE) return;
36+
37+
cycle.push(key);
38+
if (visited[key] === VISIT_IN_PROGRESS) {
39+
cycle.splice(0, cycle.indexOf(key));
40+
throw new Error("Cyclic dependency: " + cycle.join(" -> "));
41+
}
42+
visited[key] = VISIT_IN_PROGRESS;
43+
44+
if (isString(value)) {
45+
plan.push(key, [ function() { return $injector.get(key); }], NO_DEPENDENCIES);
46+
} else {
47+
var params = $injector.annotate(value);
48+
forEach(params, function (param) {
49+
if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param);
50+
});
51+
plan.push(key, value, params);
52+
}
53+
54+
cycle.pop();
55+
visited[key] = VISIT_DONE;
56+
}
57+
forEach(invocables, visit);
58+
invocables = cycle = visited = null; // plan is all that's required
59+
60+
function isResolve(value) {
61+
return isObject(value) && value.then && value.$$promises;
62+
}
63+
64+
return function (locals, parent, self) {
65+
if (isResolve(locals) && self === undefined) {
66+
self = parent; parent = locals; locals = null;
67+
}
68+
if (!locals) locals = NO_LOCALS;
69+
else if (!isObject(locals)) {
70+
throw new Error("'locals' must be an object");
71+
}
72+
if (!parent) parent = NO_PARENT;
73+
else if (!isResolve(parent)) {
74+
throw new Error("'parent' must be a promise returned by $resolve.resolve()");
75+
}
76+
77+
// To complete the overall resolution, we have to wait for the parent
78+
// promise and for the promise for each invokable in our plan.
79+
var resolution = $q.defer(),
80+
result = resolution.promise,
81+
promises = result.$$promises = {},
82+
values = extend({}, locals),
83+
wait = 1 + plan.length/3,
84+
merged = false;
85+
86+
function done() {
87+
// Merge parent values we haven't got yet and publish our own $$values
88+
if (!--wait) {
89+
if (!merged) merge(values, parent.$$values);
90+
result.$$values = values;
91+
result.$$promises = true; // keep for isResolve()
92+
resolution.resolve(values);
93+
}
94+
}
95+
96+
function fail(reason) {
97+
result.$$failure = reason;
98+
resolution.reject(reason);
99+
}
100+
101+
// Short-circuit if parent has already failed
102+
if (isDefined(parent.$$failure)) {
103+
fail(parent.$$failure);
104+
return result;
105+
}
106+
107+
// Merge parent values if the parent has already resolved, or merge
108+
// parent promises and wait if the parent resolve is still in progress.
109+
if (parent.$$values) {
110+
merged = merge(values, parent.$$values);
111+
done();
112+
} else {
113+
extend(promises, parent.$$promises);
114+
parent.then(done, fail);
115+
}
116+
117+
// Process each invocable in the plan, but ignore any where a local of the same name exists.
118+
for (var i=0, ii=plan.length; i<ii; i+=3) {
119+
if (locals.hasOwnProperty(plan[i])) done();
120+
else invoke(plan[i], plan[i+1], plan[i+2]);
121+
}
122+
123+
function invoke(key, invocable, params) {
124+
// Create a deferred for this invocation. Failures will propagate to the resolution as well.
125+
var invocation = $q.defer(), waitParams = 0;
126+
function onfailure(reason) {
127+
invocation.reject(reason);
128+
fail(reason);
129+
}
130+
// Wait for any parameter that we have a promise for (either from parent or from this
131+
// resolve; in that case study() will have made sure it's ordered before us in the plan).
132+
params.forEach(function (dep) {
133+
if (promises.hasOwnProperty(dep) && !locals.hasOwnProperty(dep)) {
134+
waitParams++;
135+
promises[dep].then(function (result) {
136+
values[dep] = result;
137+
if (!(--waitParams)) proceed();
138+
}, onfailure);
139+
}
140+
});
141+
if (!waitParams) proceed();
142+
function proceed() {
143+
if (isDefined(result.$$failure)) return;
144+
try {
145+
invocation.resolve($injector.invoke(invocable, self, values));
146+
invocation.promise.then(function (result) {
147+
values[key] = result;
148+
done();
149+
}, onfailure);
150+
} catch (e) {
151+
onfailure(e);
152+
}
153+
}
154+
// Publish promise synchronously; invocations further down in the plan may depend on it.
155+
promises[key] = invocation.promise;
156+
}
157+
158+
return result;
159+
};
160+
};
161+
162+
/**
163+
* Resolves a set of invocables. An invocable is a function to be invoked via `$injector.invoke()`,
164+
* and can have an arbitrary number of dependencies. An invocable can either return a value directly,
165+
* or a `$q` promise. If a promise is returned it will be resolved and the resulting value will be
166+
* used instead. Dependencies of invocables are resolved (in this order of precedence)
167+
*
168+
* - from the specified `locals`
169+
* - from another invocable that is part of this `$resolve` call
170+
* - from an invocable that is inherited from a `parent` call to `$resolve` (or recursively
171+
* from any ancestor `$resolve` of that parent).
172+
*
173+
* The return value of `$resolve` is a promise for an object that contains (in this order of precedence)
174+
*
175+
* - any `locals` (if specified)
176+
* - the resolved return values of all injectables
177+
* - any values inherited from a `parent` call to `$resolve` (if specified)
178+
*
179+
* The promise will resolve after the `parent` promise (if any) and all promises returned by injectables
180+
* have been resolved. If any invocable (or `$injector.invoke`) throws an exception, or if a promise
181+
* returned by an invocable is rejected, the `$resolve` promise is immediately rejected with the same error.
182+
* A rejection of a `parent` promise (if specified) will likewise be propagated immediately. Once the
183+
* `$resolve` promise has been rejected, no further invocables will be called.
184+
*
185+
* Cyclic dependencies between invocables are not permitted and will caues `$resolve` to throw an
186+
* error. As a special case, an injectable can depend on a parameter with the same name as the injectable,
187+
* which will be fulfilled from the `parent` injectable of the same name. This allows inherited values
188+
* to be decorated. Note that in this case any other injectable in the same `$resolve` with the same
189+
* dependency would see the decorated value, not the inherited value.
190+
*
191+
* Note that missing dependencies -- unlike cyclic dependencies -- will cause an (asynchronous) rejection
192+
* of the `$resolve` promise rather than a (synchronous) exception.
193+
*
194+
* Invocables are invoked eagerly as soon as all dependencies are available. This is true even for
195+
* dependencies inherited from a `parent` call to `$resolve`.
196+
*
197+
* As a special case, an invocable can be a string, in which case it is taken to be a service name
198+
* to be passed to `$injector.get()`. This is supported primarily for backwards-compatibility with the
199+
* `resolve` property of `$routeProvider` routes.
200+
*
201+
* @function
202+
* @param {Object.<string, Function|string>} invocables functions to invoke or `$injector` services to fetch.
203+
* @param {Object.<string, *>} [locals] values to make available to the injectables
204+
* @param {Promise.<Object>} [parent] a promise returned by another call to `$resolve`.
205+
* @param {Object} [self] the `this` for the invoked methods
206+
* @return {Promise.<Object>} Promise for an object that contains the resolved return value
207+
* of all invocables, as well as any inherited and local values.
208+
*/
209+
this.resolve = function (invocables, locals, parent, self) {
210+
return this.study(invocables)(locals, parent, self);
211+
};
212+
}
213+
214+
angular.module('ui.router.util').service('$resolve', $Resolve);
215+

src/state.js

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
175175
views: null,
176176
'abstract': true
177177
});
178-
root.locals = { globals: { $stateParams: {} } };
179178
root.navigable = null;
180179

181180

@@ -192,12 +191,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
192191

193192
// $urlRouter is injected just to ensure it gets instantiated
194193
this.$get = $get;
195-
$get.$inject = ['$rootScope', '$q', '$view', '$injector', '$stateParams', '$location', '$urlRouter'];
196-
function $get( $rootScope, $q, $view, $injector, $stateParams, $location, $urlRouter) {
194+
$get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$location', '$urlRouter'];
195+
function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $location, $urlRouter) {
197196

198197
var TransitionSuperseded = $q.reject(new Error('transition superseded'));
199198
var TransitionPrevented = $q.reject(new Error('transition prevented'));
200199

200+
root.locals = { resolve: null, globals: { $stateParams: {} } };
201201
$state = {
202202
params: {},
203203
current: root.self,
@@ -259,7 +259,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
259259
resolved = resolveState(state, toParams, state===to, resolved, locals);
260260
}
261261

262-
// Once everything is resolved, we are ready to perform the actual transition
262+
// Once everything is resolved, wer are ready to perform the actual transition
263263
// and return a promise for the new state. We also keep track of what the
264264
// current promise is, so that we can detect overlapping transitions and
265265
// keep only the outcome of the last transition.
@@ -341,67 +341,41 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
341341
};
342342

343343
function resolveState(state, params, paramsAreFiltered, inherited, dst) {
344-
// We need to track all the promises generated during the resolution process.
345-
// The first of these is for the fully resolved parent locals.
346-
var promises = [inherited];
347-
348344
// Make a restricted $stateParams with only the parameters that apply to this state if
349345
// necessary. In addition to being available to the controller and onEnter/onExit callbacks,
350346
// we also need $stateParams to be available for any $injector calls we make during the
351347
// dependency resolution process.
352348
var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params, params);
353349
var locals = { $stateParams: $stateParams };
354350

355-
// Resolves the values from an individual 'resolve' dependency spec
356-
function resolve(deps, dst) {
357-
forEach(deps, function (value, key) {
358-
promises.push($q
359-
.when(isString(value) ?
360-
$injector.get(value) :
361-
$injector.invoke(value, state.self, locals))
362-
.then(function (result) {
363-
dst[key] = result;
364-
}));
365-
});
366-
}
367-
368351
// Resolve 'global' dependencies for the state, i.e. those not specific to a view.
369352
// We're also including $stateParams in this; that way the parameters are restricted
370353
// to the set that should be visible to the state, and are independent of when we update
371354
// the global $state and $stateParams values.
372-
var globals = dst.globals = { $stateParams: $stateParams };
373-
resolve(state.resolve, globals);
374-
globals.$$state = state; // Provide access to the state itself for internal use
355+
dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
356+
var promises = [ dst.resolve.then(function (globals) {
357+
dst.globals = globals;
358+
}) ];
359+
if (inherited) promises.push(inherited);
375360

376361
// Resolve template and dependencies for all views.
377362
forEach(state.views, function (view, name) {
378-
// References to the controller (only instantiated at link time)
379-
var _$view = dst[name] = {
380-
$$controller: view.controller
381-
};
382-
383-
// Template
384-
promises.push($q
385-
.when($view.load(name, { view: view, locals: locals, params: $stateParams, notify: false }) || '')
386-
.then(function (result) {
387-
_$view.$template = result;
388-
}));
389-
390-
// View-local dependencies. If we've reused the state definition as the default
391-
// view definition in .state(), we can end up with state.resolve === view.resolve.
392-
// Avoid resolving everything twice in that case.
393-
if (view.resolve !== state.resolve) resolve(view.resolve, _$view);
363+
var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {});
364+
injectables.$template = [ function () {
365+
return $view.load(name, { view: view, locals: locals, params: $stateParams, notify: false }) || '';
366+
}];
367+
368+
promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) {
369+
// References to the controller (only instantiated at link time)
370+
result.$$controller = view.controller;
371+
// Provide access to the state itself for internal use
372+
result.$$state = state;
373+
dst[name] = result;
374+
}));
394375
});
395376

396-
// Once we've resolved all the dependencies for this state, merge
397-
// in any inherited dependencies, and merge common state dependencies
398-
// into the dependency set for each view. Finally return a promise
399-
// for the fully popuplated state dependencies.
377+
// Wait for all the promises and then return the activation object
400378
return $q.all(promises).then(function (values) {
401-
merge(dst.globals, values[0].globals); // promises[0] === inherited
402-
forEach(state.views, function (view, name) {
403-
merge(dst[name], dst.globals);
404-
});
405379
return dst;
406380
});
407381
}

0 commit comments

Comments
 (0)