3 * @name ui.router.util.$resolve
9 * Manages resolution of (acyclic) graphs of promises.
11 $Resolve.$inject = ['$q', '$injector'];
12 function $Resolve( $q, $injector) {
14 var VISIT_IN_PROGRESS = 1,
19 NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING });
24 * @name ui.router.util.$resolve#study
25 * @methodOf ui.router.util.$resolve
28 * Studies a set of invocables that are likely to be used multiple times.
30 * $resolve.study(invocables)(locals, parent, self)
34 * $resolve.resolve(invocables, locals, parent, self)
36 * but the former is more efficient (in fact `resolve` just calls `study`
39 * @param {object} invocables Invocable objects
40 * @return {function} a function to pass in locals, parent and self
42 this.study = function (invocables) {
43 if (!isObject(invocables)) throw new Error("'invocables' must be an object");
44 var invocableKeys = objectKeys(invocables || {});
46 // Perform a topological sort of invocables to build an ordered plan
47 var plan = [], cycle = [], visited = {};
48 function visit(value, key) {
49 if (visited[key] === VISIT_DONE) return;
52 if (visited[key] === VISIT_IN_PROGRESS) {
53 cycle.splice(0, indexOf(cycle, key));
54 throw new Error("Cyclic dependency: " + cycle.join(" -> "));
56 visited[key] = VISIT_IN_PROGRESS;
58 if (isString(value)) {
59 plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES);
61 var params = $injector.annotate(value);
62 forEach(params, function (param) {
63 if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param);
65 plan.push(key, value, params);
69 visited[key] = VISIT_DONE;
71 forEach(invocables, visit);
72 invocables = cycle = visited = null; // plan is all that's required
74 function isResolve(value) {
75 return isObject(value) && value.then && value.$$promises;
78 return function (locals, parent, self) {
79 if (isResolve(locals) && self === undefined) {
80 self = parent; parent = locals; locals = null;
82 if (!locals) locals = NO_LOCALS;
83 else if (!isObject(locals)) {
84 throw new Error("'locals' must be an object");
86 if (!parent) parent = NO_PARENT;
87 else if (!isResolve(parent)) {
88 throw new Error("'parent' must be a promise returned by $resolve.resolve()");
91 // To complete the overall resolution, we have to wait for the parent
92 // promise and for the promise for each invokable in our plan.
93 var resolution = $q.defer(),
94 result = resolution.promise,
95 promises = result.$$promises = {},
96 values = extend({}, locals),
97 wait = 1 + plan.length/3,
101 // Merge parent values we haven't got yet and publish our own $$values
103 if (!merged) merge(values, parent.$$values);
104 result.$$values = values;
105 result.$$promises = result.$$promises || true; // keep for isResolve()
106 delete result.$$inheritedValues;
107 resolution.resolve(values);
111 function fail(reason) {
112 result.$$failure = reason;
113 resolution.reject(reason);
116 // Short-circuit if parent has already failed
117 if (isDefined(parent.$$failure)) {
118 fail(parent.$$failure);
122 if (parent.$$inheritedValues) {
123 merge(values, omit(parent.$$inheritedValues, invocableKeys));
126 // Merge parent values if the parent has already resolved, or merge
127 // parent promises and wait if the parent resolve is still in progress.
128 extend(promises, parent.$$promises);
129 if (parent.$$values) {
130 merged = merge(values, omit(parent.$$values, invocableKeys));
131 result.$$inheritedValues = omit(parent.$$values, invocableKeys);
134 if (parent.$$inheritedValues) {
135 result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys);
137 parent.then(done, fail);
140 // Process each invocable in the plan, but ignore any where a local of the same name exists.
141 for (var i=0, ii=plan.length; i<ii; i+=3) {
142 if (locals.hasOwnProperty(plan[i])) done();
143 else invoke(plan[i], plan[i+1], plan[i+2]);
146 function invoke(key, invocable, params) {
147 // Create a deferred for this invocation. Failures will propagate to the resolution as well.
148 var invocation = $q.defer(), waitParams = 0;
149 function onfailure(reason) {
150 invocation.reject(reason);
153 // Wait for any parameter that we have a promise for (either from parent or from this
154 // resolve; in that case study() will have made sure it's ordered before us in the plan).
155 forEach(params, function (dep) {
156 if (promises.hasOwnProperty(dep) && !locals.hasOwnProperty(dep)) {
158 promises[dep].then(function (result) {
159 values[dep] = result;
160 if (!(--waitParams)) proceed();
164 if (!waitParams) proceed();
166 if (isDefined(result.$$failure)) return;
168 invocation.resolve($injector.invoke(invocable, self, values));
169 invocation.promise.then(function (result) {
170 values[key] = result;
177 // Publish promise synchronously; invocations further down in the plan may depend on it.
178 promises[key] = invocation.promise;
187 * @name ui.router.util.$resolve#resolve
188 * @methodOf ui.router.util.$resolve
191 * Resolves a set of invocables. An invocable is a function to be invoked via
192 * `$injector.invoke()`, and can have an arbitrary number of dependencies.
193 * An invocable can either return a value directly,
194 * or a `$q` promise. If a promise is returned it will be resolved and the
195 * resulting value will be used instead. Dependencies of invocables are resolved
196 * (in this order of precedence)
198 * - from the specified `locals`
199 * - from another invocable that is part of this `$resolve` call
200 * - from an invocable that is inherited from a `parent` call to `$resolve`
202 * - from any ancestor `$resolve` of that parent).
204 * The return value of `$resolve` is a promise for an object that contains
205 * (in this order of precedence)
207 * - any `locals` (if specified)
208 * - the resolved return values of all injectables
209 * - any values inherited from a `parent` call to `$resolve` (if specified)
211 * The promise will resolve after the `parent` promise (if any) and all promises
212 * returned by injectables have been resolved. If any invocable
213 * (or `$injector.invoke`) throws an exception, or if a promise returned by an
214 * invocable is rejected, the `$resolve` promise is immediately rejected with the
215 * same error. A rejection of a `parent` promise (if specified) will likewise be
216 * propagated immediately. Once the `$resolve` promise has been rejected, no
217 * further invocables will be called.
219 * Cyclic dependencies between invocables are not permitted and will cause `$resolve`
220 * to throw an error. As a special case, an injectable can depend on a parameter
221 * with the same name as the injectable, which will be fulfilled from the `parent`
222 * injectable of the same name. This allows inherited values to be decorated.
223 * Note that in this case any other injectable in the same `$resolve` with the same
224 * dependency would see the decorated value, not the inherited value.
226 * Note that missing dependencies -- unlike cyclic dependencies -- will cause an
227 * (asynchronous) rejection of the `$resolve` promise rather than a (synchronous)
230 * Invocables are invoked eagerly as soon as all dependencies are available.
231 * This is true even for dependencies inherited from a `parent` call to `$resolve`.
233 * As a special case, an invocable can be a string, in which case it is taken to
234 * be a service name to be passed to `$injector.get()`. This is supported primarily
235 * for backwards-compatibility with the `resolve` property of `$routeProvider`
238 * @param {object} invocables functions to invoke or
239 * `$injector` services to fetch.
240 * @param {object} locals values to make available to the injectables
241 * @param {object} parent a promise returned by another call to `$resolve`.
242 * @param {object} self the `this` for the invoked methods
243 * @return {object} Promise for an object that contains the resolved return value
244 * of all invocables, as well as any inherited and local values.
246 this.resolve = function (invocables, locals, parent, self) {
247 return this.study(invocables)(locals, parent, self);
251 angular.module('ui.router.util').service('$resolve', $Resolve);