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