lavkach3

Форк
0
452 строки · 11.5 Кб
1
'use strict';
2

3
/**
4
 * @typedef {Object<string, ComponentCategory>} Components
5
 * @typedef {Object<string, ComponentEntry | string>} ComponentCategory
6
 *
7
 * @typedef ComponentEntry
8
 * @property {string} [title] The title of the component.
9
 * @property {string} [owner] The GitHub user name of the owner.
10
 * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
11
 * @property {string | string[]} [alias] An optional list of aliases for the id of the component.
12
 * @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
13
 *
14
 * Aliases which are not in this map will the get title of the component.
15
 * @property {string | string[]} [optional]
16
 * @property {string | string[]} [require]
17
 * @property {string | string[]} [modify]
18
 */
19

20
var getLoader = (function () {
21

22
	/**
23
	 * A function which does absolutely nothing.
24
	 *
25
	 * @type {any}
26
	 */
27
	var noop = function () { };
28

29
	/**
30
	 * Invokes the given callback for all elements of the given value.
31
	 *
32
	 * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or
33
	 * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given
34
	 * value as parameter.
35
	 *
36
	 * @param {null | undefined | T | T[]} value
37
	 * @param {(value: T, index: number) => void} callbackFn
38
	 * @returns {void}
39
	 * @template T
40
	 */
41
	function forEach(value, callbackFn) {
42
		if (Array.isArray(value)) {
43
			value.forEach(callbackFn);
44
		} else if (value != null) {
45
			callbackFn(value, 0);
46
		}
47
	}
48

49
	/**
50
	 * Returns a new set for the given string array.
51
	 *
52
	 * @param {string[]} array
53
	 * @returns {StringSet}
54
	 *
55
	 * @typedef {Object<string, true>} StringSet
56
	 */
57
	function toSet(array) {
58
		/** @type {StringSet} */
59
		var set = {};
60
		for (var i = 0, l = array.length; i < l; i++) {
61
			set[array[i]] = true;
62
		}
63
		return set;
64
	}
65

66
	/**
67
	 * Creates a map of every components id to its entry.
68
	 *
69
	 * @param {Components} components
70
	 * @returns {EntryMap}
71
	 *
72
	 * @typedef {{ readonly [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
73
	 */
74
	function createEntryMap(components) {
75
		/** @type {Object<string, Readonly<ComponentEntry>>} */
76
		var map = {};
77

78
		for (var categoryName in components) {
79
			var category = components[categoryName];
80
			for (var id in category) {
81
				if (id != 'meta') {
82
					/** @type {ComponentEntry | string} */
83
					var entry = category[id];
84
					map[id] = typeof entry == 'string' ? { title: entry } : entry;
85
				}
86
			}
87
		}
88

89
		return map;
90
	}
91

92
	/**
93
	 * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
94
	 *
95
	 * @param {EntryMap} entryMap
96
	 * @returns {DependencyResolver}
97
	 *
98
	 * @typedef {(id: string) => StringSet} DependencyResolver
99
	 */
100
	function createDependencyResolver(entryMap) {
101
		/** @type {Object<string, StringSet>} */
102
		var map = {};
103
		var _stackArray = [];
104

105
		/**
106
		 * Adds the dependencies of the given component to the dependency map.
107
		 *
108
		 * @param {string} id
109
		 * @param {string[]} stack
110
		 */
111
		function addToMap(id, stack) {
112
			if (id in map) {
113
				return;
114
			}
115

116
			stack.push(id);
117

118
			// check for circular dependencies
119
			var firstIndex = stack.indexOf(id);
120
			if (firstIndex < stack.length - 1) {
121
				throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
122
			}
123

124
			/** @type {StringSet} */
125
			var dependencies = {};
126

127
			var entry = entryMap[id];
128
			if (entry) {
129
				/**
130
				 * This will add the direct dependency and all of its transitive dependencies to the set of
131
				 * dependencies of `entry`.
132
				 *
133
				 * @param {string} depId
134
				 * @returns {void}
135
				 */
136
				function handleDirectDependency(depId) {
137
					if (!(depId in entryMap)) {
138
						throw new Error(id + ' depends on an unknown component ' + depId);
139
					}
140
					if (depId in dependencies) {
141
						// if the given dependency is already in the set of deps, then so are its transitive deps
142
						return;
143
					}
144

145
					addToMap(depId, stack);
146
					dependencies[depId] = true;
147
					for (var transitiveDepId in map[depId]) {
148
						dependencies[transitiveDepId] = true;
149
					}
150
				}
151

152
				forEach(entry.require, handleDirectDependency);
153
				forEach(entry.optional, handleDirectDependency);
154
				forEach(entry.modify, handleDirectDependency);
155
			}
156

157
			map[id] = dependencies;
158

159
			stack.pop();
160
		}
161

162
		return function (id) {
163
			var deps = map[id];
164
			if (!deps) {
165
				addToMap(id, _stackArray);
166
				deps = map[id];
167
			}
168
			return deps;
169
		};
170
	}
171

172
	/**
173
	 * Returns a function which resolves the aliases of its given id of alias.
174
	 *
175
	 * @param {EntryMap} entryMap
176
	 * @returns {(idOrAlias: string) => string}
177
	 */
178
	function createAliasResolver(entryMap) {
179
		/** @type {Object<string, string> | undefined} */
180
		var map;
181

182
		return function (idOrAlias) {
183
			if (idOrAlias in entryMap) {
184
				return idOrAlias;
185
			} else {
186
				// only create the alias map if necessary
187
				if (!map) {
188
					map = {};
189

190
					for (var id in entryMap) {
191
						var entry = entryMap[id];
192
						forEach(entry && entry.alias, function (alias) {
193
							if (alias in map) {
194
								throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
195
							}
196
							if (alias in entryMap) {
197
								throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
198
							}
199
							map[alias] = id;
200
						});
201
					}
202
				}
203
				return map[idOrAlias] || idOrAlias;
204
			}
205
		};
206
	}
207

208
	/**
209
	 * @typedef LoadChainer
210
	 * @property {(before: T, after: () => T) => T} series
211
	 * @property {(values: T[]) => T} parallel
212
	 * @template T
213
	 */
214

215
	/**
216
	 * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
217
	 * component in topological order.
218
	 *
219
	 * @param {DependencyResolver} dependencyResolver
220
	 * @param {StringSet} ids
221
	 * @param {(id: string) => T} loadComponent
222
	 * @param {LoadChainer<T>} [chainer]
223
	 * @returns {T}
224
	 * @template T
225
	 */
226
	function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) {
227
		var series = chainer ? chainer.series : undefined;
228
		var parallel = chainer ? chainer.parallel : noop;
229

230
		/** @type {Object<string, T>} */
231
		var cache = {};
232

233
		/**
234
		 * A set of ids of nodes which are not depended upon by any other node in the graph.
235
		 *
236
		 * @type {StringSet}
237
		 */
238
		var ends = {};
239

240
		/**
241
		 * Loads the given component and its dependencies or returns the cached value.
242
		 *
243
		 * @param {string} id
244
		 * @returns {T}
245
		 */
246
		function handleId(id) {
247
			if (id in cache) {
248
				return cache[id];
249
			}
250

251
			// assume that it's an end
252
			// if it isn't, it will be removed later
253
			ends[id] = true;
254

255
			// all dependencies of the component in the given ids
256
			var dependsOn = [];
257
			for (var depId in dependencyResolver(id)) {
258
				if (depId in ids) {
259
					dependsOn.push(depId);
260
				}
261
			}
262

263
			/**
264
			 * The value to be returned.
265
			 *
266
			 * @type {T}
267
			 */
268
			var value;
269

270
			if (dependsOn.length === 0) {
271
				value = loadComponent(id);
272
			} else {
273
				var depsValue = parallel(dependsOn.map(function (depId) {
274
					var value = handleId(depId);
275
					// none of the dependencies can be ends
276
					delete ends[depId];
277
					return value;
278
				}));
279
				if (series) {
280
					// the chainer will be responsibly for calling the function calling loadComponent
281
					value = series(depsValue, function () { return loadComponent(id); });
282
				} else {
283
					// we don't have a chainer, so we call loadComponent ourselves
284
					loadComponent(id);
285
				}
286
			}
287

288
			// cache and return
289
			return cache[id] = value;
290
		}
291

292
		for (var id in ids) {
293
			handleId(id);
294
		}
295

296
		/** @type {T[]} */
297
		var endValues = [];
298
		for (var endId in ends) {
299
			endValues.push(cache[endId]);
300
		}
301
		return parallel(endValues);
302
	}
303

304
	/**
305
	 * Returns whether the given object has any keys.
306
	 *
307
	 * @param {object} obj
308
	 */
309
	function hasKeys(obj) {
310
		for (var key in obj) {
311
			return true;
312
		}
313
		return false;
314
	}
315

316
	/**
317
	 * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
318
	 * a way to efficiently load them in synchronously and asynchronous contexts (`load`).
319
	 *
320
	 * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
321
	 * components will have to reloaded.
322
	 *
323
	 * The ids in `load` and `loaded` may be in any order and can contain duplicates.
324
	 *
325
	 * @param {Components} components
326
	 * @param {string[]} load
327
	 * @param {string[]} [loaded=[]] A list of already loaded components.
328
	 *
329
	 * If a component is in this list, then all of its requirements will also be assumed to be in the list.
330
	 * @returns {Loader}
331
	 *
332
	 * @typedef Loader
333
	 * @property {() => string[]} getIds A function to get all ids of the components to load.
334
	 *
335
	 * The returned ids will be duplicate-free, alias-free and in load order.
336
	 * @property {LoadFunction} load A functional interface to load components.
337
	 *
338
	 * @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
339
	 * A functional interface to load components.
340
	 *
341
	 * The `loadComponent` function will be called for every component in the order in which they have to be loaded.
342
	 *
343
	 * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
344
	 * `Promise#then` and `Promise.all`.
345
	 *
346
	 * @example
347
	 * load(id => { loadComponent(id); }); // returns undefined
348
	 *
349
	 * await load(
350
	 *     id => loadComponentAsync(id), // returns a Promise for each id
351
	 *     {
352
	 *         series: async (before, after) => {
353
	 *             await before;
354
	 *             await after();
355
	 *         },
356
	 *         parallel: async (values) => {
357
	 *             await Promise.all(values);
358
	 *         }
359
	 *     }
360
	 * );
361
	 */
362
	function getLoader(components, load, loaded) {
363
		var entryMap = createEntryMap(components);
364
		var resolveAlias = createAliasResolver(entryMap);
365

366
		load = load.map(resolveAlias);
367
		loaded = (loaded || []).map(resolveAlias);
368

369
		var loadSet = toSet(load);
370
		var loadedSet = toSet(loaded);
371

372
		// add requirements
373

374
		load.forEach(addRequirements);
375
		function addRequirements(id) {
376
			var entry = entryMap[id];
377
			forEach(entry && entry.require, function (reqId) {
378
				if (!(reqId in loadedSet)) {
379
					loadSet[reqId] = true;
380
					addRequirements(reqId);
381
				}
382
			});
383
		}
384

385
		// add components to reload
386

387
		// A component x in `loaded` has to be reloaded if
388
		//  1) a component in `load` modifies x.
389
		//  2) x depends on a component in `load`.
390
		// The above two condition have to be applied until nothing changes anymore.
391

392
		var dependencyResolver = createDependencyResolver(entryMap);
393

394
		/** @type {StringSet} */
395
		var loadAdditions = loadSet;
396
		/** @type {StringSet} */
397
		var newIds;
398
		while (hasKeys(loadAdditions)) {
399
			newIds = {};
400

401
			// condition 1)
402
			for (var loadId in loadAdditions) {
403
				var entry = entryMap[loadId];
404
				forEach(entry && entry.modify, function (modId) {
405
					if (modId in loadedSet) {
406
						newIds[modId] = true;
407
					}
408
				});
409
			}
410

411
			// condition 2)
412
			for (var loadedId in loadedSet) {
413
				if (!(loadedId in loadSet)) {
414
					for (var depId in dependencyResolver(loadedId)) {
415
						if (depId in loadSet) {
416
							newIds[loadedId] = true;
417
							break;
418
						}
419
					}
420
				}
421
			}
422

423
			loadAdditions = newIds;
424
			for (var newId in loadAdditions) {
425
				loadSet[newId] = true;
426
			}
427
		}
428

429
		/** @type {Loader} */
430
		var loader = {
431
			getIds: function () {
432
				var ids = [];
433
				loader.load(function (id) {
434
					ids.push(id);
435
				});
436
				return ids;
437
			},
438
			load: function (loadComponent, chainer) {
439
				return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer);
440
			}
441
		};
442

443
		return loader;
444
	}
445

446
	return getLoader;
447

448
}());
449

450
if (typeof module !== 'undefined') {
451
	module.exports = getLoader;
452
}
453

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.