Skip to content

Commit 2f31e1c

Browse files
Merge pull request #6 from nateabele/resolvable-rebase
feat($resolve): Super-duper-lazy Resolvable system merge PR from resolvable-rebase
2 parents 041c71b + 641c60e commit 2f31e1c

File tree

7 files changed

+812
-91
lines changed

7 files changed

+812
-91
lines changed

Gruntfile.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ module.exports = function (grunt) {
9797
background: {
9898
background: true,
9999
browsers: [ grunt.option('browser') || 'PhantomJS' ]
100+
},
101+
watch: {
102+
configFile: 'config/karma.js',
103+
singleRun: false,
104+
autoWatch: true,
105+
autoWatchInterval: 1
100106
}
101107
},
102108
changelog: {

src/common.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,70 @@ function filterByKeys(keys, values) {
146146
});
147147
return filtered;
148148
}
149+
150+
// like _.indexBy
151+
// when you know that your index values will be unique, or you want last-one-in to win
152+
function indexBy(array, propName) {
153+
var result = {};
154+
forEach(array, function(item) {
155+
result[item[propName]] = item;
156+
});
157+
return result;
158+
}
159+
160+
// extracted from underscore.js
161+
// Return a copy of the object only containing the whitelisted properties.
162+
function pick(obj) {
163+
var copy = {};
164+
var keys = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1));
165+
forEach(keys, function(key) {
166+
if (key in obj) copy[key] = obj[key];
167+
});
168+
return copy;
169+
}
170+
171+
// extracted from underscore.js
172+
// Return a copy of the object omitting the blacklisted properties.
173+
function omit(obj) {
174+
var copy = {};
175+
var keys = Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(arguments, 1));
176+
for (var key in obj) {
177+
if (keys.indexOf(key) == -1) copy[key] = obj[key];
178+
}
179+
return copy;
180+
}
181+
182+
function pluck(collection, key) {
183+
var result = isArray(collection) ? [] : {};
184+
185+
forEach(collection, function(val, i) {
186+
result[i] = isFunction(key) ? key(val) : val[key];
187+
});
188+
return result;
189+
}
190+
191+
function map(collection, callback) {
192+
var result = isArray(collection) ? [] : {};
193+
194+
forEach(collection, function(val, i) {
195+
result[i] = callback(val, i);
196+
});
197+
return result;
198+
}
199+
200+
function flattenPrototypeChain(obj) {
201+
var objs = [];
202+
do {
203+
objs.push(obj);
204+
} while (obj = Object.getPrototypeOf(obj));
205+
objs.reverse();
206+
207+
var result = {};
208+
forEach(objs, function(obj) {
209+
extend(result, obj);
210+
});
211+
return result;
212+
}
149213
/**
150214
* @ngdoc overview
151215
* @name ui.router.util

src/resolve.js

Lines changed: 285 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
var Resolvable, Path, PathElement, ResolveContext;
2+
13
/**
24
* @ngdoc object
35
* @name ui.router.util.$resolve
@@ -10,7 +12,289 @@
1012
*/
1113
$Resolve.$inject = ['$q', '$injector'];
1214
function $Resolve( $q, $injector) {
13-
15+
16+
/*
17+
------- Resolvable, PathElement, Path, ResolveContext ------------------
18+
I think these should be private API for now because we may need to iterate it for a while.
19+
/*
20+
21+
/* Resolvable
22+
23+
The basic building block for the new resolve system.
24+
Resolvables encapsulate a state's resolve's resolveFn, the resolveFn's declared dependencies, and the wrapped (.promise)
25+
and unwrapped-when-complete (.data) result of the resolveFn.
26+
27+
Resolvable.get() either retrieves the Resolvable's existing promise, or else invokes resolve() (which invokes the
28+
resolveFn) and returns the resulting promise.
29+
30+
Resolvable.get() and Resolvable.resolve() both execute within a ResolveContext, which is passed as the first
31+
parameter to those fns.
32+
*/
33+
34+
35+
Resolvable = function Resolvable(name, resolveFn, state) {
36+
var self = this;
37+
38+
// Resolvable: resolveResolvable() This function is aliased to Resolvable.resolve()
39+
40+
// synchronous part:
41+
// - sets up the Resolvable's promise
42+
// - retrieves dependencies' promises
43+
// - returns promise for async part
44+
45+
// asynchronous part:
46+
// - wait for dependencies promises to resolve
47+
// - invoke the resolveFn
48+
// - wait for resolveFn promise to resolve
49+
// - store unwrapped data
50+
// - resolve the Resolvable's promise
51+
function resolveResolvable(resolveContext) {
52+
// First, set up an overall deferred/promise for this Resolvable
53+
var deferred = $q.defer();
54+
self.promise = deferred.promise;
55+
56+
// Load an assoc-array of all resolvables for this state from the resolveContext
57+
// omit the current Resolvable from the PathElement in the ResolveContext so we don't try to inject self into self
58+
var options = { omitPropsFromPrototype: [ self.name ], flatten: true };
59+
var ancestorsByName = resolveContext.getResolvableLocals(self.state.name, options);
60+
61+
// Limit the ancestors Resolvables map to only those that the current Resolvable fn's annotations depends on
62+
var depResolvables = pick(ancestorsByName, self.deps);
63+
64+
// Get promises (or synchronously invoke resolveFn) for deps
65+
var depPromises = map(depResolvables, function(resolvable) {
66+
return resolvable.get(resolveContext);
67+
});
68+
69+
// Return a promise chain that waits for all the deps to resolve, then invokes the resolveFn passing in the
70+
// dependencies as locals, then unwraps the resulting promise's data.
71+
return $q.all(depPromises).then(function invokeResolve(locals) {
72+
try {
73+
var result = $injector.invoke(self.resolveFn, state, locals);
74+
deferred.resolve(result);
75+
} catch (error) {
76+
deferred.reject(error);
77+
}
78+
return self.promise;
79+
}).then(function(data) {
80+
self.data = data;
81+
return self.promise;
82+
})
83+
}
84+
85+
// Public API
86+
extend(this, {
87+
name: name,
88+
resolveFn: resolveFn,
89+
state: state,
90+
deps: $injector.annotate(resolveFn),
91+
resolve: resolveResolvable, // aliased function name for stacktraces
92+
promise: undefined,
93+
data: undefined,
94+
get: function(resolveContext) {
95+
return self.promise || resolveResolvable(resolveContext);
96+
}
97+
});
98+
};
99+
100+
// An element in the path which represents a state and that state's Resolvables and their resolve statuses.
101+
// When the resolved data is ready, it is stored in each Resolvable object within the PathElement
102+
103+
// Should be passed a state object. I think maybe state could even be the public state, so users can add resolves
104+
// on the fly.
105+
PathElement = function PathElement(state) {
106+
var self = this;
107+
// Convert state's resolvable assoc-array into an assoc-array of empty Resolvable(s)
108+
var resolvables = map(state.resolve || {}, function(resolveFn, resolveName) {
109+
return new Resolvable(resolveName, resolveFn, state);
110+
});
111+
112+
// private function
113+
// returns a promise for all resolvables on this PathElement
114+
function resolvePathElement(resolveContext) {
115+
return $q.all(map(resolvables, function(resolvable) { return resolvable.get(resolveContext); }));
116+
}
117+
118+
// Injects a function at this PathElement level with available Resolvables
119+
// First it resolves all resolvables. When they are done resolving, invokes the function.
120+
// Returns a promise for the return value of the function.
121+
// public function
122+
// fn is the function to inject (onEnter, onExit, controller)
123+
// locals are the regular-style locals to inject
124+
// resolveContext is a ResolveContext which is for injecting state Resolvable(s)
125+
function invokeLater(fn, locals, resolveContext) {
126+
var deps = $injector.annotate(fn);
127+
var resolvables = pick(resolveContext.getResolvableLocals(self.$$state.name), deps);
128+
var promises = map(resolvables, function(resolvable) { return resolvable.get(resolveContext); });
129+
return $q.all(promises).then(function() {
130+
try {
131+
return self.invokeNow(fn, locals, resolveContext);
132+
} catch (error) {
133+
return $q.reject(error);
134+
}
135+
});
136+
}
137+
138+
// private function? Maybe needs to be public-to-$transition to allow onEnter/onExit to be invoked synchronously
139+
// and in the correct order, but only after we've manually ensured all the deps are resolved.
140+
141+
// Injects a function at this PathElement level with available Resolvables
142+
// Does not wait until all Resolvables have been resolved; you must call PathElement.resolve() (or manually resolve each dep) first
143+
function invokeNow(fn, locals, resolveContext) {
144+
var resolvables = resolveContext.getResolvableLocals(self.$$state.name);
145+
var moreLocals = map(resolvables, function(resolvable) { return resolvable.data; });
146+
var combinedLocals = extend({}, locals, moreLocals);
147+
return $injector.invoke(fn, self.$$state, combinedLocals);
148+
}
149+
150+
// public API so far
151+
extend(this, {
152+
state: function() { return state; },
153+
$$state: state,
154+
resolvables: function() { return resolvables; },
155+
$$resolvables: resolvables,
156+
resolve: resolvePathElement, // aliased function for stacktraces
157+
invokeNow: invokeNow, // this might be private later
158+
invokeLater: invokeLater
159+
});
160+
};
161+
162+
// A Path Object holds an ordered list of PathElements.
163+
// This object is used by ResolveContext to store resolve status for an entire path of states.
164+
// It has concat and slice helper methods to return new Paths, based on the current Path.
165+
166+
// statesOrPathElements must be an array of either state(s) or PathElement(s)
167+
// states could be "public" state objects for this?
168+
Path = function Path(statesOrPathElements) {
169+
var self = this;
170+
if (!isArray(statesOrPathElements)) throw new Error("states must be an array of state(s) or PathElement(s)", statesOrPathElements);
171+
var isPathElementArray = (statesOrPathElements.length && (statesOrPathElements[0] instanceof PathElement));
172+
173+
var elements = statesOrPathElements;
174+
if (!isPathElementArray) { // they passed in states; convert them to PathElements
175+
elements = map(elements, function (state) { return new PathElement(state); });
176+
}
177+
178+
// resolveContext holds stateful Resolvables (containing possibly resolved data), mapped per state-name.
179+
function resolvePath(resolveContext) {
180+
return $q.all(map(elements, function(element) { return element.resolve(resolveContext); }));
181+
}
182+
183+
// Not used
184+
function invoke(hook, self, locals) {
185+
if (!hook) return;
186+
return $injector.invoke(hook, self, locals);
187+
}
188+
189+
// Public API
190+
extend(this, {
191+
resolve: resolvePath,
192+
$$elements: elements, // for development at least
193+
concat: function(path) {
194+
return new Path(elements.concat(path.elements()));
195+
},
196+
slice: function(start, end) {
197+
return new Path(elements.slice(start, end));
198+
},
199+
elements: function() {
200+
return elements;
201+
},
202+
// I haven't looked at how $$enter and $$exit are going be used.
203+
$$enter: function(/* locals */) {
204+
// TODO: Replace with PathElement.invoke(Now|Later)
205+
// TODO: If invokeNow (synchronous) then we have to .get() all Resolvables for all functions first.
206+
for (var i = 0; i < states.length; i++) {
207+
// entering.locals = toLocals[i];
208+
if (invoke(states[i].self.onEnter, states[i].self, locals(states[i])) === false) return false;
209+
}
210+
return true;
211+
},
212+
$$exit: function(/* locals */) {
213+
// TODO: Replace with PathElement.invoke(Now|Later)
214+
for (var i = states.length - 1; i >= 0; i--) {
215+
if (invoke(states[i].self.onExit, states[i].self, locals(states[i])) === false) return false;
216+
// states[i].locals = null;
217+
}
218+
return true;
219+
}
220+
});
221+
};
222+
223+
// ResolveContext is passed into each resolve() function, and is used to statefully manage Resolve status.
224+
// ResolveContext is essentially the replacement data structure for $state.$current.locals and we'll have to
225+
// figure out where to store/manage this data structure.
226+
// It manages a set of Resolvables that are available at each level of the Path.
227+
// It follows the list of PathElements and inherit()s the PathElement's Resolvables on top of the
228+
// previous PathElement's Resolvables. i.e., it builds a prototypal chain for the PathElements' Resolvables.
229+
// Before moving on to the next PathElement, it makes a note of what Resolvables are available for the current
230+
// PathElement, and maps it by state name.
231+
232+
// ResolveContext constructor takes a path which is assumed to be partially resolved, or
233+
// not resolved at all, which we're in process of resolving
234+
ResolveContext = function ResolveContext(path) {
235+
if (path === undefined) path = new Path([]);
236+
var resolvablesByState = {}, previousIteration = {};
237+
238+
forEach(path.elements(), function (pathElem) {
239+
var resolvablesForPE = pathElem.resolvables();
240+
var resolvesbyName = indexBy(resolvablesForPE, 'name');
241+
var resolvables = inherit(previousIteration, resolvesbyName); // note prototypal inheritance
242+
previousIteration = resolvablesByState[pathElem.state().name] = resolvables;
243+
});
244+
245+
// Gets resolvables available for a particular state.
246+
// TODO: This should probably be "for a particular PathElement" instead of state, but PathElements encapsulate a state.
247+
// This returns the Resolvable map by state name.
248+
249+
// options.omitPropsFromPrototype
250+
// Remove the props specified in options.omitPropsFromPrototype from the prototype of the object.
251+
252+
// This hides a top-level resolvable by name, potentially exposing a parent resolvable of the same name
253+
// further down the prototype chain.
254+
255+
// This is used to provide a Resolvable access to all other Resolvables in its same PathElement, yet disallow
256+
// that Resolvable access to its own injectable Resolvable reference.
257+
258+
// This is also used to allow a state to override a parent state's resolve while also injecting
259+
// that parent state's resolve:
260+
261+
// state({ name: 'G', resolve: { _G: function() { return "G"; } } });
262+
// state({ name: 'G.G2', resolve: { _G: function(_G) { return _G + "G2"; } } });
263+
// where injecting _G into a controller will yield "GG2"
264+
265+
// options.flatten
266+
// $$resolvablesByState has resolvables organized in a prototypal inheritance chain. options.flatten will
267+
// flatten the object from prototypal inheritance to a simple object with all its prototype chain properties
268+
// exposed with child properties taking precedence over parent properties.
269+
function getResolvableLocals(stateName, options) {
270+
var resolvables = (resolvablesByState[stateName] || {});
271+
options = extend({ flatten: true, omitPropsFromPrototype: [] }, options);
272+
273+
// Create a shallow clone referencing the original prototype chain. This is so we can alter the clone's
274+
// prototype without affecting the actual object (for options.omitPropsFromPrototype)
275+
var shallowClone = Object.create(Object.getPrototypeOf(resolvables));
276+
for (property in resolvables) {
277+
if (resolvables.hasOwnProperty(property)) { shallowClone[property] = resolvables[property]; }
278+
}
279+
280+
// Omit any specified top-level prototype properties
281+
forEach(options.omitPropsFromPrototype, function(prop) {
282+
delete(shallowClone[prop]); // possibly exposes the same prop from prototype chain
283+
});
284+
285+
if (options.flatten) // Flatten from prototypal chain to simple object
286+
shallowClone = flattenPrototypeChain(shallowClone);
287+
288+
return shallowClone;
289+
}
290+
291+
extend(this, {
292+
getResolvableLocals: getResolvableLocals,
293+
$$resolvablesByState: resolvablesByState
294+
});
295+
};
296+
297+
// ----------------- 0.2.xx Legacy API here ------------------------
14298
var VISIT_IN_PROGRESS = 1,
15299
VISIT_DONE = 2,
16300
NOTHING = {},

0 commit comments

Comments
 (0)