Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardized dependency logic implementation #1998

Merged
merged 31 commits into from Dec 16, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d43e9c6
First draft: Unified dependency logic
RunDevelopment Jul 24, 2019
b57c3d1
Fix and test in index.html
RunDevelopment Jul 24, 2019
c2c1a33
Added tests
RunDevelopment Jul 24, 2019
e065553
Added native support for aliases
RunDevelopment Jul 30, 2019
f8803c9
Fixed types' scope
RunDevelopment Jul 30, 2019
bf81cfe
Resolved conflict
RunDevelopment Sep 3, 2019
382948b
More Doc + changed API
RunDevelopment Sep 3, 2019
c464239
Removed from index.html
RunDevelopment Sep 3, 2019
76e88a3
Fixed unknown ids and renames test file
RunDevelopment Sep 5, 2019
14d790a
Minor change
RunDevelopment Sep 5, 2019
479b372
More efficient implementation
RunDevelopment Sep 5, 2019
29fc8f9
Fixed npm test
RunDevelopment Sep 5, 2019
382ff4b
Merge branch 'master' into deps-update
RunDevelopment Sep 5, 2019
736103a
Added a check for circular dependencies
RunDevelopment Sep 5, 2019
3755eb6
Download page uses new dependency logic
RunDevelopment Sep 5, 2019
c0759f0
loadLanguages uses the new dependency logic
RunDevelopment Sep 5, 2019
839591c
Updated components.json
RunDevelopment Sep 5, 2019
1850b36
Resolved conflict
RunDevelopment Sep 5, 2019
bb12309
Added alias check
RunDevelopment Sep 6, 2019
6d59466
Extended deps test
RunDevelopment Sep 6, 2019
8fcf8b1
Test suite uses the new dependency logic
RunDevelopment Sep 6, 2019
946281f
Readded loadLanguages' load all languages behavior
RunDevelopment Sep 6, 2019
15ba68a
Added check for unknown dependency ids
RunDevelopment Sep 6, 2019
746a4dc
loadLanguages: Removed check for valid ids
RunDevelopment Sep 6, 2019
049f8c4
Removed type hack
RunDevelopment Oct 22, 2019
81e4b0d
Fixed async loading and added chainer
RunDevelopment Oct 22, 2019
148823d
Resolved conflict
RunDevelopment Oct 22, 2019
4ce06aa
String quote style
RunDevelopment Oct 22, 2019
6098aea
Merge branch 'master' into deps-update
RunDevelopment Dec 15, 2019
5275691
after → optional and some other renaming
RunDevelopment Dec 15, 2019
39ff76e
Fixed download page
RunDevelopment Dec 15, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
364 changes: 364 additions & 0 deletions dependencies.js
@@ -0,0 +1,364 @@
"use strict";

/**
* @typedef {Object<string, ComponentCategory>} Components
* @typedef {{ meta: Object<string, any> } & Object<string, ComponentEntry>} 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[]} [require]
* @property {string | string[]} [modify]
* @property {string | string[]} [after]
*/

