|
| 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 | + |
0 commit comments