"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) { const series = chainer ? chainer.series : undefined; const 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; }