|
|
/** * @ngdoc object * @name ui.router.util.$resolve * * @requires $q * @requires $injector * * @description * Manages resolution of (acyclic) graphs of promises. */ $Resolve.$inject = ['$q', '$injector']; function $Resolve( $q, $injector) { var VISIT_IN_PROGRESS = 1, VISIT_DONE = 2, NOTHING = {}, NO_DEPENDENCIES = [], NO_LOCALS = NOTHING, NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING });
/** * @ngdoc function * @name ui.router.util.$resolve#study * @methodOf ui.router.util.$resolve * * @description * Studies a set of invocables that are likely to be used multiple times. * <pre> * $resolve.study(invocables)(locals, parent, self) * </pre> * is equivalent to * <pre> * $resolve.resolve(invocables, locals, parent, self) * </pre> * but the former is more efficient (in fact `resolve` just calls `study` * internally). * * @param {object} invocables Invocable objects * @return {function} a function to pass in locals, parent and self */ this.study = function (invocables) { if (!isObject(invocables)) throw new Error("'invocables' must be an object"); var invocableKeys = objectKeys(invocables || {}); // Perform a topological sort of invocables to build an ordered plan
var plan = [], cycle = [], visited = {}; function visit(value, key) { if (visited[key] === VISIT_DONE) return; cycle.push(key); if (visited[key] === VISIT_IN_PROGRESS) { cycle.splice(0, indexOf(cycle, key)); throw new Error("Cyclic dependency: " + cycle.join(" -> ")); } visited[key] = VISIT_IN_PROGRESS; if (isString(value)) { plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); } else { var params = $injector.annotate(value); forEach(params, function (param) { if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); }); plan.push(key, value, params); } cycle.pop(); visited[key] = VISIT_DONE; } forEach(invocables, visit); invocables = cycle = visited = null; // plan is all that's required
function isResolve(value) { return isObject(value) && value.then && value.$$promises; } return function (locals, parent, self) { if (isResolve(locals) && self === undefined) { self = parent; parent = locals; locals = null; } if (!locals) locals = NO_LOCALS; else if (!isObject(locals)) { throw new Error("'locals' must be an object"); } if (!parent) parent = NO_PARENT; else if (!isResolve(parent)) { throw new Error("'parent' must be a promise returned by $resolve.resolve()"); } // To complete the overall resolution, we have to wait for the parent
// promise and for the promise for each invokable in our plan.
var resolution = $q.defer(), result = resolution.promise, promises = result.$$promises = {}, values = extend({}, locals), wait = 1 + plan.length/3, merged = false; function done() { // Merge parent values we haven't got yet and publish our own $$values
if (!--wait) { if (!merged) merge(values, parent.$$values); result.$$values = values; result.$$promises = result.$$promises || true; // keep for isResolve()
delete result.$$inheritedValues; resolution.resolve(values); } } function fail(reason) { result.$$failure = reason; resolution.reject(reason); }
// Short-circuit if parent has already failed
if (isDefined(parent.$$failure)) { fail(parent.$$failure); return result; } if (parent.$$inheritedValues) { merge(values, omit(parent.$$inheritedValues, invocableKeys)); }
// Merge parent values if the parent has already resolved, or merge
// parent promises and wait if the parent resolve is still in progress.
extend(promises, parent.$$promises); if (parent.$$values) { merged = merge(values, omit(parent.$$values, invocableKeys)); result.$$inheritedValues = omit(parent.$$values, invocableKeys); done(); } else { if (parent.$$inheritedValues) { result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); } parent.then(done, fail); } // Process each invocable in the plan, but ignore any where a local of the same name exists.
for (var i=0, ii=plan.length; i<ii; i+=3) { if (locals.hasOwnProperty(plan[i])) done(); else invoke(plan[i], plan[i+1], plan[i+2]); } function invoke(key, invocable, params) { // Create a deferred for this invocation. Failures will propagate to the resolution as well.
var invocation = $q.defer(), waitParams = 0; function onfailure(reason) { invocation.reject(reason); fail(reason); } // Wait for any parameter that we have a promise for (either from parent or from this
// resolve; in that case study() will have made sure it's ordered before us in the plan).
forEach(params, function (dep) { if (promises.hasOwnProperty(dep) && !locals.hasOwnProperty(dep)) { waitParams++; promises[dep].then(function (result) { values[dep] = result; if (!(--waitParams)) proceed(); }, onfailure); } }); if (!waitParams) proceed(); function proceed() { if (isDefined(result.$$failure)) return; try { invocation.resolve($injector.invoke(invocable, self, values)); invocation.promise.then(function (result) { values[key] = result; done(); }, onfailure); } catch (e) { onfailure(e); } } // Publish promise synchronously; invocations further down in the plan may depend on it.
promises[key] = invocation.promise; } return result; }; }; /** * @ngdoc function * @name ui.router.util.$resolve#resolve * @methodOf ui.router.util.$resolve * * @description * Resolves a set of invocables. An invocable is a function to be invoked via * `$injector.invoke()`, and can have an arbitrary number of dependencies. * An invocable can either return a value directly, * or a `$q` promise. If a promise is returned it will be resolved and the * resulting value will be used instead. Dependencies of invocables are resolved * (in this order of precedence) * * - from the specified `locals` * - from another invocable that is part of this `$resolve` call * - from an invocable that is inherited from a `parent` call to `$resolve` * (or recursively * - from any ancestor `$resolve` of that parent). * * The return value of `$resolve` is a promise for an object that contains * (in this order of precedence) * * - any `locals` (if specified) * - the resolved return values of all injectables * - any values inherited from a `parent` call to `$resolve` (if specified) * * The promise will resolve after the `parent` promise (if any) and all promises * returned by injectables have been resolved. If any invocable * (or `$injector.invoke`) throws an exception, or if a promise returned by an * invocable is rejected, the `$resolve` promise is immediately rejected with the * same error. A rejection of a `parent` promise (if specified) will likewise be * propagated immediately. Once the `$resolve` promise has been rejected, no * further invocables will be called. * * Cyclic dependencies between invocables are not permitted and will caues `$resolve` * to throw an error. As a special case, an injectable can depend on a parameter * with the same name as the injectable, which will be fulfilled from the `parent` * injectable of the same name. This allows inherited values to be decorated. * Note that in this case any other injectable in the same `$resolve` with the same * dependency would see the decorated value, not the inherited value. * * Note that missing dependencies -- unlike cyclic dependencies -- will cause an * (asynchronous) rejection of the `$resolve` promise rather than a (synchronous) * exception. * * Invocables are invoked eagerly as soon as all dependencies are available. * This is true even for dependencies inherited from a `parent` call to `$resolve`. * * As a special case, an invocable can be a string, in which case it is taken to * be a service name to be passed to `$injector.get()`. This is supported primarily * for backwards-compatibility with the `resolve` property of `$routeProvider` * routes. * * @param {object} invocables functions to invoke or * `$injector` services to fetch. * @param {object} locals values to make available to the injectables * @param {object} parent a promise returned by another call to `$resolve`. * @param {object} self the `this` for the invoked methods * @return {object} Promise for an object that contains the resolved return value * of all invocables, as well as any inherited and local values. */ this.resolve = function (invocables, locals, parent, self) { return this.study(invocables)(locals, parent, self); }; }
angular.module('ui.router.util').service('$resolve', $Resolve);
|