prism/dependencies.js

453 lines
12 KiB
JavaScript

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