1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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 '';
}
};
|