UNPKG

11.8 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * @typedef {Object<string, ComponentCategory>} Components
5 * @typedef {Object<string, ComponentEntry | string>} ComponentCategory
6 *
7 * @typedef ComponentEntry
8 * @property {string} [title] The title of the component.
9 * @property {string} [owner] The GitHub user name of the owner.
10 * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
11 * @property {string | string[]} [alias] An optional list of aliases for the id of the component.
12 * @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
13 *
14 * Aliases which are not in this map will the get title of the component.
15 * @property {string | string[]} [optional]
16 * @property {string | string[]} [require]
17 * @property {string | string[]} [modify]
18 */
19
20var getLoader = (function () {
21
22 /**
23 * A function which does absolutely nothing.
24 *
25 * @type {any}
26 */
27 var noop = function () { };
28
29 /**
30 * Invokes the given callback for all elements of the given value.
31 *
32 * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or
33 * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given
34 * value as parameter.
35 *
36 * @param {null | undefined | T | T[]} value
37 * @param {(value: T, index: number) => void} callbackFn
38 * @returns {void}
39 * @template T
40 */
41 function forEach(value, callbackFn) {
42 if (Array.isArray(value)) {
43 value.forEach(callbackFn);
44 } else if (value != null) {
45 callbackFn(value, 0);
46 }
47 }
48
49 /**
50 * Returns a new set for the given string array.
51 *
52 * @param {string[]} array
53 * @returns {StringSet}
54 *
55 * @typedef {Object<string, true>} StringSet
56 */
57 function toSet(array) {
58 /** @type {StringSet} */
59 var set = {};
60 for (var i = 0, l = array.length; i < l; i++) {
61 set[array[i]] = true;
62 }
63 return set;
64 }
65
66 /**
67 * Creates a map of every components id to its entry.
68 *
69 * @param {Components} components
70 * @returns {EntryMap}
71 *
72 * @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
73 */
74 function createEntryMap(components) {
75 /** @type {Object<string, Readonly<ComponentEntry>>} */
76 var map = {};
77
78 for (var categoryName in components) {
79 var category = components[categoryName];
80 for (var id in category) {
81 if (id != 'meta') {
82 /** @type {ComponentEntry | string} */
83 var entry = category[id];
84 map[id] = typeof entry == 'string' ? { title: entry } : entry;
85 }
86 }
87 }
88
89 return map;
90 }
91
92 /**
93 * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
94 *
95 * @param {EntryMap} entryMap
96 * @returns {DependencyResolver}
97 *
98 * @typedef {(id: string) => StringSet} DependencyResolver
99 */
100 function createDependencyResolver(entryMap) {
101 /** @type {Object<string, StringSet>} */
102 var map = {};
103 var _stackArray = [];
104
105 /**
106 * Adds the dependencies of the given component to the dependency map.
107 *
108 * @param {string} id
109 * @param {string[]} stack
110 */
111 function addToMap(id, stack) {
112 if (id in map) {
113 return;
114 }
115
116 stack.push(id);
117
118 // check for circular dependencies
119 var firstIndex = stack.indexOf(id);
120 if (firstIndex < stack.length - 1) {
121 throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
122 }
123
124 /** @type {StringSet} */
125 var dependencies = {};
126
127 var entry = entryMap[id];
128 if (entry) {
129 /**
130 * This will add the direct dependency and all of its transitive dependencies to the set of
131 * dependencies of `entry`.
132 *
133 * @param {string} depId
134 * @returns {void}
135 */
136 function handleDirectDependency(depId) {
137 if (!(depId in entryMap)) {
138 throw new Error(id + ' depends on an unknown component ' + depId);
139 }
140 if (depId in dependencies) {
141 // if the given dependency is already in the set of deps, then so are its transitive deps
142 return;
143 }
144
145 addToMap(depId, stack);
146 dependencies[depId] = true;
147 for (var transitiveDepId in map[depId]) {
148 dependencies[transitiveDepId] = true;
149 }
150 }
151
152 forEach(entry.require, handleDirectDependency);
153 forEach(entry.optional, handleDirectDependency);
154 forEach(entry.modify, handleDirectDependency);
155 }
156
157 map[id] = dependencies;
158
159 stack.pop();
160 }
161
162 return function (id) {
163 var deps = map[id];
164 if (!deps) {
165 addToMap(id, _stackArray);
166 deps = map[id];
167 }
168 return deps;
169 };
170 }
171
172 /**
173 * Returns a function which resolves the aliases of its given id of alias.
174 *
175 * @param {EntryMap} entryMap
176 * @returns {(idOrAlias: string) => string}
177 */
178 function createAliasResolver(entryMap) {
179 /** @type {Object<string, string> | undefined} */
180 var map;
181
182 return function (idOrAlias) {
183 if (idOrAlias in entryMap) {
184 return idOrAlias;
185 } else {
186 // only create the alias map if necessary
187 if (!map) {
188 map = {};
189
190 for (var id in entryMap) {
191 var entry = entryMap[id];
192 forEach(entry && entry.alias, function (alias) {
193 if (alias in map) {
194 throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
195 }
196 if (alias in entryMap) {
197 throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
198 }
199 map[alias] = id;
200 });
201 }
202 }
203 return map[idOrAlias] || idOrAlias;
204 }
205 };
206 }
207
208 /**
209 * @typedef LoadChainer
210 * @property {(before: T, after: () => T) => T} series
211 * @property {(values: T[]) => T} parallel
212 * @template T
213 */
214
215 /**
216 * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
217 * component in topological order.
218 *
219 * @param {DependencyResolver} dependencyResolver
220 * @param {StringSet} ids
221 * @param {(id: string) => T} loadComponent
222 * @param {LoadChainer<T>} [chainer]
223 * @returns {T}
224 * @template T
225 */
226 function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {
227 var series = chainer ? chainer.series : undefined;
228 var parallel = chainer ? chainer.parallel : noop;
229
230 /** @type {Object<string, T>} */
231 var cache = {};
232
233 /**
234 * A set of ids of nodes which are not depended upon by any other node in the graph.
235 *
236 * @type {StringSet}
237 */
238 var ends = {};
239
240 /**
241 * Loads the given component and its dependencies or returns the cached value.
242 *
243 * @param {string} id
244 * @returns {T}
245 */
246 function handleId(id) {
247 if (id in cache) {
248 return cache[id];
249 }
250
251 // assume that it's an end
252 // if it isn't, it will be removed later
253 ends[id] = true;
254
255 // all dependencies of the component in the given ids
256 var dependsOn = [];
257 for (var depId in dependencyResolver(id)) {
258 if (depId in ids) {
259 dependsOn.push(depId);
260 }
261 }
262
263 /**
264 * The value to be returned.
265 *
266 * @type {T}
267 */
268 var value;
269
270 if (dependsOn.length === 0) {
271 value = loadComponent(id);
272 } else {
273 var depsValue = parallel(dependsOn.map(function (depId) {
274 var value = handleId(depId);
275 // none of the dependencies can be ends
276 delete ends[depId];
277 return value;
278 }));
279 if (series) {
280 // the chainer will be responsibly for calling the function calling loadComponent
281 value = series(depsValue, function () { return loadComponent(id); });
282 } else {
283 // we don't have a chainer, so we call loadComponent ourselves
284 loadComponent(id);
285 }
286 }
287
288 // cache and return
289 return cache[id] = value;
290 }
291
292 for (var id in ids) {
293 handleId(id);
294 }
295
296 /** @type {T[]} */
297 var endValues = [];
298 for (var endId in ends) {
299 endValues.push(cache[endId]);
300 }
301 return parallel(endValues);
302 }
303
304 /**
305 * Returns whether the given object has any keys.
306 *
307 * @param {object} obj
308 */
309 function hasKeys(obj) {
310 for (var key in obj) {
311 return true;
312 }
313 return false;
314 }
315
316 /**
317 * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
318 * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
319 *
320 * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
321 * components will have to reloaded.
322 *
323 * The ids in `load` and `loaded` may be in any order and can contain duplicates.
324 *
325 * @param {Components} components
326 * @param {string[]} load
327 * @param {string[]} [loaded=[]] A list of already loaded components.
328 *
329 * If a component is in this list, then all of its requirements will also be assumed to be in the list.
330 * @returns {Loader}
331 *
332 * @typedef Loader
333 * @property {() => string[]} getIds A function to get all ids of the components to load.
334 *
335 * The returned ids will be duplicate-free, alias-free and in load order.
336 * @property {LoadFunction} load A functional interface to load components.
337 *
338 * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
339 * A functional interface to load components.
340 *
341 * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
342 *
343 * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
344 * `Promise#then` and `Promise.all`.
345 *
346 * @example
347 * load(id => { loadComponent(id); }); // returns undefined
348 *
349 * await load(
350 * id => loadComponentAsync(id), // returns a Promise for each id
351 * {
352 * series: async (before, after) => {
353 * await before;
354 * await after();
355 * },
356 * parallel: async (values) => {
357 * await Promise.all(values);
358 * }
359 * }
360 * );
361 */
362 function getLoader(components, load, loaded) {
363 var entryMap = createEntryMap(components);
364 var resolveAlias = createAliasResolver(entryMap);
365
366 load = load.map(resolveAlias);
367 loaded = (loaded || []).map(resolveAlias);
368
369 var loadSet = toSet(load);
370 var loadedSet = toSet(loaded);
371
372 // add requirements
373
374 load.forEach(addRequirements);
375 function addRequirements(id) {
376 var entry = entryMap[id];
377 forEach(entry && entry.require, function (reqId) {
378 if (!(reqId in loadedSet)) {
379 loadSet[reqId] = true;
380 addRequirements(reqId);
381 }
382 });
383 }
384
385 // add components to reload
386
387 // A component x in `loaded` has to be reloaded if
388 // 1) a component in `load` modifies x.
389 // 2) x depends on a component in `load`.
390 // The above two condition have to be applied until nothing changes anymore.
391
392 var dependencyResolver = createDependencyResolver(entryMap);
393
394 /** @type {StringSet} */
395 var loadAdditions = loadSet;
396 /** @type {StringSet} */
397 var newIds;
398 while (hasKeys(loadAdditions)) {
399 newIds = {};
400
401 // condition 1)
402 for (var loadId in loadAdditions) {
403 var entry = entryMap[loadId];
404 forEach(entry && entry.modify, function (modId) {
405 if (modId in loadedSet) {
406 newIds[modId] = true;
407 }
408 });
409 }
410
411 // condition 2)
412 for (var loadedId in loadedSet) {
413 if (!(loadedId in loadSet)) {
414 for (var depId in dependencyResolver(loadedId)) {
415 if (depId in loadSet) {
416 newIds[loadedId] = true;
417 break;
418 }
419 }
420 }
421 }
422
423 loadAdditions = newIds;
424 for (var newId in loadAdditions) {
425 loadSet[newId] = true;
426 }
427 }
428
429 /** @type {Loader} */
430 var loader = {
431 getIds: function () {
432 var ids = [];
433 loader.load(function (id) {
434 ids.push(id);
435 });
436 return ids;
437 },
438 load: function (loadComponent, chainer) {
439 return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
440 }
441 };
442
443 return loader;
444 }
445
446 return getLoader;
447
448}());
449
450if (typeof module !== 'undefined') {
451 module.exports = getLoader;
452}