dependencies.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. 'use strict';
  2. /**
  3. * @typedef {Object<string, ComponentCategory>} Components
  4. * @typedef {Object<string, ComponentEntry | string>} ComponentCategory
  5. *
  6. * @typedef ComponentEntry
  7. * @property {string} [title] The title of the component.
  8. * @property {string} [owner] The GitHub user name of the owner.
  9. * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
  10. * @property {string | string[]} [alias] An optional list of aliases for the id of the component.
  11. * @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
  12. *
  13. * Aliases which are not in this map will the get title of the component.
  14. * @property {string | string[]} [optional]
  15. * @property {string | string[]} [require]
  16. * @property {string | string[]} [modify]
  17. */
  18. var getLoader = (function () {
  19. /**
  20. * A function which does absolutely nothing.
  21. *
  22. * @type {any}
  23. */
  24. var noop = function () { };
  25. /**
  26. * Invokes the given callback for all elements of the given value.
  27. *
  28. * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or
  29. * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given
  30. * value as parameter.
  31. *
  32. * @param {null | undefined | T | T[]} value
  33. * @param {(value: T, index: number) => void} callbackFn
  34. * @returns {void}
  35. * @template T
  36. */
  37. function forEach(value, callbackFn) {
  38. if (Array.isArray(value)) {
  39. value.forEach(callbackFn);
  40. } else if (value != null) {
  41. callbackFn(value, 0);
  42. }
  43. }
  44. /**
  45. * Returns a new set for the given string array.
  46. *
  47. * @param {string[]} array
  48. * @returns {StringSet}
  49. *
  50. * @typedef {Object<string, true>} StringSet
  51. */
  52. function toSet(array) {
  53. /** @type {StringSet} */
  54. var set = {};
  55. for (var i = 0, l = array.length; i < l; i++) {
  56. set[array[i]] = true;
  57. }
  58. return set;
  59. }
  60. /**
  61. * Creates a map of every components id to its entry.
  62. *
  63. * @param {Components} components
  64. * @returns {EntryMap}
  65. *
  66. * @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
  67. */
  68. function createEntryMap(components) {
  69. /** @type {Object<string, Readonly<ComponentEntry>>} */
  70. var map = {};
  71. for (var categoryName in components) {
  72. var category = components[categoryName];
  73. for (var id in category) {
  74. if (id != 'meta') {
  75. /** @type {ComponentEntry | string} */
  76. var entry = category[id];
  77. map[id] = typeof entry == 'string' ? { title: entry } : entry;
  78. }
  79. }
  80. }
  81. return map;
  82. }
  83. /**
  84. * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
  85. *
  86. * @param {EntryMap} entryMap
  87. * @returns {DependencyResolver}
  88. *
  89. * @typedef {(id: string) => StringSet} DependencyResolver
  90. */
  91. function createDependencyResolver(entryMap) {
  92. /** @type {Object<string, StringSet>} */
  93. var map = {};
  94. var _stackArray = [];
  95. /**
  96. * Adds the dependencies of the given component to the dependency map.
  97. *
  98. * @param {string} id
  99. * @param {string[]} stack
  100. */
  101. function addToMap(id, stack) {
  102. if (id in map) {
  103. return;
  104. }
  105. stack.push(id);
  106. // check for circular dependencies
  107. var firstIndex = stack.indexOf(id);
  108. if (firstIndex < stack.length - 1) {
  109. throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
  110. }
  111. /** @type {StringSet} */
  112. var dependencies = {};
  113. var entry = entryMap[id];
  114. if (entry) {
  115. /**
  116. * This will add the direct dependency and all of its transitive dependencies to the set of
  117. * dependencies of `entry`.
  118. *
  119. * @param {string} depId
  120. * @returns {void}
  121. */
  122. function handleDirectDependency(depId) {
  123. if (!(depId in entryMap)) {
  124. throw new Error(id + ' depends on an unknown component ' + depId);
  125. }
  126. if (depId in dependencies) {
  127. // if the given dependency is already in the set of deps, then so are its transitive deps
  128. return;
  129. }
  130. addToMap(depId, stack);
  131. dependencies[depId] = true;
  132. for (var transitiveDepId in map[depId]) {
  133. dependencies[transitiveDepId] = true;
  134. }
  135. }
  136. forEach(entry.require, handleDirectDependency);
  137. forEach(entry.optional, handleDirectDependency);
  138. forEach(entry.modify, handleDirectDependency);
  139. }
  140. map[id] = dependencies;
  141. stack.pop();
  142. }
  143. return function (id) {
  144. var deps = map[id];
  145. if (!deps) {
  146. addToMap(id, _stackArray);
  147. deps = map[id];
  148. }
  149. return deps;
  150. };
  151. }
  152. /**
  153. * Returns a function which resolves the aliases of its given id of alias.
  154. *
  155. * @param {EntryMap} entryMap
  156. * @returns {(idOrAlias: string) => string}
  157. */
  158. function createAliasResolver(entryMap) {
  159. /** @type {Object<string, string> | undefined} */
  160. var map;
  161. return function (idOrAlias) {
  162. if (idOrAlias in entryMap) {
  163. return idOrAlias;
  164. } else {
  165. // only create the alias map if necessary
  166. if (!map) {
  167. map = {};
  168. for (var id in entryMap) {
  169. var entry = entryMap[id];
  170. forEach(entry && entry.alias, function (alias) {
  171. if (alias in map) {
  172. throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
  173. }
  174. if (alias in entryMap) {
  175. throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
  176. }
  177. map[alias] = id;
  178. });
  179. }
  180. }
  181. return map[idOrAlias] || idOrAlias;
  182. }
  183. };
  184. }
  185. /**
  186. * @typedef LoadChainer
  187. * @property {(before: T, after: () => T) => T} series
  188. * @property {(values: T[]) => T} parallel
  189. * @template T
  190. */
  191. /**
  192. * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
  193. * component in topological order.
  194. *
  195. * @param {DependencyResolver} dependencyResolver
  196. * @param {StringSet} ids
  197. * @param {(id: string) => T} loadComponent
  198. * @param {LoadChainer<T>} [chainer]
  199. * @returns {T}
  200. * @template T
  201. */
  202. function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {
  203. var series = chainer ? chainer.series : undefined;
  204. var parallel = chainer ? chainer.parallel : noop;
  205. /** @type {Object<string, T>} */
  206. var cache = {};
  207. /**
  208. * A set of ids of nodes which are not depended upon by any other node in the graph.
  209. *
  210. * @type {StringSet}
  211. */
  212. var ends = {};
  213. /**
  214. * Loads the given component and its dependencies or returns the cached value.
  215. *
  216. * @param {string} id
  217. * @returns {T}
  218. */
  219. function handleId(id) {
  220. if (id in cache) {
  221. return cache[id];
  222. }
  223. // assume that it's an end
  224. // if it isn't, it will be removed later
  225. ends[id] = true;
  226. // all dependencies of the component in the given ids
  227. var dependsOn = [];
  228. for (var depId in dependencyResolver(id)) {
  229. if (depId in ids) {
  230. dependsOn.push(depId);
  231. }
  232. }
  233. /**
  234. * The value to be returned.
  235. *
  236. * @type {T}
  237. */
  238. var value;
  239. if (dependsOn.length === 0) {
  240. value = loadComponent(id);
  241. } else {
  242. var depsValue = parallel(dependsOn.map(function (depId) {
  243. var value = handleId(depId);
  244. // none of the dependencies can be ends
  245. delete ends[depId];
  246. return value;
  247. }));
  248. if (series) {
  249. // the chainer will be responsibly for calling the function calling loadComponent
  250. value = series(depsValue, function () { return loadComponent(id); });
  251. } else {
  252. // we don't have a chainer, so we call loadComponent ourselves
  253. loadComponent(id);
  254. }
  255. }
  256. // cache and return
  257. return cache[id] = value;
  258. }
  259. for (var id in ids) {
  260. handleId(id);
  261. }
  262. /** @type {T[]} */
  263. var endValues = [];
  264. for (var endId in ends) {
  265. endValues.push(cache[endId]);
  266. }
  267. return parallel(endValues);
  268. }
  269. /**
  270. * Returns whether the given object has any keys.
  271. *
  272. * @param {object} obj
  273. */
  274. function hasKeys(obj) {
  275. for (var key in obj) {
  276. return true;
  277. }
  278. return false;
  279. }
  280. /**
  281. * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
  282. * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
  283. *
  284. * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
  285. * components will have to reloaded.
  286. *
  287. * The ids in `load` and `loaded` may be in any order and can contain duplicates.
  288. *
  289. * @param {Components} components
  290. * @param {string[]} load
  291. * @param {string[]} [loaded=[]] A list of already loaded components.
  292. *
  293. * If a component is in this list, then all of its requirements will also be assumed to be in the list.
  294. * @returns {Loader}
  295. *
  296. * @typedef Loader
  297. * @property {() => string[]} getIds A function to get all ids of the components to load.
  298. *
  299. * The returned ids will be duplicate-free, alias-free and in load order.
  300. * @property {LoadFunction} load A functional interface to load components.
  301. *
  302. * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
  303. * A functional interface to load components.
  304. *
  305. * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
  306. *
  307. * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
  308. * `Promise#then` and `Promise.all`.
  309. *
  310. * @example
  311. * load(id => { loadComponent(id); }); // returns undefined
  312. *
  313. * await load(
  314. * id => loadComponentAsync(id), // returns a Promise for each id
  315. * {
  316. * series: async (before, after) => {
  317. * await before;
  318. * await after();
  319. * },
  320. * parallel: async (values) => {
  321. * await Promise.all(values);
  322. * }
  323. * }
  324. * );
  325. */
  326. function getLoader(components, load, loaded) {
  327. var entryMap = createEntryMap(components);
  328. var resolveAlias = createAliasResolver(entryMap);
  329. load = load.map(resolveAlias);
  330. loaded = (loaded || []).map(resolveAlias);
  331. var loadSet = toSet(load);
  332. var loadedSet = toSet(loaded);
  333. // add requirements
  334. load.forEach(addRequirements);
  335. function addRequirements(id) {
  336. var entry = entryMap[id];
  337. forEach(entry && entry.require, function (reqId) {
  338. if (!(reqId in loadedSet)) {
  339. loadSet[reqId] = true;
  340. addRequirements(reqId);
  341. }
  342. });
  343. }
  344. // add components to reload
  345. // A component x in `loaded` has to be reloaded if
  346. // 1) a component in `load` modifies x.
  347. // 2) x depends on a component in `load`.
  348. // The above two condition have to be applied until nothing changes anymore.
  349. var dependencyResolver = createDependencyResolver(entryMap);
  350. /** @type {StringSet} */
  351. var loadAdditions = loadSet;
  352. /** @type {StringSet} */
  353. var newIds;
  354. while (hasKeys(loadAdditions)) {
  355. newIds = {};
  356. // condition 1)
  357. for (var loadId in loadAdditions) {
  358. var entry = entryMap[loadId];
  359. forEach(entry && entry.modify, function (modId) {
  360. if (modId in loadedSet) {
  361. newIds[modId] = true;
  362. }
  363. });
  364. }
  365. // condition 2)
  366. for (var loadedId in loadedSet) {
  367. if (!(loadedId in loadSet)) {
  368. for (var depId in dependencyResolver(loadedId)) {
  369. if (depId in loadSet) {
  370. newIds[loadedId] = true;
  371. break;
  372. }
  373. }
  374. }
  375. }
  376. loadAdditions = newIds;
  377. for (var newId in loadAdditions) {
  378. loadSet[newId] = true;
  379. }
  380. }
  381. /** @type {Loader} */
  382. var loader = {
  383. getIds: function () {
  384. var ids = [];
  385. loader.load(function (id) {
  386. ids.push(id);
  387. });
  388. return ids;
  389. },
  390. load: function (loadComponent, chainer) {
  391. return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
  392. }
  393. };
  394. return loader;
  395. }
  396. return getLoader;
  397. }());
  398. if (typeof module !== 'undefined') {
  399. module.exports = getLoader;
  400. }