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.
452 lines
12 KiB
452 lines
12 KiB
'use strict'; |
|
|
|
/** |
|
* @typedef {Object<string, ComponentCategory>} Components |
|
* @typedef {Object<string, ComponentEntry | string>} ComponentCategory |
|
* |
|
* @typedef ComponentEntry |
|
* @property {string} [title] The title of the component. |
|
* @property {string} [owner] The GitHub user name of the owner. |
|
* @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded. |
|
* @property {string | string[]} [alias] An optional list of aliases for the id of the component. |
|
* @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title. |
|
* |
|
* Aliases which are not in this map will the get title of the component. |
|
* @property {string | string[]} [optional] |
|
* @property {string | string[]} [require] |
|
* @property {string | string[]} [modify] |
|
*/ |
|
|
|
var getLoader = (function () { |
|
|
|
/** |
|
* A function which does absolutely nothing. |
|
* |
|
* @type {any} |
|
*/ |
|
var noop = function () { }; |
|
|
|
/** |
|
* Invokes the given callback for all elements of the given value. |
|
* |
|
* If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or |
|
* `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given |
|
* value as parameter. |
|
* |
|
* @param {null | undefined | T | T[]} value |
|
* @param {(value: T, index: number) => void} callbackFn |
|
* @returns {void} |
|
* @template T |
|
*/ |
|
function forEach(value, callbackFn) { |
|
if (Array.isArray(value)) { |
|
value.forEach(callbackFn); |
|
} else if (value != null) { |
|
callbackFn(value, 0); |
|
} |
|
} |
|
|
|
/** |
|
* Returns a new set for the given string array. |
|
* |
|
* @param {string[]} array |
|
* @returns {StringSet} |
|
* |
|
* @typedef {Object<string, true>} StringSet |
|
*/ |
|
function toSet(array) { |
|
/** @type {StringSet} */ |
|
var set = {}; |
|
for (var i = 0, l = array.length; i < l; i++) { |
|
set[array[i]] = true; |
|
} |
|
return set; |
|
} |
|
|
|
/** |
|
* Creates a map of every components id to its entry. |
|
* |
|
* @param {Components} components |
|
* @returns {EntryMap} |
|
* |
|
* @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap |
|
*/ |
|
function createEntryMap(components) { |
|
/** @type {Object<string, Readonly<ComponentEntry>>} */ |
|
var map = {}; |
|
|
|
for (var categoryName in components) { |
|
var category = components[categoryName]; |
|
for (var id in category) { |
|
if (id != 'meta') { |
|
/** @type {ComponentEntry | string} */ |
|
var entry = category[id]; |
|
map[id] = typeof entry == 'string' ? { title: entry } : entry; |
|
} |
|
} |
|
} |
|
|
|
return map; |
|
} |
|
|
|
/** |
|
* Creates a full dependencies map which includes all types of dependencies and their transitive dependencies. |
|
* |
|
* @param {EntryMap} entryMap |
|
* @returns {DependencyResolver} |
|
* |
|
* @typedef {(id: string) => StringSet} DependencyResolver |
|
*/ |
|
function createDependencyResolver(entryMap) { |
|
/** @type {Object<string, StringSet>} */ |
|
var map = {}; |
|
var _stackArray = []; |
|
|
|
/** |
|
* Adds the dependencies of the given component to the dependency map. |
|
* |
|
* @param {string} id |
|
* @param {string[]} stack |
|
*/ |
|
function addToMap(id, stack) { |
|
if (id in map) { |
|
return; |
|
} |
|
|
|
stack.push(id); |
|
|
|
// check for circular dependencies |
|
var firstIndex = stack.indexOf(id); |
|
if (firstIndex < stack.length - 1) { |
|
throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> ')); |
|
} |
|
|
|
/** @type {StringSet} */ |
|
var dependencies = {}; |
|
|
|
var entry = entryMap[id]; |
|
if (entry) { |
|
/** |
|
* This will add the direct dependency and all of its transitive dependencies to the set of |
|
* dependencies of `entry`. |
|
* |
|
* @param {string} depId |
|
* @returns {void} |
|
*/ |
|
function handleDirectDependency(depId) { |
|
if (!(depId in entryMap)) { |
|
throw new Error(id + ' depends on an unknown component ' + depId); |
|
} |
|
if (depId in dependencies) { |
|
// if the given dependency is already in the set of deps, then so are its transitive deps |
|
return; |
|
} |
|
|
|
addToMap(depId, stack); |
|
dependencies[depId] = true; |
|
for (var transitiveDepId in map[depId]) { |
|
dependencies[transitiveDepId] = true; |
|
} |
|
} |
|
|
|
forEach(entry.require, handleDirectDependency); |
|
forEach(entry.optional, handleDirectDependency); |
|
forEach(entry.modify, handleDirectDependency); |
|
} |
|
|
|
map[id] = dependencies; |
|
|
|
stack.pop(); |
|
} |
|
|
|
return function (id) { |
|
var deps = map[id]; |
|
if (!deps) { |
|
addToMap(id, _stackArray); |
|
deps = map[id]; |
|
} |
|
return deps; |
|
}; |
|
} |
|
|
|
/** |
|
* Returns a function which resolves the aliases of its given id of alias. |
|
* |
|
* @param {EntryMap} entryMap |
|
* @returns {(idOrAlias: string) => string} |
|
*/ |
|
function createAliasResolver(entryMap) { |
|
/** @type {Object<string, string> | undefined} */ |
|
var map; |
|
|
|
return function (idOrAlias) { |
|
if (idOrAlias in entryMap) { |
|
return idOrAlias; |
|
} else { |
|
// only create the alias map if necessary |
|
if (!map) { |
|
map = {}; |
|
|
|
for (var id in entryMap) { |
|
var entry = entryMap[id]; |
|
forEach(entry && entry.alias, function (alias) { |
|
if (alias in map) { |
|
throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]); |
|
} |
|
if (alias in entryMap) { |
|
throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.'); |
|
} |
|
map[alias] = id; |
|
}); |
|
} |
|
} |
|
return map[idOrAlias] || idOrAlias; |
|
} |
|
}; |
|
} |
|
|
|
/** |
|
* @typedef LoadChainer |
|
* @property {(before: T, after: () => T) => T} series |
|
* @property {(values: T[]) => T} parallel |
|
* @template T |
|
*/ |
|
|
|
/** |
|
* Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each |
|
* component in topological order. |
|
* |
|
* @param {DependencyResolver} dependencyResolver |
|
* @param {StringSet} ids |
|
* @param {(id: string) => T} loadComponent |
|
* @param {LoadChainer<T>} [chainer] |
|
* @returns {T} |
|
* @template T |
|
*/ |
|
function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) { |
|
var series = chainer ? chainer.series : undefined; |
|
var parallel = chainer ? chainer.parallel : noop; |
|
|
|
/** @type {Object<string, T>} */ |
|
var cache = {}; |
|
|
|
/** |
|
* A set of ids of nodes which are not depended upon by any other node in the graph. |
|
* |
|
* @type {StringSet} |
|
*/ |
|
var ends = {}; |
|
|
|
/** |
|
* Loads the given component and its dependencies or returns the cached value. |
|
* |
|
* @param {string} id |
|
* @returns {T} |
|
*/ |
|
function handleId(id) { |
|
if (id in cache) { |
|
return cache[id]; |
|
} |
|
|
|
// assume that it's an end |
|
// if it isn't, it will be removed later |
|
ends[id] = true; |
|
|
|
// all dependencies of the component in the given ids |
|
var dependsOn = []; |
|
for (var depId in dependencyResolver(id)) { |
|
if (depId in ids) { |
|
dependsOn.push(depId); |
|
} |
|
} |
|
|
|
/** |
|
* The value to be returned. |
|
* |
|
* @type {T} |
|
*/ |
|
var value; |
|
|
|
if (dependsOn.length === 0) { |
|
value = loadComponent(id); |
|
} else { |
|
var depsValue = parallel(dependsOn.map(function (depId) { |
|
var value = handleId(depId); |
|
// none of the dependencies can be ends |
|
delete ends[depId]; |
|
return value; |
|
})); |
|
if (series) { |
|
// the chainer will be responsibly for calling the function calling loadComponent |
|
value = series(depsValue, function () { return loadComponent(id); }); |
|
} else { |
|
// we don't have a chainer, so we call loadComponent ourselves |
|
loadComponent(id); |
|
} |
|
} |
|
|
|
// cache and return |
|
return cache[id] = value; |
|
} |
|
|
|
for (var id in ids) { |
|
handleId(id); |
|
} |
|
|
|
/** @type {T[]} */ |
|
var endValues = []; |
|
for (var endId in ends) { |
|
endValues.push(cache[endId]); |
|
} |
|
return parallel(endValues); |
|
} |
|
|
|
/** |
|
* Returns whether the given object has any keys. |
|
* |
|
* @param {object} obj |
|
*/ |
|
function hasKeys(obj) { |
|
for (var key in obj) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and |
|
* a way to efficiently load them in synchronously and asynchronous contexts (`load`). |
|
* |
|
* The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding |
|
* components will have to reloaded. |
|
* |
|
* The ids in `load` and `loaded` may be in any order and can contain duplicates. |
|
* |
|
* @param {Components} components |
|
* @param {string[]} load |
|
* @param {string[]} [loaded=[]] A list of already loaded components. |
|
* |
|
* If a component is in this list, then all of its requirements will also be assumed to be in the list. |
|
* @returns {Loader} |
|
* |
|
* @typedef Loader |
|
* @property {() => string[]} getIds A function to get all ids of the components to load. |
|
* |
|
* The returned ids will be duplicate-free, alias-free and in load order. |
|
* @property {LoadFunction} load A functional interface to load components. |
|
* |
|
* @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction |
|
* A functional interface to load components. |
|
* |
|
* The `loadComponent` function will be called for every component in the order in which they have to be loaded. |
|
* |
|
* The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as |
|
* `Promise#then` and `Promise.all`. |
|
* |
|
* @example |
|
* load(id => { loadComponent(id); }); // returns undefined |
|
* |
|
* await load( |
|
* id => loadComponentAsync(id), // returns a Promise for each id |
|
* { |
|
* series: async (before, after) => { |
|
* await before; |
|
* await after(); |
|
* }, |
|
* parallel: async (values) => { |
|
* await Promise.all(values); |
|
* } |
|
* } |
|
* ); |
|
*/ |
|
function getLoader(components, load, loaded) { |
|
var entryMap = createEntryMap(components); |
|
var resolveAlias = createAliasResolver(entryMap); |
|
|
|
load = load.map(resolveAlias); |
|
loaded = (loaded || []).map(resolveAlias); |
|
|
|
var loadSet = toSet(load); |
|
var loadedSet = toSet(loaded); |
|
|
|
// add requirements |
|
|
|
load.forEach(addRequirements); |
|
function addRequirements(id) { |
|
var entry = entryMap[id]; |
|
forEach(entry && entry.require, function (reqId) { |
|
if (!(reqId in loadedSet)) { |
|
loadSet[reqId] = true; |
|
addRequirements(reqId); |
|
} |
|
}); |
|
} |
|
|
|
// add components to reload |
|
|
|
// A component x in `loaded` has to be reloaded if |
|
// 1) a component in `load` modifies x. |
|
// 2) x depends on a component in `load`. |
|
// The above two condition have to be applied until nothing changes anymore. |
|
|
|
var dependencyResolver = createDependencyResolver(entryMap); |
|
|
|
/** @type {StringSet} */ |
|
var loadAdditions = loadSet; |
|
/** @type {StringSet} */ |
|
var newIds; |
|
while (hasKeys(loadAdditions)) { |
|
newIds = {}; |
|
|
|
// condition 1) |
|
for (var loadId in loadAdditions) { |
|
var entry = entryMap[loadId]; |
|
forEach(entry && entry.modify, function (modId) { |
|
if (modId in loadedSet) { |
|
newIds[modId] = true; |
|
} |
|
}); |
|
} |
|
|
|
// condition 2) |
|
for (var loadedId in loadedSet) { |
|
if (!(loadedId in loadSet)) { |
|
for (var depId in dependencyResolver(loadedId)) { |
|
if (depId in loadSet) { |
|
newIds[loadedId] = true; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
loadAdditions = newIds; |
|
for (var newId in loadAdditions) { |
|
loadSet[newId] = true; |
|
} |
|
} |
|
|
|
/** @type {Loader} */ |
|
var loader = { |
|
getIds: function () { |
|
var ids = []; |
|
loader.load(function (id) { |
|
ids.push(id); |
|
}); |
|
return ids; |
|
}, |
|
load: function (loadComponent, chainer) { |
|
return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer); |
|
} |
|
}; |
|
|
|
return loader; |
|
} |
|
|
|
return getLoader; |
|
|
|
}()); |
|
|
|
if (typeof module !== 'undefined') { |
|
module.exports = getLoader; |
|
}
|
|
|