]> git.r.bdr.sh - rbdr/grafn/blob - lib/grafn.js
b07c04a8cac1836886779d9a4d4f95c2fe295cb7
[rbdr/grafn] / lib / grafn.js
1 'use strict';
2
3 const internals = {
4 kVertexNotFound: 'There is no vertex with the name: ',
5 kColors: {
6 fulfilled: 'green',
7 rejected: 'red'
8 }
9 };
10
11 /**
12 * The definition of a vertex that can be executed in the graph.
13 *
14 * @typedef {object} tVertex
15 * @property {string} name The name of the vertex
16 * @property {string[]} [dependencies=[]] The names of vertices that need to run before this one
17 * @property {function} action The action to execute
18 */
19
20 /**
21 * Represents a graph of functions. You can call run on any specific vertex,
22 * which will trigger execution of it and its dependencies.
23 *
24 * It guarantees that each vertex will only run once.
25 *
26 * It can be represented in graphviz format, highlighting fulfilled and
27 * rejected vertices.
28 *
29 * @class Grafn
30 */
31 module.exports = class Grafn {
32
33 constructor() {
34
35 this._vertices = {};
36 this._dependents = {};
37 this._state = {};
38 }
39
40 /**
41 * Executes the named vertex and all its dependents
42 * @method run
43 * @memberof Grafn
44 * @instance
45 * @param {string} vertexName the name of the vertex to run
46 * @throws Will throw an error if a requested vertex does not exist
47 */
48 async run(vertexName) {
49
50 const vertex = this._vertices[vertexName];
51
52 if (!vertex) {
53 throw new Error(internals.kVertexNotFound + vertexName);
54 }
55
56 if (!vertex.isFulfilled && this._dependenciesFulfilled(vertex.dependencies)) {
57 try {
58 this._state[vertexName] = await vertex.action(this._state);
59 vertex.isFulfilled = true;
60 }
61 catch (error) {
62 vertex.isRejected = true;
63 throw error;
64 }
65 }
66
67 await Promise.all(this._dependents[vertexName].map((dependent) => this.run(dependent)));
68 }
69
70 /**
71 * Adds a vertex to the graph.
72 * @method vertex
73 * @memberof Grafn
74 * @instance
75 * @param {tVertex} vertex the definition of the vertex to add
76 */
77 vertex({ name, action, dependencies = [] }) {
78
79 this._vertices[name] = { action, dependencies, isFulfilled: false, isRejected: false };
80 this._dependents[name] = this._dependents[name] || [];
81
82 dependencies.forEach((dependency) => {
83
84 this._dependents[dependency] = this._dependents[dependency] || [];
85 this._dependents[dependency].push(name);
86 });
87 }
88
89 /**
90 * Converts the graph to a graphviz digraph. If vertices have been executed,
91 * they will be highlighted depending on whether they fulfilled or rejected.
92 * @method toString
93 * @memberof Grafn
94 * @instance
95 * @return {string} The graphviz digraph representation
96 */
97 toString() {
98
99 const string = ['digraph {'];
100
101 Object.entries(this._vertices).forEach(([name, vertex]) => {
102
103 string.push(` ${name}${this._vertexColor(vertex)}`);
104 vertex.dependencies.forEach((dependency) => string.push(` ${dependency} -> ${name}`));
105 });
106
107 string.push('}');
108
109 return string.join('\n');
110 }
111
112 // Given a list of dependencies, check that all of them are fulfilled
113
114 _dependenciesFulfilled(dependencies) {
115
116 return dependencies
117 .map((dependency) => (this._vertices[dependency] || {}).isFulfilled)
118 .reduce((test, isFulfilled) => test && isFulfilled, true);
119 }
120
121 // Given the state of a vertex, returns the graphviz color configuration
122
123 _vertexColor(vertex) {
124
125 if (vertex.isFulfilled) {
126 return `[color=${internals.kColors.fulfilled}]`;
127 }
128
129 if (vertex.isRejected) {
130 return `[color=${internals.kColors.rejected}]`;
131 }
132
133 return '';
134 }
135 };