123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- '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;
- }
|