lavkach3
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*/
27var 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*/
41function forEach(value, callbackFn) {42if (Array.isArray(value)) {43value.forEach(callbackFn);44} else if (value != null) {45callbackFn(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*/
57function toSet(array) {58/** @type {StringSet} */59var set = {};60for (var i = 0, l = array.length; i < l; i++) {61set[array[i]] = true;62}63return 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*/
74function createEntryMap(components) {75/** @type {Object<string, Readonly<ComponentEntry>>} */76var map = {};77
78for (var categoryName in components) {79var category = components[categoryName];80for (var id in category) {81if (id != 'meta') {82/** @type {ComponentEntry | string} */83var entry = category[id];84map[id] = typeof entry == 'string' ? { title: entry } : entry;85}86}87}88
89return 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*/
100function createDependencyResolver(entryMap) {101/** @type {Object<string, StringSet>} */102var map = {};103var _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*/
111function addToMap(id, stack) {112if (id in map) {113return;114}115
116stack.push(id);117
118// check for circular dependencies119var firstIndex = stack.indexOf(id);120if (firstIndex < stack.length - 1) {121throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));122}123
124/** @type {StringSet} */125var dependencies = {};126
127var entry = entryMap[id];128if (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*/
136function handleDirectDependency(depId) {137if (!(depId in entryMap)) {138throw new Error(id + ' depends on an unknown component ' + depId);139}140if (depId in dependencies) {141// if the given dependency is already in the set of deps, then so are its transitive deps142return;143}144
145addToMap(depId, stack);146dependencies[depId] = true;147for (var transitiveDepId in map[depId]) {148dependencies[transitiveDepId] = true;149}150}151
152forEach(entry.require, handleDirectDependency);153forEach(entry.optional, handleDirectDependency);154forEach(entry.modify, handleDirectDependency);155}156
157map[id] = dependencies;158
159stack.pop();160}161
162return function (id) {163var deps = map[id];164if (!deps) {165addToMap(id, _stackArray);166deps = map[id];167}168return 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*/
178function createAliasResolver(entryMap) {179/** @type {Object<string, string> | undefined} */180var map;181
182return function (idOrAlias) {183if (idOrAlias in entryMap) {184return idOrAlias;185} else {186// only create the alias map if necessary187if (!map) {188map = {};189
190for (var id in entryMap) {191var entry = entryMap[id];192forEach(entry && entry.alias, function (alias) {193if (alias in map) {194throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);195}196if (alias in entryMap) {197throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');198}199map[alias] = id;200});201}202}203return 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*/
226function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {227var series = chainer ? chainer.series : undefined;228var parallel = chainer ? chainer.parallel : noop;229
230/** @type {Object<string, T>} */231var 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*/
238var 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*/
246function handleId(id) {247if (id in cache) {248return cache[id];249}250
251// assume that it's an end252// if it isn't, it will be removed later253ends[id] = true;254
255// all dependencies of the component in the given ids256var dependsOn = [];257for (var depId in dependencyResolver(id)) {258if (depId in ids) {259dependsOn.push(depId);260}261}262
263/**264* The value to be returned.
265*
266* @type {T}
267*/
268var value;269
270if (dependsOn.length === 0) {271value = loadComponent(id);272} else {273var depsValue = parallel(dependsOn.map(function (depId) {274var value = handleId(depId);275// none of the dependencies can be ends276delete ends[depId];277return value;278}));279if (series) {280// the chainer will be responsibly for calling the function calling loadComponent281value = series(depsValue, function () { return loadComponent(id); });282} else {283// we don't have a chainer, so we call loadComponent ourselves284loadComponent(id);285}286}287
288// cache and return289return cache[id] = value;290}291
292for (var id in ids) {293handleId(id);294}295
296/** @type {T[]} */297var endValues = [];298for (var endId in ends) {299endValues.push(cache[endId]);300}301return parallel(endValues);302}303
304/**305* Returns whether the given object has any keys.
306*
307* @param {object} obj
308*/
309function hasKeys(obj) {310for (var key in obj) {311return true;312}313return 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*/
362function getLoader(components, load, loaded) {363var entryMap = createEntryMap(components);364var resolveAlias = createAliasResolver(entryMap);365
366load = load.map(resolveAlias);367loaded = (loaded || []).map(resolveAlias);368
369var loadSet = toSet(load);370var loadedSet = toSet(loaded);371
372// add requirements373
374load.forEach(addRequirements);375function addRequirements(id) {376var entry = entryMap[id];377forEach(entry && entry.require, function (reqId) {378if (!(reqId in loadedSet)) {379loadSet[reqId] = true;380addRequirements(reqId);381}382});383}384
385// add components to reload386
387// A component x in `loaded` has to be reloaded if388// 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
392var dependencyResolver = createDependencyResolver(entryMap);393
394/** @type {StringSet} */395var loadAdditions = loadSet;396/** @type {StringSet} */397var newIds;398while (hasKeys(loadAdditions)) {399newIds = {};400
401// condition 1)402for (var loadId in loadAdditions) {403var entry = entryMap[loadId];404forEach(entry && entry.modify, function (modId) {405if (modId in loadedSet) {406newIds[modId] = true;407}408});409}410
411// condition 2)412for (var loadedId in loadedSet) {413if (!(loadedId in loadSet)) {414for (var depId in dependencyResolver(loadedId)) {415if (depId in loadSet) {416newIds[loadedId] = true;417break;418}419}420}421}422
423loadAdditions = newIds;424for (var newId in loadAdditions) {425loadSet[newId] = true;426}427}428
429/** @type {Loader} */430var loader = {431getIds: function () {432var ids = [];433loader.load(function (id) {434ids.push(id);435});436return ids;437},438load: function (loadComponent, chainer) {439return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);440}441};442
443return loader;444}445
446return getLoader;447
448}());449
450if (typeof module !== 'undefined') {451module.exports = getLoader;452}
453