lulz 1.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/CHICKEN.md +156 -0
- package/README.md +285 -0
- package/examples.js +317 -0
- package/index.js +21 -0
- package/package.json +20 -0
- package/src/flow.js +313 -0
- package/src/nodes.js +520 -0
- package/test.js +430 -0
package/test.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lulz Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
flow,
|
|
9
|
+
subflow,
|
|
10
|
+
compose,
|
|
11
|
+
isOuter,
|
|
12
|
+
isInner,
|
|
13
|
+
Inject,
|
|
14
|
+
Debug,
|
|
15
|
+
Function as FunctionNode,
|
|
16
|
+
Change,
|
|
17
|
+
Switch,
|
|
18
|
+
Template,
|
|
19
|
+
Delay,
|
|
20
|
+
Join,
|
|
21
|
+
Split,
|
|
22
|
+
} from './index.js';
|
|
23
|
+
|
|
24
|
+
// ============ TESTS ============
|
|
25
|
+
|
|
26
|
+
test('Regular function is outer', () => {
|
|
27
|
+
function outer() { return () => {}; }
|
|
28
|
+
assert.ok(isOuter(outer), 'Should detect regular function as outer');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('Arrow function is inner', () => {
|
|
32
|
+
const inner = () => {};
|
|
33
|
+
assert.ok(isInner(inner), 'Should detect arrow function as inner');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Pre-called outer returns inner', () => {
|
|
37
|
+
function factory(options) {
|
|
38
|
+
return (send, packet) => send(packet);
|
|
39
|
+
}
|
|
40
|
+
const inner = factory({});
|
|
41
|
+
assert.ok(isOuter(factory), 'Factory should be outer');
|
|
42
|
+
assert.ok(isInner(inner), 'Result should be inner');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Simple pipe connection', (t, done) => {
|
|
46
|
+
const results = [];
|
|
47
|
+
|
|
48
|
+
function producer(options) {
|
|
49
|
+
return (send) => {
|
|
50
|
+
send({ payload: 'test' });
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function consumer(options) {
|
|
55
|
+
return (send, packet) => {
|
|
56
|
+
results.push(packet);
|
|
57
|
+
send(packet);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const app = flow([
|
|
62
|
+
[producer, 'out'],
|
|
63
|
+
['out', consumer],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
app.start();
|
|
67
|
+
|
|
68
|
+
// Give it a tick
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
assert.strictEqual(results.length, 1, 'Should receive one packet');
|
|
71
|
+
assert.strictEqual(results[0].payload, 'test', 'Payload should match');
|
|
72
|
+
done();
|
|
73
|
+
}, 10);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('Direct inject into pipe', () => {
|
|
77
|
+
const results = [];
|
|
78
|
+
|
|
79
|
+
function collector(options) {
|
|
80
|
+
return (send, packet) => {
|
|
81
|
+
results.push(packet.payload);
|
|
82
|
+
send(packet);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const app = flow([
|
|
87
|
+
['input', collector],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
app.inject('input', { payload: 'hello' });
|
|
91
|
+
app.inject('input', { payload: 'world' });
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(results.length, 2, 'Should receive two packets');
|
|
94
|
+
assert.strictEqual(results[0], 'hello');
|
|
95
|
+
assert.strictEqual(results[1], 'world');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('Fan-out sends to multiple transforms', () => {
|
|
99
|
+
const results = { a: [], b: [], c: [] };
|
|
100
|
+
|
|
101
|
+
function transformA(options) {
|
|
102
|
+
return (send, packet) => {
|
|
103
|
+
results.a.push(packet.payload);
|
|
104
|
+
send({ ...packet, from: 'A' });
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function transformB(options) {
|
|
109
|
+
return (send, packet) => {
|
|
110
|
+
results.b.push(packet.payload);
|
|
111
|
+
send({ ...packet, from: 'B' });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function collector(options) {
|
|
116
|
+
return (send, packet) => {
|
|
117
|
+
results.c.push(packet.from);
|
|
118
|
+
send(packet);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const app = flow([
|
|
123
|
+
['input', transformA, transformB, 'output'],
|
|
124
|
+
['output', collector],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
app.inject('input', { payload: 42 });
|
|
128
|
+
|
|
129
|
+
assert.strictEqual(results.a.length, 1, 'A should receive packet');
|
|
130
|
+
assert.strictEqual(results.b.length, 1, 'B should receive packet');
|
|
131
|
+
assert.strictEqual(results.c.length, 2, 'Collector should receive from both');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('Series processes in order: [a, b, c]', () => {
|
|
135
|
+
const order = [];
|
|
136
|
+
|
|
137
|
+
function step(name) {
|
|
138
|
+
return function factory(options) {
|
|
139
|
+
return (send, packet) => {
|
|
140
|
+
order.push(name);
|
|
141
|
+
send({ ...packet, steps: [...(packet.steps || []), name] });
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const app = flow([
|
|
147
|
+
['input', [step('A'), step('B'), step('C')], 'output'],
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
let finalPacket = null;
|
|
151
|
+
app.pipes['output'].connect({
|
|
152
|
+
receive: (packet) => { finalPacket = packet; }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.inject('input', { payload: 'start' });
|
|
156
|
+
|
|
157
|
+
assert.deepStrictEqual(order, ['A', 'B', 'C'], 'Should process in order');
|
|
158
|
+
assert.deepStrictEqual(finalPacket.steps, ['A', 'B', 'C'], 'Packet should have all steps');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Series with pre-configured function', () => {
|
|
162
|
+
const order = [];
|
|
163
|
+
|
|
164
|
+
function configurable(options) {
|
|
165
|
+
return (send, packet) => {
|
|
166
|
+
order.push(options.name);
|
|
167
|
+
send(packet);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const app = flow([
|
|
172
|
+
['input', [
|
|
173
|
+
configurable({ name: 'first' }),
|
|
174
|
+
configurable, // outer, will get empty config
|
|
175
|
+
configurable({ name: 'third' }),
|
|
176
|
+
], 'output'],
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
app.inject('input', { payload: 'test' });
|
|
180
|
+
|
|
181
|
+
assert.strictEqual(order[0], 'first');
|
|
182
|
+
assert.strictEqual(order[2], 'third');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('Subflow can be embedded', () => {
|
|
186
|
+
const results = [];
|
|
187
|
+
|
|
188
|
+
// Create a subflow that doubles the payload
|
|
189
|
+
const doubler = subflow([
|
|
190
|
+
['in', FunctionNode({ func: (msg) => ({ ...msg, payload: msg.payload * 2 }) }), 'out'],
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
function collector(options) {
|
|
194
|
+
return (send, packet) => {
|
|
195
|
+
results.push(packet.payload);
|
|
196
|
+
send(packet);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Main flow: inject into subflow, subflow outputs to collector
|
|
201
|
+
// We wire: input → subflow.in → (doubles) → subflow.out → collector
|
|
202
|
+
const app = flow([
|
|
203
|
+
['output', collector],
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Connect: inject directly into subflow, subflow outputs to 'output' pipe
|
|
207
|
+
doubler._output.connect(app.pipes['output']);
|
|
208
|
+
|
|
209
|
+
// Inject into subflow's input
|
|
210
|
+
doubler._input.receive({ payload: 21 });
|
|
211
|
+
|
|
212
|
+
assert.strictEqual(results[0], 42, 'Subflow should double the value');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('Inject node produces packets', (t, done) => {
|
|
216
|
+
const results = [];
|
|
217
|
+
|
|
218
|
+
function collector(options) {
|
|
219
|
+
return (send, packet) => {
|
|
220
|
+
results.push(packet);
|
|
221
|
+
send(packet);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const app = flow([
|
|
226
|
+
[Inject({ payload: 'hello', once: true }), collector],
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
app.start();
|
|
230
|
+
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
assert.ok(results.length >= 1, 'Should produce at least one packet');
|
|
233
|
+
assert.strictEqual(results[0].payload, 'hello');
|
|
234
|
+
done();
|
|
235
|
+
}, 50);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('Debug node logs and passes through', () => {
|
|
239
|
+
const logs = [];
|
|
240
|
+
const results = [];
|
|
241
|
+
|
|
242
|
+
const customLogger = (...args) => logs.push(args);
|
|
243
|
+
|
|
244
|
+
function collector(options) {
|
|
245
|
+
return (send, packet) => {
|
|
246
|
+
results.push(packet);
|
|
247
|
+
send(packet);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const app = flow([
|
|
252
|
+
['input', Debug({ name: 'test', logger: customLogger }), collector],
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
app.inject('input', { payload: 42 });
|
|
256
|
+
|
|
257
|
+
assert.strictEqual(logs.length, 1, 'Should log once');
|
|
258
|
+
assert.strictEqual(logs[0][0], '[test]', 'Should have correct label');
|
|
259
|
+
assert.strictEqual(logs[0][1], 42, 'Should log payload');
|
|
260
|
+
assert.strictEqual(results.length, 1, 'Should pass through');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('Function node transforms messages', () => {
|
|
264
|
+
const results = [];
|
|
265
|
+
|
|
266
|
+
function collector(options) {
|
|
267
|
+
return (send, packet) => {
|
|
268
|
+
results.push(packet);
|
|
269
|
+
send(packet);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const app = flow([
|
|
274
|
+
['input', FunctionNode({
|
|
275
|
+
func: (msg) => ({
|
|
276
|
+
...msg,
|
|
277
|
+
payload: msg.payload.toUpperCase()
|
|
278
|
+
})
|
|
279
|
+
}), collector],
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
app.inject('input', { payload: 'hello' });
|
|
283
|
+
|
|
284
|
+
assert.strictEqual(results[0].payload, 'HELLO');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('Change node sets properties', () => {
|
|
288
|
+
const results = [];
|
|
289
|
+
|
|
290
|
+
function collector(options) {
|
|
291
|
+
return (send, packet) => {
|
|
292
|
+
results.push(packet);
|
|
293
|
+
send(packet);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const app = flow([
|
|
298
|
+
['input', Change({
|
|
299
|
+
rules: [
|
|
300
|
+
{ type: 'set', prop: 'payload', to: 'changed' },
|
|
301
|
+
{ type: 'set', prop: 'topic', to: 'test' },
|
|
302
|
+
]
|
|
303
|
+
}), collector],
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
app.inject('input', { payload: 'original' });
|
|
307
|
+
|
|
308
|
+
assert.strictEqual(results[0].payload, 'changed');
|
|
309
|
+
assert.strictEqual(results[0].topic, 'test');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('Switch node routes messages', () => {
|
|
313
|
+
const results = { high: [], low: [] };
|
|
314
|
+
|
|
315
|
+
function highCollector(options) {
|
|
316
|
+
return (send, packet) => {
|
|
317
|
+
results.high.push(packet.payload);
|
|
318
|
+
send(packet);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function lowCollector(options) {
|
|
323
|
+
return (send, packet) => {
|
|
324
|
+
results.low.push(packet.payload);
|
|
325
|
+
send(packet);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const app = flow([
|
|
330
|
+
['input', Switch({
|
|
331
|
+
property: 'payload',
|
|
332
|
+
rules: [
|
|
333
|
+
{ type: 'gte', value: 50 },
|
|
334
|
+
]
|
|
335
|
+
}), 'high'],
|
|
336
|
+
['high', highCollector],
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
app.inject('input', { payload: 75 });
|
|
340
|
+
app.inject('input', { payload: 25 });
|
|
341
|
+
|
|
342
|
+
assert.strictEqual(results.high.length, 1, 'Only high value should route');
|
|
343
|
+
assert.strictEqual(results.high[0], 75);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('Template node renders mustache', () => {
|
|
347
|
+
const results = [];
|
|
348
|
+
|
|
349
|
+
function collector(options) {
|
|
350
|
+
return (send, packet) => {
|
|
351
|
+
results.push(packet);
|
|
352
|
+
send(packet);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const app = flow([
|
|
357
|
+
['input', Template({
|
|
358
|
+
template: 'Hello {{name}}, you have {{count}} messages!'
|
|
359
|
+
}), collector],
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
app.inject('input', { payload: '', name: 'Alice', count: 5 });
|
|
363
|
+
|
|
364
|
+
assert.strictEqual(results[0].payload, 'Hello Alice, you have 5 messages!');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('Split node splits arrays', () => {
|
|
368
|
+
const results = [];
|
|
369
|
+
|
|
370
|
+
function collector(options) {
|
|
371
|
+
return (send, packet) => {
|
|
372
|
+
results.push(packet.payload);
|
|
373
|
+
send(packet);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const app = flow([
|
|
378
|
+
['input', Split(), collector],
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
app.inject('input', { payload: [1, 2, 3] });
|
|
382
|
+
|
|
383
|
+
assert.strictEqual(results.length, 3);
|
|
384
|
+
assert.deepStrictEqual(results, [1, 2, 3]);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('Tutorial 1: Inject timestamp to Debug', (t, done) => {
|
|
388
|
+
const logs = [];
|
|
389
|
+
const customLogger = (...args) => logs.push(args);
|
|
390
|
+
|
|
391
|
+
// Recreate first tutorial flow
|
|
392
|
+
const app = flow([
|
|
393
|
+
[Inject({ payload: () => Date.now(), once: true }), Debug({ name: 'debug', logger: customLogger })],
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
app.start();
|
|
397
|
+
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
assert.ok(logs.length >= 1, 'Should have logged');
|
|
400
|
+
assert.strictEqual(typeof logs[0][1], 'number', 'Should be timestamp');
|
|
401
|
+
done();
|
|
402
|
+
}, 50);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('Tutorial 2: Modify payload with Function node', (t, done) => {
|
|
406
|
+
const logs = [];
|
|
407
|
+
const customLogger = (...args) => logs.push(args);
|
|
408
|
+
|
|
409
|
+
// Second tutorial: inject → function (modify payload) → debug
|
|
410
|
+
const app = flow([
|
|
411
|
+
[Inject({
|
|
412
|
+
payload: 'Hello World!',
|
|
413
|
+
once: true
|
|
414
|
+
}), 'msg'],
|
|
415
|
+
['msg', FunctionNode({
|
|
416
|
+
func: (msg) => ({
|
|
417
|
+
...msg,
|
|
418
|
+
payload: msg.payload.toLowerCase()
|
|
419
|
+
})
|
|
420
|
+
}), Debug({ name: 'output', logger: customLogger })],
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
app.start();
|
|
424
|
+
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
assert.ok(logs.length >= 1, 'Should have logged');
|
|
427
|
+
assert.strictEqual(logs[0][1], 'hello world!', 'Should be lowercase');
|
|
428
|
+
done();
|
|
429
|
+
}, 50);
|
|
430
|
+
});
|