var getLoad = (function () {
mAAdhaTTah marked this conversation as resolved.
Show resolved Hide resolved

/**
* A function which does absolutely nothing.
*
* @type {any}
*/
var noop = function () { };

/**
*
* @param {null | undefined | T | T[]} value
* @returns {T[]}
* @template T
*/
function toArray(value) {
if (Array.isArray(value)) {
return value;
} else {
return value == null ? [] : [value];
}
}

/**
* Returns a new set for the given string array.
*
* @param {string[]} array
* @returns {StringSet}
* @typedef {Object<string, true>} StringSet
*/
function toSet(array) {
/** @type {StringSet} */
mAAdhaTTah marked this conversation as resolved.
Show resolved Hide resolved
var set = {};
for (var i = 0, l = array.length; i < l; i++) {
set[array[i]] = true;
}
return set;
}

/**
*
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
* @param {Components} components
* @param {string} id
* @returns {ComponentEntry}
*/
function getEntry(components, id) {
for (var categoryName in components) {
var category = components[categoryName];
for (var entryId in category) {
if (entryId === id) {
return category[entryId];
}
}
}
}

/**
* Iterates all component entries in the given components object.
*
* _Note:_ This does not include meta entries.
*
* @param {Components} components
* @param {(id: string, entry: ComponentEntry) => void} callback
*/
function forEachEntry(components, callback) {
for (var categoryName in components) {
var category = components[categoryName];
for (var id in category) {
if (id !== 'meta') {
callback(id, category[id]);
}
}
}
}

/**
* Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
*
* @param {Components} components
* @returns {DependencyMap}
* @typedef {Object<string, StringSet>} DependencyMap
*/
function createDependencyMap(components) {
/** @type {DependencyMap} */
var map = {};

/**
*
* @param {string} id
* @param {ComponentEntry} [entry]
*/
function addToMap(id, entry) {
if (id in map) {
return;
}

if (entry == undefined) {
entry = getEntry(components, id);
}

/** @type {StringSet} */
var dependencies = {};

[].concat(entry.require, entry.modify, entry.after).filter(Boolean).forEach(function (depId) {
addToMap(depId);
dependencies[depId] = true;
for (var transitiveDepId in map[depId]) {
dependencies[transitiveDepId] = true;
}
});

map[id] = dependencies;
}

forEachEntry(components, addToMap);

return map;
}

/**
* Returns a function which resolves the aliases of its given id of alias.
*
* @param {Components} components
* @returns {(idOrAlias: string) => string}
*/
function createAliasResolver(components) {
/** @type {Object<string, string>} */
var map = {};

forEachEntry(components, function (id, entry) {
var aliases = toArray(entry.alias);
aliases.forEach(function (alias) {
map[alias] = id;
});
});

return function (idOrAlias) {
return map[idOrAlias] || idOrAlias;
};
}

/**
*
* @param {DependencyMap} dependencyMap
* @param {StringSet} ids
* @param {(id: string) => T} loadComponent
* @param {(before: T, after: T) => T} series
* @param {(values: T[]) => T} parallel
* @returns {T}
* @template T
*/
function loadDAG(dependencyMap, ids, loadComponent, series, parallel) {
/** @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 dependencyMap[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;
}));
value = series(depsValue, loadComponent(id));
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 a new array with the ids of the components which have to be loaded. The returns ids can be in any order
* and will be duplicate-free.
*
* The returned ids will be superset of `load`. If some of the returned 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.
*/
function getLoad(components, load, loaded) {
var resolveAlias = createAliasResolver(components);

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 require = toArray(getEntry(components, id).require);
require.forEach(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 dependencyMap = createDependencyMap(components);

/** @type {StringSet} */
var loadAdditions = loadSet;
/** @type {StringSet} */
var newIds;
while (hasKeys(loadAdditions)) {
newIds = {};

// condition 1)
for (var loadId in loadAdditions) {
var modify = toArray(getEntry(components, loadId).modify);
modify.forEach(function (modId) {
if (modId in loadedSet) {
newIds[modId] = true;
}
});
}

// condition 2)
for (var loadedId in loadedSet) {
if (!(loadedId in loadSet)) {
for (var depId in dependencyMap[loadedId]) {
if (depId in loadSet) {
newIds[loadedId] = true;
break;
}
}
}
}

loadAdditions = newIds;
for (var newId in loadAdditions) {
loadSet[newId] = true;
}
}

// return ids and a method to load them

return {
ids: Object.keys(loadSet),
/**
* A functional interface to load components.
*
* The `loadComponent` will be called for every component in the order in which they are loaded.
*
* `series` and `parallel` are useful for asynchronous loading and can be thought of as
* `Promise#then` and `Promise.all`.
*
* _Note:_ Even though, both `series` and `parallel` are optional, they have to both defined or both
* undefined together. It's not valid for just one to be defined while the other is undefined.
*
* @param {(id: string) => T} loadComponent
* @param {(before: T, after: T) => T} [series]
* @param {(values: T[]) => T} [parallel]
* @return {T}
* @template T
*/
load: function (loadComponent, series, parallel) {
return loadDAG(dependencyMap, loadSet, loadComponent, series || noop, parallel || noop);
}
};
}

return getLoad;

}());

if (typeof module !== 'undefined') {
module.exports = getLoad;
}
1 change: 1 addition & 0 deletions index.html
Expand Up @@ -286,6 +286,7 @@ <h1>Credits</h1>
<script src="scripts/utopia.js"></script>
<script src="prism.js"></script>
<script src="components.js"></script>
<script src="dependencies.js"></script>
<script src="scripts/code.js"></script>
<script>
(function() {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -6,6 +6,7 @@
"style": "themes/prism.css",
"scripts": {
"test:aliases": "mocha tests/aliases-test.js",
"test:dependency": "mocha tests/dependency-test.js",
"test:languages": "mocha tests/run.js",
"test:plugins": "mocha tests/plugins/**/*.js",
"test:regex": "mocha tests/regex-tests.js",
Expand Down