aboutsummaryrefslogtreecommitdiff
path: root/lib/grafn.js
diff options
context:
space:
mode:
authorRuben Beltran del Rio <ruben@unlimited.pizza>2020-09-20 16:55:36 +0200
committerRuben Beltran del Rio <ruben@unlimited.pizza>2020-09-20 16:55:36 +0200
commite4c7bafd276049c805039b240e0a83346c31f41b (patch)
tree4c8788909ed67710a1628b9e4ae51c1a45b3b3ce /lib/grafn.js
Initial release
Diffstat (limited to 'lib/grafn.js')
-rw-r--r--lib/grafn.js135
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 '';
+ }
+};