diff options
| author | Ruben Beltran del Rio <ruben@unlimited.pizza> | 2020-09-20 16:55:36 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <ruben@unlimited.pizza> | 2020-09-20 16:55:36 +0200 |
| commit | e4c7bafd276049c805039b240e0a83346c31f41b (patch) | |
| tree | 4c8788909ed67710a1628b9e4ae51c1a45b3b3ce /lib/grafn.js | |
Initial release
Diffstat (limited to 'lib/grafn.js')
| -rw-r--r-- | lib/grafn.js | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/lib/grafn.js b/lib/grafn.js new file mode 100644 index 0000000..b07c04a --- /dev/null +++ b/lib/grafn.js @@ -0,0 +1,135 @@ +'use strict'; + +const internals = { + kVertexNotFound: 'There is no vertex with the name: ', + kColors: { + fulfilled: 'green', + rejected: 'red' + } +}; + +/** + * The definition of a vertex that can be executed in the graph. + * + * @typedef {object} tVertex + * @property {string} name The name of the vertex + * @property {string[]} [dependencies=[]] The names of vertices that need to run before this one + * @property {function} action The action to execute + */ + +/** + * Represents a graph of functions. You can call run on any specific vertex, + * which will trigger execution of it and its dependencies. + * + * It guarantees that each vertex will only run once. + * + * It can be represented in graphviz format, highlighting fulfilled and + * rejected vertices. + * + * @class Grafn + */ +module.exports = class Grafn { + + constructor() { + + this._vertices = {}; + this._dependents = {}; + this._state = {}; + } + + /** + * Executes the named vertex and all its dependents + * @method run + * @memberof Grafn + * @instance + * @param {string} vertexName the name of the vertex to run + * @throws Will throw an error if a requested vertex does not exist + */ + async run(vertexName) { + + const vertex = this._vertices[vertexName]; + + if (!vertex) { + throw new Error(internals.kVertexNotFound + vertexName); + } + + if (!vertex.isFulfilled && this._dependenciesFulfilled(vertex.dependencies)) { + try { + this._state[vertexName] = await vertex.action(this._state); + vertex.isFulfilled = true; + } + catch (error) { + vertex.isRejected = true; + throw error; + } + } + + await Promise.all(this._dependents[vertexName].map((dependent) => this.run(dependent))); + } + + /** + * Adds a vertex to the graph. + * @method vertex + * @memberof Grafn + * @instance + * @param {tVertex} vertex the definition of the vertex to add + */ + vertex({ name, action, dependencies = [] }) { + + this._vertices[name] = { action, dependencies, isFulfilled: false, isRejected: false }; + this._dependents[name] = this._dependents[name] || []; + + dependencies.forEach((dependency) => { + + this._dependents[dependency] = this._dependents[dependency] || []; + this._dependents[dependency].push(name); + }); + } + + /** + * Converts the graph to a graphviz digraph. If vertices have been executed, + * they will be highlighted depending on whether they fulfilled or rejected. + * @method toString + * @memberof Grafn + * @instance + * @return {string} The graphviz digraph representation + */ + toString() { + + const string = ['digraph {']; + + Object.entries(this._vertices).forEach(([name, vertex]) => { + + string.push(` ${name}${this._vertexColor(vertex)}`); + vertex.dependencies.forEach((dependency) => string.push(` ${dependency} -> ${name}`)); + }); + + string.push('}'); + + return string.join('\n'); + } + + // Given a list of dependencies, check that all of them are fulfilled + + _dependenciesFulfilled(dependencies) { + + return dependencies + .map((dependency) => (this._vertices[dependency] || {}).isFulfilled) + .reduce((test, isFulfilled) => test && isFulfilled, true); + } + + // Given the state of a vertex, returns the graphviz color configuration + + _vertexColor(vertex) { + + if (vertex.isFulfilled) { + return `[color=${internals.kColors.fulfilled}]`; + } + + if (vertex.isRejected) { + return `[color=${internals.kColors.rejected}]`; + } + + return ''; + } +}; |