You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

252 lines
9.5 KiB

/**
* @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);