lulz 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -140
- package/TODO.md +132 -0
- package/examples.js +169 -197
- package/index.js +164 -14
- package/package.json +16 -17
- package/src/flow.js +362 -215
- package/src/red-lib.js +595 -0
- package/src/rx-lib.js +679 -0
- package/src/utils.js +270 -0
- package/src/workers.js +367 -0
- package/test.js +505 -279
- package/src/nodes.js +0 -520
package/src/flow.js
CHANGED
|
@@ -1,38 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lulz -
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
2
|
+
* lulz - Core Flow Engine
|
|
3
|
+
*
|
|
4
|
+
* A reactive dataflow system that makes coders happy.
|
|
5
|
+
*
|
|
6
|
+
* Processing Modes (2.0):
|
|
7
|
+
* - Series (default): ['in', a, b, c, 'out'] → a→b→c sequential
|
|
8
|
+
* - Parallel (explicit): ['in', [a, b, c], 'out'] → all receive same packet
|
|
9
|
+
*
|
|
10
|
+
* Helper functions for readability:
|
|
11
|
+
* - series(a, b, c) → sequential processing
|
|
12
|
+
* - parallel(a, b, c) → fan-out processing
|
|
13
|
+
*
|
|
14
|
+
* EventEmitter API:
|
|
15
|
+
* - app.emit('pipeName', packet) → inject packet
|
|
16
|
+
* - app.on('pipeName', handler) → intercept packets
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
// Function Type Detection
|
|
24
|
+
// ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export const isOuter = (fn) =>
|
|
27
|
+
typeof fn === 'function' && fn.hasOwnProperty('prototype');
|
|
28
|
+
|
|
29
|
+
export const isInner = (fn) =>
|
|
30
|
+
typeof fn === 'function' && !fn.hasOwnProperty('prototype');
|
|
31
|
+
|
|
32
|
+
export const isFlow = (obj) =>
|
|
33
|
+
obj && obj._isFlow === true;
|
|
34
|
+
|
|
35
|
+
export const isParallel = (item) =>
|
|
36
|
+
Array.isArray(item) && item._parallel === true;
|
|
37
|
+
|
|
38
|
+
export const isSeriesMarker = (item) =>
|
|
39
|
+
Array.isArray(item) && item._series === true;
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────
|
|
43
|
+
// Processing Mode Markers
|
|
44
|
+
// ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mark functions for parallel (fan-out) processing
|
|
48
|
+
* All functions receive the same packet simultaneously
|
|
49
|
+
*/
|
|
50
|
+
export function parallel(...fns) {
|
|
51
|
+
const arr = [...fns];
|
|
52
|
+
arr._parallel = true;
|
|
53
|
+
return arr;
|
|
26
54
|
}
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Mark functions for series (sequential) processing
|
|
58
|
+
* Explicit marker, though series is the default
|
|
59
|
+
*/
|
|
60
|
+
export function series(...fns) {
|
|
61
|
+
const arr = [...fns];
|
|
62
|
+
arr._series = true;
|
|
63
|
+
return arr;
|
|
30
64
|
}
|
|
31
65
|
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────
|
|
68
|
+
// Node Factory
|
|
69
|
+
// ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
32
71
|
/**
|
|
33
72
|
* Create a processing node
|
|
34
73
|
*/
|
|
35
|
-
function makeNode(name, fn) {
|
|
74
|
+
export function makeNode(name, fn) {
|
|
36
75
|
const outputs = new Set();
|
|
37
76
|
|
|
38
77
|
function send(packet) {
|
|
@@ -41,91 +80,173 @@ function makeNode(name, fn) {
|
|
|
41
80
|
}
|
|
42
81
|
}
|
|
43
82
|
|
|
44
|
-
function
|
|
83
|
+
function node(packet) {
|
|
45
84
|
fn(send, packet);
|
|
46
85
|
}
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
node.connect = function(next) {
|
|
49
88
|
outputs.add(next);
|
|
50
89
|
return next;
|
|
51
90
|
};
|
|
52
91
|
|
|
53
|
-
|
|
92
|
+
node.disconnect = function(next) {
|
|
54
93
|
outputs.delete(next);
|
|
94
|
+
return next;
|
|
55
95
|
};
|
|
56
96
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
node.receive = node;
|
|
98
|
+
node.send = send;
|
|
99
|
+
node.nodeName = name;
|
|
100
|
+
node.outputs = outputs;
|
|
101
|
+
node._isNode = true;
|
|
61
102
|
|
|
62
|
-
return
|
|
103
|
+
return node;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
|
-
/**
|
|
66
|
-
* Create a pass-through pipe
|
|
67
|
-
*/
|
|
68
|
-
function makePipe(name) {
|
|
69
|
-
return makeNode(name, (send, packet) => send(packet));
|
|
70
|
-
}
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
function buildSeries(fns, context, prepareFunction) {
|
|
77
|
-
if (fns.length === 0) {
|
|
78
|
-
const passthrough = makeNode('passthrough', (send, packet) => send(packet));
|
|
79
|
-
return { input: passthrough, output: passthrough };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const nodes = fns.map((fn, i) => {
|
|
83
|
-
const inner = prepareFunction(fn);
|
|
84
|
-
return makeNode(fn.name || `series-${i}`, inner);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Chain them: node[0] → node[1] → ... → node[n-1]
|
|
88
|
-
for (let i = 0; i < nodes.length - 1; i++) {
|
|
89
|
-
nodes[i].connect(nodes[i + 1]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
input: nodes[0],
|
|
94
|
-
output: nodes[nodes.length - 1]
|
|
95
|
-
};
|
|
96
|
-
}
|
|
107
|
+
// ─────────────────────────────────────────────────────────────
|
|
108
|
+
// Main Flow Builder
|
|
109
|
+
// ─────────────────────────────────────────────────────────────
|
|
97
110
|
|
|
98
111
|
/**
|
|
99
|
-
*
|
|
112
|
+
* Create a reactive flow from a graph definition
|
|
113
|
+
*
|
|
114
|
+
* @param {Array} graph - Array of pipeline definitions
|
|
115
|
+
* @param {Object} context - Shared context object
|
|
116
|
+
* @returns {EventEmitter} Flow instance with emit/on API
|
|
100
117
|
*/
|
|
101
|
-
function flow(graph, context = {}) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
export function flow(graph, context = {}) {
|
|
119
|
+
|
|
120
|
+
// ─── EventEmitter Base ───
|
|
121
|
+
const app = new EventEmitter();
|
|
122
|
+
app._isFlow = true;
|
|
123
|
+
app._context = context;
|
|
124
|
+
app._pipes = {};
|
|
125
|
+
app._nodes = [];
|
|
126
|
+
app._producers = [];
|
|
127
|
+
app._cleanups = [];
|
|
128
|
+
|
|
129
|
+
// Store original emit for internal use
|
|
130
|
+
const _originalEmit = app.emit.bind(app);
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// ─── Pipe Management ───
|
|
134
|
+
|
|
135
|
+
const getPipe = (name) => {
|
|
136
|
+
if (!app._pipes[name]) {
|
|
137
|
+
const node = makeNode(`pipe:${name}`, (send, packet) => {
|
|
138
|
+
// Notify EventEmitter listeners (use original to avoid loop)
|
|
139
|
+
_originalEmit(name, packet);
|
|
140
|
+
// Continue down the chain
|
|
141
|
+
send(packet);
|
|
142
|
+
});
|
|
143
|
+
app._pipes[name] = node;
|
|
109
144
|
}
|
|
110
|
-
return
|
|
111
|
-
}
|
|
145
|
+
return app._pipes[name];
|
|
146
|
+
};
|
|
112
147
|
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
|
|
149
|
+
// ─── Function Preparation ───
|
|
150
|
+
|
|
151
|
+
const prepareFunction = (fn) => {
|
|
115
152
|
if (isOuter(fn)) {
|
|
116
|
-
// Factory function: call with empty options
|
|
153
|
+
// Factory function: bind context, call with empty options
|
|
117
154
|
const bound = fn.bind({ context });
|
|
118
155
|
return bound({});
|
|
119
156
|
} else {
|
|
120
|
-
//
|
|
121
|
-
return
|
|
122
|
-
// Create a this-like object for arrow functions via closure
|
|
123
|
-
fn(send, packet);
|
|
124
|
-
};
|
|
157
|
+
// Already an inner (arrow) function
|
|
158
|
+
return fn;
|
|
125
159
|
}
|
|
126
|
-
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// ─── Build Series Chain ───
|
|
164
|
+
|
|
165
|
+
const buildSeries = (fns) => {
|
|
166
|
+
if (fns.length === 0) {
|
|
167
|
+
const passthrough = makeNode('passthrough', (send, packet) => send(packet));
|
|
168
|
+
return { input: passthrough, output: passthrough };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nodes = fns.map((fn, i) => {
|
|
172
|
+
if (isFlow(fn)) {
|
|
173
|
+
// Embedded flow - return its input, wire to its output
|
|
174
|
+
return {
|
|
175
|
+
_isEmbeddedFlow: true,
|
|
176
|
+
flow: fn,
|
|
177
|
+
receive: (packet) => fn.emit('in', packet)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const inner = prepareFunction(fn);
|
|
182
|
+
return makeNode(fn.name || `series:${i}`, inner);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Chain: node[0] → node[1] → ... → node[n-1]
|
|
186
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
187
|
+
const current = nodes[i];
|
|
188
|
+
const next = nodes[i + 1];
|
|
189
|
+
|
|
190
|
+
if (current._isEmbeddedFlow) {
|
|
191
|
+
// Wire flow's output to next node
|
|
192
|
+
current.flow.on('out', (packet) => {
|
|
193
|
+
if (next._isEmbeddedFlow) {
|
|
194
|
+
next.flow.emit('in', packet);
|
|
195
|
+
} else {
|
|
196
|
+
next.receive(packet);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
} else if (next._isEmbeddedFlow) {
|
|
200
|
+
// Wire current node to flow's input
|
|
201
|
+
current.connect({
|
|
202
|
+
receive: (packet) => next.flow.emit('in', packet)
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
current.connect(next);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Return input of first, output of last
|
|
210
|
+
const first = nodes[0];
|
|
211
|
+
const last = nodes[nodes.length - 1];
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
input: first._isEmbeddedFlow
|
|
215
|
+
? { receive: (packet) => first.flow.emit('in', packet) }
|
|
216
|
+
: first,
|
|
217
|
+
output: last._isEmbeddedFlow
|
|
218
|
+
? { connect: (next) => last.flow.on('out', (packet) => next.receive(packet)) }
|
|
219
|
+
: last
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
// ─── Build Parallel Fan-out ───
|
|
225
|
+
|
|
226
|
+
const buildParallel = (fns, dest) => {
|
|
227
|
+
const nodes = [];
|
|
228
|
+
|
|
229
|
+
for (const fn of fns) {
|
|
230
|
+
if (isFlow(fn)) {
|
|
231
|
+
// Embedded flow
|
|
232
|
+
const adapter = {
|
|
233
|
+
receive: (packet) => fn.emit('in', packet),
|
|
234
|
+
connect: (next) => fn.on('out', (packet) => next.receive(packet))
|
|
235
|
+
};
|
|
236
|
+
nodes.push(adapter);
|
|
237
|
+
} else {
|
|
238
|
+
const inner = prepareFunction(fn);
|
|
239
|
+
const node = makeNode(fn.name || 'parallel', inner);
|
|
240
|
+
nodes.push(node);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return nodes;
|
|
245
|
+
};
|
|
127
246
|
|
|
128
|
-
|
|
247
|
+
|
|
248
|
+
// ─── Process Graph ───
|
|
249
|
+
|
|
129
250
|
for (const line of graph) {
|
|
130
251
|
if (!Array.isArray(line) || line.length < 2) continue;
|
|
131
252
|
|
|
@@ -134,180 +255,206 @@ function flow(graph, context = {}) {
|
|
|
134
255
|
const last = elements[elements.length - 1];
|
|
135
256
|
const middle = elements.slice(1, -1);
|
|
136
257
|
|
|
137
|
-
//
|
|
258
|
+
// ─── SOURCE ───
|
|
138
259
|
let source;
|
|
260
|
+
|
|
139
261
|
if (typeof first === 'string') {
|
|
140
262
|
source = getPipe(first);
|
|
263
|
+
} else if (isFlow(first)) {
|
|
264
|
+
// Flow as source - create adapter
|
|
265
|
+
source = makeNode('flow-source', (send, packet) => send(packet));
|
|
266
|
+
first.on('out', (packet) => source.receive(packet));
|
|
267
|
+
first.start?.();
|
|
141
268
|
} else if (typeof first === 'function') {
|
|
142
269
|
// Producer function
|
|
143
|
-
const producerFn = isOuter(first)
|
|
144
|
-
|
|
270
|
+
const producerFn = isOuter(first)
|
|
271
|
+
? first.bind({ context })({})
|
|
272
|
+
: first;
|
|
273
|
+
|
|
274
|
+
source = makeNode(first.name || 'producer', (send, packet) => send(packet));
|
|
145
275
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Subflow as source
|
|
151
|
-
source = first._output || getPipe('__subflow_out__');
|
|
276
|
+
app._producers.push({
|
|
277
|
+
fn: producerFn,
|
|
278
|
+
node: source
|
|
279
|
+
});
|
|
152
280
|
}
|
|
153
281
|
|
|
154
|
-
//
|
|
282
|
+
// ─── DESTINATION ───
|
|
155
283
|
let dest;
|
|
284
|
+
|
|
156
285
|
if (typeof last === 'string') {
|
|
157
286
|
dest = getPipe(last);
|
|
287
|
+
} else if (isFlow(last)) {
|
|
288
|
+
// Flow as destination
|
|
289
|
+
dest = {
|
|
290
|
+
receive: (packet) => last.emit('in', packet),
|
|
291
|
+
_isNode: true
|
|
292
|
+
};
|
|
158
293
|
} else if (typeof last === 'function') {
|
|
159
294
|
const inner = prepareFunction(last);
|
|
160
295
|
dest = makeNode(last.name || 'sink', inner);
|
|
161
|
-
|
|
162
|
-
} else if (last?._isFlow) {
|
|
163
|
-
// Subflow as destination
|
|
164
|
-
dest = last._input || getPipe('__subflow_in__');
|
|
296
|
+
app._nodes.push(dest);
|
|
165
297
|
}
|
|
166
298
|
|
|
167
|
-
//
|
|
299
|
+
// ─── MIDDLE (transforms) ───
|
|
300
|
+
|
|
168
301
|
if (middle.length === 0) {
|
|
169
302
|
// Direct connection
|
|
170
|
-
source.connect(dest);
|
|
303
|
+
if (source.connect) source.connect(dest);
|
|
304
|
+
|
|
305
|
+
} else if (middle.length === 1 && isSeriesMarker(middle[0])) {
|
|
306
|
+
// Explicit series marker: ['in', series(a, b, c), 'out']
|
|
307
|
+
const { input, output } = buildSeries(middle[0]);
|
|
308
|
+
if (source.connect) source.connect(input);
|
|
309
|
+
if (output.connect) output.connect(dest);
|
|
310
|
+
|
|
311
|
+
} else if (middle.length === 1 && (isParallel(middle[0]) || Array.isArray(middle[0]))) {
|
|
312
|
+
// Explicit parallel: ['in', [a, b, c], 'out'] or ['in', parallel(a,b,c), 'out']
|
|
313
|
+
const fns = middle[0];
|
|
314
|
+
const nodes = buildParallel(fns, dest);
|
|
315
|
+
|
|
316
|
+
for (const node of nodes) {
|
|
317
|
+
if (source.connect) source.connect(node);
|
|
318
|
+
if (node.connect) node.connect(dest);
|
|
319
|
+
}
|
|
320
|
+
|
|
171
321
|
} else {
|
|
172
|
-
//
|
|
173
|
-
const
|
|
322
|
+
// Default: SERIES processing (a → b → c)
|
|
323
|
+
const allFns = [];
|
|
174
324
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (isSeriesArray(item)) {
|
|
185
|
-
// Series within fan-out: ['in', a, [b, c, d], e, 'out']
|
|
186
|
-
const { input, output } = buildSeries(item, context, prepareFunction);
|
|
187
|
-
source.connect(input);
|
|
188
|
-
output.connect(dest);
|
|
189
|
-
nodes.push(input);
|
|
190
|
-
} else if (typeof item === 'function') {
|
|
191
|
-
// Regular transform in fan-out
|
|
192
|
-
const inner = prepareFunction(item);
|
|
193
|
-
const node = makeNode(item.name || 'transform', inner);
|
|
194
|
-
source.connect(node);
|
|
195
|
-
node.connect(dest);
|
|
196
|
-
nodes.push(node);
|
|
197
|
-
}
|
|
325
|
+
for (const item of middle) {
|
|
326
|
+
if (isParallel(item) || (Array.isArray(item) && !isSeriesMarker(item))) {
|
|
327
|
+
// Nested parallel within series - treat as single parallel block
|
|
328
|
+
allFns.push({ _parallelBlock: true, fns: item });
|
|
329
|
+
} else if (isSeriesMarker(item)) {
|
|
330
|
+
// Nested explicit series
|
|
331
|
+
allFns.push(...item);
|
|
332
|
+
} else {
|
|
333
|
+
allFns.push(item);
|
|
198
334
|
}
|
|
199
335
|
}
|
|
336
|
+
|
|
337
|
+
// Build the series chain
|
|
338
|
+
const { input, output } = buildSeries(allFns.filter(f => !f._parallelBlock));
|
|
339
|
+
|
|
340
|
+
if (source.connect) source.connect(input);
|
|
341
|
+
if (output.connect) output.connect(dest);
|
|
200
342
|
}
|
|
201
343
|
}
|
|
202
344
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
inject(pipeName, packet) {
|
|
226
|
-
const pipe = pipes[pipeName];
|
|
227
|
-
if (pipe) {
|
|
228
|
-
pipe.receive(packet);
|
|
229
|
-
} else {
|
|
230
|
-
console.warn(`Pipe "${pipeName}" not found`);
|
|
345
|
+
|
|
346
|
+
// ─── EventEmitter Injection Setup ───
|
|
347
|
+
|
|
348
|
+
// Override emit to inject into pipes
|
|
349
|
+
app.emit = (event, packet) => {
|
|
350
|
+
// If it's a pipe name, inject the packet (which will trigger _originalEmit)
|
|
351
|
+
if (app._pipes[event] && packet !== undefined) {
|
|
352
|
+
app._pipes[event].receive(packet);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
// For non-pipe events, use original emit
|
|
356
|
+
return _originalEmit(event, packet);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// ─── API Methods ───
|
|
361
|
+
|
|
362
|
+
app.start = () => {
|
|
363
|
+
for (const { fn, node } of app._producers) {
|
|
364
|
+
const cleanup = fn((packet) => node.receive(packet));
|
|
365
|
+
if (typeof cleanup === 'function') {
|
|
366
|
+
app._cleanups.push(cleanup);
|
|
231
367
|
}
|
|
232
|
-
|
|
233
|
-
|
|
368
|
+
}
|
|
369
|
+
app.emit('start');
|
|
370
|
+
return app;
|
|
371
|
+
};
|
|
234
372
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
373
|
+
app.stop = () => {
|
|
374
|
+
for (const cleanup of app._cleanups) {
|
|
375
|
+
cleanup();
|
|
376
|
+
}
|
|
377
|
+
app._cleanups = [];
|
|
378
|
+
app.emit('stop');
|
|
379
|
+
return app;
|
|
380
|
+
};
|
|
239
381
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
382
|
+
app.inject = (pipeName, packet) => {
|
|
383
|
+
app.emit(pipeName, packet);
|
|
384
|
+
return app;
|
|
243
385
|
};
|
|
244
386
|
|
|
245
|
-
|
|
387
|
+
app.pipe = (name) => getPipe(name);
|
|
388
|
+
|
|
389
|
+
// Expose for subflow wiring
|
|
390
|
+
app._input = getPipe('in');
|
|
391
|
+
app._output = getPipe('out');
|
|
392
|
+
|
|
393
|
+
return app;
|
|
246
394
|
}
|
|
247
395
|
|
|
396
|
+
|
|
397
|
+
// ─────────────────────────────────────────────────────────────
|
|
398
|
+
// Subflow Helper
|
|
399
|
+
// ─────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
248
401
|
/**
|
|
249
|
-
* Create a subflow
|
|
402
|
+
* Create a subflow - a reusable flow component
|
|
403
|
+
* Uses 'in' and 'out' pipes for embedding
|
|
250
404
|
*/
|
|
251
|
-
function subflow(graph, context = {}) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Mark as subflow
|
|
256
|
-
flowObj._isSubflow = true;
|
|
257
|
-
flowObj._input = flowObj.getPipe('in');
|
|
258
|
-
flowObj._output = flowObj.getPipe('out');
|
|
259
|
-
|
|
260
|
-
return flowObj;
|
|
405
|
+
export function subflow(graph, context = {}) {
|
|
406
|
+
const sub = flow(graph, context);
|
|
407
|
+
sub._isSubflow = true;
|
|
408
|
+
return sub;
|
|
261
409
|
}
|
|
262
410
|
|
|
411
|
+
|
|
412
|
+
// ─────────────────────────────────────────────────────────────
|
|
413
|
+
// Compose Helper
|
|
414
|
+
// ─────────────────────────────────────────────────────────────
|
|
415
|
+
|
|
263
416
|
/**
|
|
264
|
-
* Compose multiple flows
|
|
417
|
+
* Compose multiple flows in sequence
|
|
265
418
|
*/
|
|
266
|
-
function compose(...flows) {
|
|
267
|
-
|
|
419
|
+
export function compose(...flows) {
|
|
420
|
+
const composed = new EventEmitter();
|
|
421
|
+
composed._isFlow = true;
|
|
422
|
+
composed._isComposed = true;
|
|
423
|
+
composed._flows = flows;
|
|
424
|
+
|
|
425
|
+
// Wire flows together
|
|
268
426
|
for (let i = 0; i < flows.length - 1; i++) {
|
|
269
427
|
const current = flows[i];
|
|
270
428
|
const next = flows[i + 1];
|
|
271
|
-
|
|
272
|
-
if (current._output && next._input) {
|
|
273
|
-
current._output.connect(next._input);
|
|
274
|
-
}
|
|
429
|
+
current.on('out', (packet) => next.emit('in', packet));
|
|
275
430
|
}
|
|
276
431
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return this;
|
|
296
|
-
},
|
|
297
|
-
|
|
298
|
-
inject(pipeName, packet) {
|
|
299
|
-
flows[0]?.inject(pipeName, packet);
|
|
300
|
-
return this;
|
|
432
|
+
// Expose first input, last output
|
|
433
|
+
composed._input = flows[0]?._input;
|
|
434
|
+
composed._output = flows[flows.length - 1]?._output;
|
|
435
|
+
|
|
436
|
+
composed.start = () => {
|
|
437
|
+
for (const f of flows) f.start?.();
|
|
438
|
+
return composed;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
composed.stop = () => {
|
|
442
|
+
for (const f of flows) f.stop?.();
|
|
443
|
+
return composed;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Forward 'in' to first flow
|
|
447
|
+
composed.emit = (event, packet) => {
|
|
448
|
+
if (event === 'in') {
|
|
449
|
+
flows[0]?.emit('in', packet);
|
|
301
450
|
}
|
|
451
|
+
return EventEmitter.prototype.emit.call(composed, event, packet);
|
|
302
452
|
};
|
|
303
|
-
}
|
|
304
453
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
isInner,
|
|
313
|
-
};
|
|
454
|
+
// Forward 'out' from last flow
|
|
455
|
+
flows[flows.length - 1]?.on('out', (packet) => {
|
|
456
|
+
EventEmitter.prototype.emit.call(composed, 'out', packet);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return composed;
|
|
460
|
+
}
|