-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathfluorine.js
298 lines (258 loc) · 10.1 KB
/
fluorine.js
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/**
Flow is the class that represents an application flow of steps to meet
a goal for an application.
@class Flow
@namespace Global
@requires CustomEventSupport
@requires NodeSupport
**/
Class('Flow').includes(CustomEventSupport, NodeSupport)({
prototype : {
/**
holder for the name of the flow
@property name <public> [String]
**/
name : '',
/**
initializer for the class
@property init <public> [Function]
@argument config <optional> [Object]
**/
init : function init(config) {
Object.keys(config).forEach(function (property) {
this[property] = config[property];
}, this);
},
/**
Define a step in the flow. this is a dsl like starter because returs functions.
flowInstance.step('name')(function (step) {...});
@property step <public> [Function]
@argument name <required> [String]
@return Function a closure to the created step that you can use either to finish the step
creation or define more metadata.
**/
step : function step(name) {
var flow, dependencies, nodeFactory;
flow = this;
dependencies = [];
nodeFactory = function nodeFactory(code, errorCode) {
var node = new FlowNode({
name : name,
code : code,
errorCode : errorCode,
dependencies : dependencies
});
flow.appendChild(node);
dependencies.forEach(function dependenciesIterator(dependencyName) {
flow.bind(dependencyName, function () {
flow.runNode(node);
});
});
flow.runNode(node);
};
nodeFactory.dependsOn = function dependsOn() {
dependencies = Array.prototype.slice.call(arguments, 0);
return nodeFactory;
};
return nodeFactory;
},
/**
Checks for a node candidate to run, in order to run this node
all its preconditions must be met (dependencies completed). after this steps
pass it creates the appropiete data that will pass to the node and finally
it executes the node passing a capability object to this step that contains
the data from its dependencies.
@property runNode <public> [Function]
@argument node <required> [FlowNode] the node that you want to execute.
**/
runNode : function runNode(node) {
var dependencies = [].concat(node.dependencies);
var hasRejectedDependency = false;
var fulfilledDependencies = dependencies.filter(function (dependencyName) {
if (this.hasOwnProperty(dependencyName) === false) {
return;
}
if (this[dependencyName].isFulfilled === true || this[dependencyName].isRejected === true) {
if (this[dependencyName].isRejected === true) {
hasRejectedDependency = true;
}
return true;
}
}, this);
if (dependencies.length === fulfilledDependencies.length) {
var nodeLikeObject = {
success : function nodeSuccess(data) {
node.fulfill(data);
},
fail : function nodeFail(error) {
node.reject(error);
}
};
if (hasRejectedDependency === false) {
nodeLikeObject.data = {};
fulfilledDependencies.forEach(function fulfilledDependenciesIterator(dependencyName) {
nodeLikeObject.data[dependencyName] = this[dependencyName].data;
}, this);
}
else {
nodeLikeObject.errors = {};
fulfilledDependencies.forEach(function (dependencyName) {
nodeLikeObject.errors[dependencyName] = this[dependencyName].error;
}, this);
}
node.run(nodeLikeObject);
}
},
/**
A flow basically is a graph which implies that the garbage collector will not be able
to clear this objects if they are no longer needed by the developer.
this method takes care of clearing those references and gives the developer full control
of when to discard the flow.
@property destroy <public> [Function]
**/
destroy : function destroy() {
this.children.forEach(function (child) {
child.destroy();
});
return null;
},
/**
This is a utility method to introspect a flow. flow use case is for when a complex sequence
needs to be solved and its very hard to solve it by simpler programming constructs, so mapping
this sequences is hard, this method dumps the flow into a dot directed graph to be visualized.
@property toDot <public> [Function]
@return string The actual dot string that represents this flow graph.
**/
toDot : function toDot() {
var dotGraph = [];
dotGraph.push("digraph " + this.name + " {");
this.children.forEach(function (step) {
dotGraph.push(step.name);
step.dependencies.forEach(function (dependencyName) {
dotGraph.push(dependencyName + " -> " + step.name);
});
});
dotGraph.push("}");
console.debug("Put this in a file and run this in a terminal: dot -Tsvg yourDotFile > graph.svg");
return dotGraph.join("\n");
}
}
});
/**
This class represents a step in a flow, it does care what the step actually executes
because the execution code is provided as a property of the node.
@class FlowNode
@namespace Global
@requires NodeSupport
**/
Class('FlowNode').includes(NodeSupport)({
prototype : {
/**
holds the name of the step
@property name <public> [String] ('')
**/
name : '',
/**
holds the data for the step once the data gets resolved and will get the value that
the step wants to pass
@property data <public> [Object] (undefined)
**/
data : undefined,
/**
This is very similar to the data property but is intended for steps that fail
@property error <public> [Object] (undefined)
**/
error : undefined,
/**
Nodes behave like promises and this parameter holds a boolean when the promise
is fulfilled or not. if a promise is rejected this will remain on false.
@property isFulfilled <public> [Boolean] (false)
**/
isFulfilled : false,
/**
Nodes behave like promises and this parameter holds a boolean when the promise
is rejected or not. if a promise is fulfilled this will remain on false.
@property isRejected <public> [Boolean] (false)
**/
isRejected : false,
/**
Every step takes care of its context in the flow it belongs to but has noting to
do with the step implementation, this property holds the actual code that the step
will execute when it runs.
@property code <public> [Function] (function () { trow 'no code' })
**/
code : function () { throw 'no code'; },
/**
Nodes can be rejected and this code is different from the success code. Similar to
the code property this holds the code for the error execution code provided.
@property errorCode <public> [Function] (undefined)
**/
errorCode : undefined,
/**
class initializer
**/
init : function (config) {
Object.keys(config).forEach(function (property) {
this[property] = config[property];
}, this);
},
/**
Executes the node. This is done via a setTimeout to release the stack depth.
Even thought it is required for events to be synchronous, a flow step does
not represent a safe data signal propagation, instead is a pass to other process
like behavior.
@property run <public> [Function]
@argument FlowNode <required> [Object]
@return Undefined
**/
run : function (nodeLikeObject) {
var node = this;
if (nodeLikeObject.hasOwnProperty('data') === true) {
setTimeout(function runFlowNode() {
node.code(nodeLikeObject);
}, 0);
}
else if (typeof this.errorCode !== 'undefined') {
setTimeout(function runFlowNodeError() {
node.errorCode(nodeLikeObject);
}, 0);
}
},
/**
method to notify that the step executed succesfully.
@property fulfill <public> [Function]
**/
fulfill : function (data) {
this.data = data;
this.isFulfilled = true;
if (this.parent === null) {
return;
}
this.parent.dispatch(this.name);
},
/**
method to notify that the step executed wrong.
@property reject <public> [Function]
**/
reject : function (error) {
this.error = error;
this.isRejected = true;
if (this.parent === null) {
return;
}
this.parent.dispatch(this.name);
this.parent.dispatch('reject', {
data : { node : this, error: error }
});
},
/**
Removes itself from the flow allowing that the garbage collector
removes this object cleanly
@property destroy <public> [Function]
**/
destroy : function destroy() {
this.parent.removeChild(this);
return null;
}
}
});