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 const series = chainer ? chainer.series : undefined;
228 const 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 * @type {StringSet}
236 */
237 var ends = {};
238
239 /**
240 * Loads the given component and its dependencies or returns the cached value.
241 *
242 * @param {string} id
243 * @returns {T}
244 */
245 function handleId(id) {
246 if (id in cache) {
247 return cache[id];
248 }
249
250 // assume that it's an end
251 // if it isn't, it will be removed later
252 ends[id] = true;
253
254 // all dependencies of the component in the given ids
255 var dependsOn = [];
256 for (var depId in dependencyResolver(id)) {
257 if (depId in ids) {
258 dependsOn.push(depId);
259 }
260 }
261
262 /**
263 * The value to be returned.
264 * @type {T}
265 */
266 var value;
267
268 if (dependsOn.length === 0) {
269 value = loadComponent(id);
270 } else {
271 var depsValue = parallel(dependsOn.map(function (depId) {
272 var value = handleId(depId);
273 // none of the dependencies can be ends
274 delete ends[depId];
275 return value;
276 }));
277 if (series) {
278 // the chainer will be responsibly for calling the function calling loadComponent
279 value = series(depsValue, function () { return loadComponent(id); });
280 } else {
281 // we don't have a chainer, so we call loadComponent ourselves
282 loadComponent(id);
283 }
284 }
285
286 // cache and return
287 return cache[id] = value;
288 }
289
290 for (var id in ids) {
291 handleId(id);
292 }
293
294 /** @type {T[]} */
295 var endValues = [];
296 for (var endId in ends) {
297 endValues.push(cache[endId]);
298 }
299 return parallel(endValues);
300 }
301
302 /**
303 * Returns whether the given object has any keys.
304 *
305 * @param {object} obj
306 */
307 function hasKeys(obj) {
308 for (var key in obj) {
309 return true;
310 }
311 return false;
312 }
313
314 /**
315 * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
316 * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
317 *
318 * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
319 * components will have to reloaded.
320 *
321 * The ids in `load` and `loaded` may be in any order and can contain duplicates.
322 *
323 * @param {Components} components
324 * @param {string[]} load
325 * @param {string[]} [loaded=[]] A list of already loaded components.
326 *
327 * If a component is in this list, then all of its requirements will also be assumed to be in the list.
328 * @returns {Loader}
329 *
330 * @typedef Loader
331 * @property {() => string[]} getIds A function to get all ids of the components to load.
332 *
333 * The returned ids will be duplicate-free, alias-free and in load order.
334 * @property {LoadFunction} load A functional interface to load components.
335 *
336 * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
337 * A functional interface to load components.
338 *
339 * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
340 *
341 * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
342 * `Promise#then` and `Promise.all`.
343 *
344 * @example
345 * load(id => { loadComponent(id); }); // returns undefined
346 *
347 * await load(
348 * id => loadComponentAsync(id), // returns a Promise for each id
349 * {
350 * series: async (before, after) => {
351 * await before;
352 * await after();
353 * },
354 * parallel: async (values) => {
355 * await Promise.all(values);
356 * }
357 * }
358 * );
359 */
360 function getLoader(components, load, loaded) {
361 var entryMap = createEntryMap(components);
362 var resolveAlias = createAliasResolver(entryMap);
363
364 load = load.map(resolveAlias);
365 loaded = (loaded || []).map(resolveAlias);
366
367 var loadSet = toSet(load);
368 var loadedSet = toSet(loaded);
369
370 // add requirements
371
372 load.forEach(addRequirements);
373 function addRequirements(id) {
374 var entry = entryMap[id];
375 forEach(entry && entry.require, function (reqId) {
376 if (!(reqId in loadedSet)) {
377 loadSet[reqId] = true;
378 addRequirements(reqId);
379 }
380 });
381 }
382
383 // add components to reload
384
385 // A component x in `loaded` has to be reloaded if
386 // 1) a component in `load` modifies x.
387 // 2) x depends on a component in `load`.
388 // The above two condition have to be applied until nothing changes anymore.
389
390 var dependencyResolver = createDependencyResolver(entryMap);
391
392 /** @type {StringSet} */
393 var loadAdditions = loadSet;
394 /** @type {StringSet} */
395 var newIds;
396 while (hasKeys(loadAdditions)) {
397 newIds = {};
398
399 // condition 1)
400 for (var loadId in loadAdditions) {
401 var entry = entryMap[loadId];
402 forEach(entry && entry.modify, function (modId) {
403 if (modId in loadedSet) {
404 newIds[modId] = true;
405 }
406 });
407 }
408
409 // condition 2)
410 for (var loadedId in loadedSet) {
411 if (!(loadedId in loadSet)) {
412 for (var depId in dependencyResolver(loadedId)) {
413 if (depId in loadSet) {
414 newIds[loadedId] = true;
415 break;
416 }
417 }
418 }
419 }
420
421 loadAdditions = newIds;
422 for (var newId in loadAdditions) {
423 loadSet[newId] = true;
424 }
425 }
426
427 /** @type {Loader} */
428 var loader = {
429 getIds: function () {
430 var ids = [];
431 loader.load(function (id) {
432 ids.push(id);
433 });
434 return ids;
435 },
436 load: function (loadComponent, chainer) {
437 return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
438 }
439 };
440
441 return loader;
442 }
443
444 return getLoader;
445
446}());
447
448if (typeof module !== 'undefined') {
449 module.exports = getLoader;
450}