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

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