lulz 1.0.3 → 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/test.js
CHANGED
|
@@ -1,36 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lulz
|
|
2
|
+
* lulz - Test Suite
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
5
|
import {
|
|
8
6
|
flow,
|
|
9
7
|
subflow,
|
|
10
8
|
compose,
|
|
9
|
+
parallel,
|
|
10
|
+
series,
|
|
11
11
|
isOuter,
|
|
12
12
|
isInner,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
isFlow,
|
|
14
|
+
inject,
|
|
15
|
+
debug,
|
|
16
|
+
func,
|
|
17
|
+
change,
|
|
18
|
+
switchNode,
|
|
19
|
+
template,
|
|
20
|
+
delay,
|
|
21
|
+
split,
|
|
22
|
+
join,
|
|
23
|
+
map,
|
|
24
|
+
filter,
|
|
25
|
+
scan,
|
|
26
|
+
debounce,
|
|
27
|
+
throttle,
|
|
28
|
+
take,
|
|
29
|
+
skip,
|
|
30
|
+
distinct,
|
|
31
|
+
pairwise,
|
|
32
|
+
tap,
|
|
33
|
+
buffer,
|
|
34
|
+
combineLatest,
|
|
22
35
|
} from './index.js';
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
// Test Utilities
|
|
40
|
+
// ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
let testCount = 0;
|
|
43
|
+
let passCount = 0;
|
|
44
|
+
|
|
45
|
+
const test = (name, fn) => {
|
|
46
|
+
testCount++;
|
|
47
|
+
try {
|
|
48
|
+
fn();
|
|
49
|
+
passCount++;
|
|
50
|
+
console.log(`✓ ${name}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log(`✗ ${name}`);
|
|
53
|
+
console.log(` Error: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const asyncTest = async (name, fn, timeout = 500) => {
|
|
58
|
+
testCount++;
|
|
59
|
+
try {
|
|
60
|
+
await Promise.race([
|
|
61
|
+
fn(),
|
|
62
|
+
new Promise((_, reject) =>
|
|
63
|
+
setTimeout(() => reject(new Error('Timeout')), timeout)
|
|
64
|
+
),
|
|
65
|
+
]);
|
|
66
|
+
passCount++;
|
|
67
|
+
console.log(`✓ ${name}`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(`✗ ${name}`);
|
|
70
|
+
console.log(` Error: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const assert = (condition, message = 'Assertion failed') => {
|
|
75
|
+
if (!condition) throw new Error(message);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const assertEqual = (actual, expected, message = '') => {
|
|
79
|
+
if (actual !== expected) {
|
|
80
|
+
throw new Error(`${message} Expected ${expected}, got ${actual}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const assertDeepEqual = (actual, expected, message = '') => {
|
|
85
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
86
|
+
throw new Error(`${message} Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────────────────────────
|
|
92
|
+
// Inner/Outer Detection Tests
|
|
93
|
+
// ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
console.log('\n═══ Inner/Outer Detection ═══\n');
|
|
25
96
|
|
|
26
97
|
test('Regular function is outer', () => {
|
|
27
98
|
function outer() { return () => {}; }
|
|
28
|
-
assert
|
|
99
|
+
assert(isOuter(outer), 'Should detect regular function as outer');
|
|
29
100
|
});
|
|
30
101
|
|
|
31
102
|
test('Arrow function is inner', () => {
|
|
32
103
|
const inner = () => {};
|
|
33
|
-
assert
|
|
104
|
+
assert(isInner(inner), 'Should detect arrow function as inner');
|
|
34
105
|
});
|
|
35
106
|
|
|
36
107
|
test('Pre-called outer returns inner', () => {
|
|
@@ -38,393 +109,548 @@ test('Pre-called outer returns inner', () => {
|
|
|
38
109
|
return (send, packet) => send(packet);
|
|
39
110
|
}
|
|
40
111
|
const inner = factory({});
|
|
41
|
-
assert
|
|
42
|
-
assert
|
|
112
|
+
assert(isOuter(factory), 'Factory should be outer');
|
|
113
|
+
assert(isInner(inner), 'Result should be inner');
|
|
43
114
|
});
|
|
44
115
|
|
|
45
|
-
test('Simple pipe connection', (t, done) => {
|
|
46
|
-
const results = [];
|
|
47
116
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
}
|
|
117
|
+
// ─────────────────────────────────────────────────────────────
|
|
118
|
+
// Basic Flow Tests
|
|
119
|
+
// ─────────────────────────────────────────────────────────────
|
|
53
120
|
|
|
54
|
-
|
|
55
|
-
return (send, packet) => {
|
|
56
|
-
results.push(packet);
|
|
57
|
-
send(packet);
|
|
58
|
-
};
|
|
59
|
-
}
|
|
121
|
+
console.log('\n═══ Basic Flow ═══\n');
|
|
60
122
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
123
|
+
test('Flow is an EventEmitter', () => {
|
|
124
|
+
const app = flow([]);
|
|
125
|
+
assert(typeof app.on === 'function', 'Should have on method');
|
|
126
|
+
assert(typeof app.emit === 'function', 'Should have emit method');
|
|
127
|
+
assert(app._isFlow === true, 'Should be marked as flow');
|
|
74
128
|
});
|
|
75
129
|
|
|
76
|
-
test('Direct inject into pipe', () => {
|
|
130
|
+
test('Direct inject into pipe via emit', () => {
|
|
77
131
|
const results = [];
|
|
78
|
-
|
|
132
|
+
|
|
79
133
|
function collector(options) {
|
|
80
134
|
return (send, packet) => {
|
|
81
135
|
results.push(packet.payload);
|
|
82
136
|
send(packet);
|
|
83
137
|
};
|
|
84
138
|
}
|
|
85
|
-
|
|
139
|
+
|
|
86
140
|
const app = flow([
|
|
87
141
|
['input', collector],
|
|
88
142
|
]);
|
|
143
|
+
|
|
144
|
+
app.emit('input', { payload: 'hello' });
|
|
145
|
+
app.emit('input', { payload: 'world' });
|
|
146
|
+
|
|
147
|
+
assertEqual(results.length, 2, 'Should receive two packets');
|
|
148
|
+
assertEqual(results[0], 'hello');
|
|
149
|
+
assertEqual(results[1], 'world');
|
|
150
|
+
});
|
|
89
151
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
152
|
+
test('Listen to pipe via on', () => {
|
|
153
|
+
const results = [];
|
|
154
|
+
|
|
155
|
+
const app = flow([
|
|
156
|
+
['input', 'output'],
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
app.on('output', (packet) => {
|
|
160
|
+
results.push(packet.payload);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
app.emit('input', { payload: 'test' });
|
|
164
|
+
|
|
165
|
+
assertEqual(results.length, 1);
|
|
166
|
+
assertEqual(results[0], 'test');
|
|
96
167
|
});
|
|
97
168
|
|
|
98
|
-
test('Fan-out sends to multiple transforms', () => {
|
|
99
|
-
const results = { a: [], b: [], c: [] };
|
|
100
169
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
send({ ...packet, from: 'A' });
|
|
105
|
-
};
|
|
106
|
-
}
|
|
170
|
+
// ─────────────────────────────────────────────────────────────
|
|
171
|
+
// Series Processing (Default)
|
|
172
|
+
// ─────────────────────────────────────────────────────────────
|
|
107
173
|
|
|
108
|
-
|
|
109
|
-
return (send, packet) => {
|
|
110
|
-
results.b.push(packet.payload);
|
|
111
|
-
send({ ...packet, from: 'B' });
|
|
112
|
-
};
|
|
113
|
-
}
|
|
174
|
+
console.log('\n═══ Series Processing (Default) ═══\n');
|
|
114
175
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
176
|
+
test('Default is series: a → b → c', () => {
|
|
177
|
+
const order = [];
|
|
178
|
+
|
|
179
|
+
function step(name) {
|
|
180
|
+
return function(options) {
|
|
181
|
+
return (send, packet) => {
|
|
182
|
+
order.push(name);
|
|
183
|
+
send({ ...packet, steps: [...(packet.steps || []), name] });
|
|
184
|
+
};
|
|
119
185
|
};
|
|
120
186
|
}
|
|
121
|
-
|
|
187
|
+
|
|
122
188
|
const app = flow([
|
|
123
|
-
['input',
|
|
124
|
-
['output', collector],
|
|
189
|
+
['input', step('A'), step('B'), step('C'), 'output'],
|
|
125
190
|
]);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
|
|
192
|
+
let finalPacket = null;
|
|
193
|
+
app.on('output', (packet) => { finalPacket = packet; });
|
|
194
|
+
|
|
195
|
+
app.emit('input', { payload: 'start' });
|
|
196
|
+
|
|
197
|
+
assertDeepEqual(order, ['A', 'B', 'C'], 'Should process in order');
|
|
198
|
+
assertDeepEqual(finalPacket.steps, ['A', 'B', 'C'], 'Packet should have all steps');
|
|
132
199
|
});
|
|
133
200
|
|
|
134
|
-
test('
|
|
201
|
+
test('Explicit series() helper', () => {
|
|
135
202
|
const order = [];
|
|
136
|
-
|
|
203
|
+
|
|
137
204
|
function step(name) {
|
|
138
|
-
return function
|
|
205
|
+
return function(options) {
|
|
139
206
|
return (send, packet) => {
|
|
140
207
|
order.push(name);
|
|
141
|
-
send(
|
|
208
|
+
send(packet);
|
|
142
209
|
};
|
|
143
210
|
};
|
|
144
211
|
}
|
|
145
|
-
|
|
212
|
+
|
|
146
213
|
const app = flow([
|
|
147
|
-
['input',
|
|
214
|
+
['input', series(step('X'), step('Y'), step('Z')), 'output'],
|
|
148
215
|
]);
|
|
216
|
+
|
|
217
|
+
app.emit('input', { payload: 'test' });
|
|
218
|
+
|
|
219
|
+
assertDeepEqual(order, ['X', 'Y', 'Z']);
|
|
220
|
+
});
|
|
149
221
|
|
|
150
|
-
let finalPacket = null;
|
|
151
|
-
app.pipes['output'].connect({
|
|
152
|
-
receive: (packet) => { finalPacket = packet; }
|
|
153
|
-
});
|
|
154
222
|
|
|
155
|
-
|
|
223
|
+
// ─────────────────────────────────────────────────────────────
|
|
224
|
+
// Parallel Processing (Explicit)
|
|
225
|
+
// ─────────────────────────────────────────────────────────────
|
|
156
226
|
|
|
157
|
-
|
|
158
|
-
assert.deepStrictEqual(finalPacket.steps, ['A', 'B', 'C'], 'Packet should have all steps');
|
|
159
|
-
});
|
|
227
|
+
console.log('\n═══ Parallel Processing (Explicit) ═══\n');
|
|
160
228
|
|
|
161
|
-
test('
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
function
|
|
165
|
-
return (
|
|
166
|
-
|
|
167
|
-
|
|
229
|
+
test('Parallel with [] syntax', () => {
|
|
230
|
+
const results = { a: false, b: false, c: false };
|
|
231
|
+
|
|
232
|
+
function setFlag(name) {
|
|
233
|
+
return function(options) {
|
|
234
|
+
return (send, packet) => {
|
|
235
|
+
results[name] = true;
|
|
236
|
+
send({ ...packet, from: name });
|
|
237
|
+
};
|
|
168
238
|
};
|
|
169
239
|
}
|
|
170
|
-
|
|
240
|
+
|
|
171
241
|
const app = flow([
|
|
172
|
-
['input', [
|
|
173
|
-
configurable({ name: 'first' }),
|
|
174
|
-
configurable, // outer, will get empty config
|
|
175
|
-
configurable({ name: 'third' }),
|
|
176
|
-
], 'output'],
|
|
242
|
+
['input', [setFlag('a'), setFlag('b'), setFlag('c')], 'output'],
|
|
177
243
|
]);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
|
|
245
|
+
const outputs = [];
|
|
246
|
+
app.on('output', (packet) => outputs.push(packet.from));
|
|
247
|
+
|
|
248
|
+
app.emit('input', { payload: 'parallel' });
|
|
249
|
+
|
|
250
|
+
assert(results.a && results.b && results.c, 'All should receive packet');
|
|
251
|
+
assertEqual(outputs.length, 3, 'Output should receive 3 packets');
|
|
183
252
|
});
|
|
184
253
|
|
|
185
|
-
test('
|
|
254
|
+
test('Explicit parallel() helper', () => {
|
|
186
255
|
const results = [];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return (send, packet) => {
|
|
195
|
-
results.push(packet.payload);
|
|
196
|
-
send(packet);
|
|
256
|
+
|
|
257
|
+
function addResult(value) {
|
|
258
|
+
return function(options) {
|
|
259
|
+
return (send, packet) => {
|
|
260
|
+
results.push(value);
|
|
261
|
+
send(packet);
|
|
262
|
+
};
|
|
197
263
|
};
|
|
198
264
|
}
|
|
199
|
-
|
|
200
|
-
// Main flow: inject into subflow, subflow outputs to collector
|
|
201
|
-
// We wire: input → subflow.in → (doubles) → subflow.out → collector
|
|
265
|
+
|
|
202
266
|
const app = flow([
|
|
203
|
-
['
|
|
267
|
+
['input', parallel(addResult(1), addResult(2), addResult(3)), 'output'],
|
|
204
268
|
]);
|
|
269
|
+
|
|
270
|
+
app.emit('input', { payload: 'test' });
|
|
271
|
+
|
|
272
|
+
assertEqual(results.length, 3, 'All parallel branches should execute');
|
|
273
|
+
});
|
|
205
274
|
|
|
206
|
-
// Connect: inject directly into subflow, subflow outputs to 'output' pipe
|
|
207
|
-
doubler._output.connect(app.pipes['output']);
|
|
208
275
|
|
|
209
|
-
|
|
210
|
-
|
|
276
|
+
// ─────────────────────────────────────────────────────────────
|
|
277
|
+
// Pre-configured Functions
|
|
278
|
+
// ─────────────────────────────────────────────────────────────
|
|
211
279
|
|
|
212
|
-
|
|
213
|
-
});
|
|
280
|
+
console.log('\n═══ Pre-configured Functions ═══\n');
|
|
214
281
|
|
|
215
|
-
test('
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
function
|
|
282
|
+
test('Mixed pre-configured and auto-configured', () => {
|
|
283
|
+
const configs = [];
|
|
284
|
+
|
|
285
|
+
function configurable(options) {
|
|
219
286
|
return (send, packet) => {
|
|
220
|
-
|
|
287
|
+
configs.push(options.name || 'default');
|
|
221
288
|
send(packet);
|
|
222
289
|
};
|
|
223
290
|
}
|
|
224
|
-
|
|
291
|
+
|
|
225
292
|
const app = flow([
|
|
226
|
-
[
|
|
293
|
+
['input', configurable({ name: 'first' }), configurable, configurable({ name: 'third' }), 'output'],
|
|
227
294
|
]);
|
|
295
|
+
|
|
296
|
+
app.emit('input', { payload: 'test' });
|
|
297
|
+
|
|
298
|
+
assertEqual(configs[0], 'first');
|
|
299
|
+
assertEqual(configs[1], 'default'); // Auto-configured with {}
|
|
300
|
+
assertEqual(configs[2], 'third');
|
|
301
|
+
});
|
|
228
302
|
|
|
229
|
-
app.start();
|
|
230
303
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
304
|
+
// ─────────────────────────────────────────────────────────────
|
|
305
|
+
// Subflows and Auto-compose
|
|
306
|
+
// ─────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
console.log('\n═══ Subflows ═══\n');
|
|
309
|
+
|
|
310
|
+
test('Subflow with in/out pipes', () => {
|
|
311
|
+
const results = [];
|
|
312
|
+
|
|
313
|
+
const doubler = subflow([
|
|
314
|
+
['in', func({ func: (msg) => ({ ...msg, payload: msg.payload * 2 }) }), 'out'],
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
doubler.on('out', (packet) => results.push(packet.payload));
|
|
318
|
+
doubler.emit('in', { payload: 21 });
|
|
319
|
+
|
|
320
|
+
assertEqual(results[0], 42);
|
|
236
321
|
});
|
|
237
322
|
|
|
238
|
-
test('
|
|
239
|
-
const
|
|
323
|
+
test('Compose multiple flows', () => {
|
|
324
|
+
const flow1 = subflow([
|
|
325
|
+
['in', func({ func: (msg) => ({ ...msg, payload: msg.payload + 10 }) }), 'out'],
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
const flow2 = subflow([
|
|
329
|
+
['in', func({ func: (msg) => ({ ...msg, payload: msg.payload * 2 }) }), 'out'],
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
const composed = compose(flow1, flow2);
|
|
333
|
+
|
|
240
334
|
const results = [];
|
|
335
|
+
composed.on('out', (packet) => results.push(packet.payload));
|
|
336
|
+
|
|
337
|
+
composed.emit('in', { payload: 5 });
|
|
338
|
+
|
|
339
|
+
// (5 + 10) * 2 = 30
|
|
340
|
+
assertEqual(results[0], 30);
|
|
341
|
+
});
|
|
241
342
|
|
|
242
|
-
const customLogger = (...args) => logs.push(args);
|
|
243
343
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
344
|
+
// ─────────────────────────────────────────────────────────────
|
|
345
|
+
// Node-RED Core Nodes
|
|
346
|
+
// ─────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
console.log('\n═══ Node-RED Core Nodes ═══\n');
|
|
250
349
|
|
|
350
|
+
test('debug node logs and passes through', () => {
|
|
351
|
+
const logs = [];
|
|
352
|
+
const results = [];
|
|
353
|
+
|
|
251
354
|
const app = flow([
|
|
252
|
-
['input',
|
|
355
|
+
['input', debug({ name: 'test', logger: (...args) => logs.push(args) }), 'output'],
|
|
253
356
|
]);
|
|
254
|
-
|
|
255
|
-
app.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
357
|
+
|
|
358
|
+
app.on('output', (packet) => results.push(packet));
|
|
359
|
+
app.emit('input', { payload: 42 });
|
|
360
|
+
|
|
361
|
+
assertEqual(logs.length, 1);
|
|
362
|
+
assertEqual(logs[0][0], '[test]');
|
|
363
|
+
assertEqual(logs[0][1], 42);
|
|
364
|
+
assertEqual(results.length, 1);
|
|
261
365
|
});
|
|
262
366
|
|
|
263
|
-
test('
|
|
367
|
+
test('func node transforms messages', () => {
|
|
264
368
|
const results = [];
|
|
265
|
-
|
|
266
|
-
function collector(options) {
|
|
267
|
-
return (send, packet) => {
|
|
268
|
-
results.push(packet);
|
|
269
|
-
send(packet);
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
369
|
+
|
|
273
370
|
const app = flow([
|
|
274
|
-
['input',
|
|
275
|
-
func: (msg) => ({
|
|
276
|
-
...msg,
|
|
277
|
-
payload: msg.payload.toUpperCase()
|
|
278
|
-
})
|
|
279
|
-
}), collector],
|
|
371
|
+
['input', func({ func: (msg) => ({ ...msg, payload: msg.payload.toUpperCase() }) }), 'output'],
|
|
280
372
|
]);
|
|
281
|
-
|
|
282
|
-
app.
|
|
283
|
-
|
|
284
|
-
|
|
373
|
+
|
|
374
|
+
app.on('output', (packet) => results.push(packet));
|
|
375
|
+
app.emit('input', { payload: 'hello' });
|
|
376
|
+
|
|
377
|
+
assertEqual(results[0].payload, 'HELLO');
|
|
285
378
|
});
|
|
286
379
|
|
|
287
|
-
test('
|
|
380
|
+
test('change node sets properties', () => {
|
|
288
381
|
const results = [];
|
|
289
|
-
|
|
290
|
-
function collector(options) {
|
|
291
|
-
return (send, packet) => {
|
|
292
|
-
results.push(packet);
|
|
293
|
-
send(packet);
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
382
|
+
|
|
297
383
|
const app = flow([
|
|
298
|
-
['input',
|
|
384
|
+
['input', change({
|
|
299
385
|
rules: [
|
|
300
386
|
{ type: 'set', prop: 'payload', to: 'changed' },
|
|
301
387
|
{ type: 'set', prop: 'topic', to: 'test' },
|
|
302
388
|
]
|
|
303
|
-
}),
|
|
389
|
+
}), 'output'],
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
app.on('output', (packet) => results.push(packet));
|
|
393
|
+
app.emit('input', { payload: 'original' });
|
|
394
|
+
|
|
395
|
+
assertEqual(results[0].payload, 'changed');
|
|
396
|
+
assertEqual(results[0].topic, 'test');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('switchNode routes messages', () => {
|
|
400
|
+
const high = [];
|
|
401
|
+
|
|
402
|
+
const app = flow([
|
|
403
|
+
['input', switchNode({
|
|
404
|
+
property: 'payload',
|
|
405
|
+
rules: [{ type: 'gte', value: 50 }]
|
|
406
|
+
}), 'high'],
|
|
304
407
|
]);
|
|
408
|
+
|
|
409
|
+
app.on('high', (packet) => high.push(packet.payload));
|
|
410
|
+
|
|
411
|
+
app.emit('input', { payload: 75 });
|
|
412
|
+
app.emit('input', { payload: 25 });
|
|
413
|
+
|
|
414
|
+
assertEqual(high.length, 1);
|
|
415
|
+
assertEqual(high[0], 75);
|
|
416
|
+
});
|
|
305
417
|
|
|
306
|
-
|
|
418
|
+
test('template node renders mustache', () => {
|
|
419
|
+
const results = [];
|
|
420
|
+
|
|
421
|
+
const app = flow([
|
|
422
|
+
['input', template({ template: 'Hello {{name}}!' }), 'output'],
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
app.on('output', (packet) => results.push(packet));
|
|
426
|
+
app.emit('input', { payload: '', name: 'Alice' });
|
|
427
|
+
|
|
428
|
+
assertEqual(results[0].payload, 'Hello Alice!');
|
|
429
|
+
});
|
|
307
430
|
|
|
308
|
-
|
|
309
|
-
|
|
431
|
+
test('split node splits arrays', () => {
|
|
432
|
+
const results = [];
|
|
433
|
+
|
|
434
|
+
const app = flow([
|
|
435
|
+
['input', split(), 'output'],
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
439
|
+
app.emit('input', { payload: [1, 2, 3] });
|
|
440
|
+
|
|
441
|
+
assertEqual(results.length, 3);
|
|
442
|
+
assertDeepEqual(results, [1, 2, 3]);
|
|
310
443
|
});
|
|
311
444
|
|
|
312
|
-
test('Switch node routes messages', () => {
|
|
313
|
-
const results = { high: [], low: [] };
|
|
314
445
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
send(packet);
|
|
319
|
-
};
|
|
320
|
-
}
|
|
446
|
+
// ─────────────────────────────────────────────────────────────
|
|
447
|
+
// RxJS-Style Operators
|
|
448
|
+
// ─────────────────────────────────────────────────────────────
|
|
321
449
|
|
|
322
|
-
|
|
323
|
-
return (send, packet) => {
|
|
324
|
-
results.low.push(packet.payload);
|
|
325
|
-
send(packet);
|
|
326
|
-
};
|
|
327
|
-
}
|
|
450
|
+
console.log('\n═══ RxJS-Style Operators ═══\n');
|
|
328
451
|
|
|
452
|
+
test('map transforms values', () => {
|
|
453
|
+
const results = [];
|
|
454
|
+
|
|
329
455
|
const app = flow([
|
|
330
|
-
['input',
|
|
331
|
-
property: 'payload',
|
|
332
|
-
rules: [
|
|
333
|
-
{ type: 'gte', value: 50 },
|
|
334
|
-
]
|
|
335
|
-
}), 'high'],
|
|
336
|
-
['high', highCollector],
|
|
456
|
+
['input', map({ fn: (x) => x * 2 }), 'output'],
|
|
337
457
|
]);
|
|
338
|
-
|
|
339
|
-
app.
|
|
340
|
-
app.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
assert.strictEqual(results.high[0], 75);
|
|
458
|
+
|
|
459
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
460
|
+
app.emit('input', { payload: 21 });
|
|
461
|
+
|
|
462
|
+
assertEqual(results[0], 42);
|
|
344
463
|
});
|
|
345
464
|
|
|
346
|
-
test('
|
|
465
|
+
test('scan accumulates values', () => {
|
|
347
466
|
const results = [];
|
|
467
|
+
|
|
468
|
+
const app = flow([
|
|
469
|
+
['input', scan({ reducer: (acc, x) => acc + x, initial: 0 }), 'output'],
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
473
|
+
|
|
474
|
+
app.emit('input', { payload: 1 });
|
|
475
|
+
app.emit('input', { payload: 2 });
|
|
476
|
+
app.emit('input', { payload: 3 });
|
|
477
|
+
|
|
478
|
+
assertDeepEqual(results, [1, 3, 6]);
|
|
479
|
+
});
|
|
348
480
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
481
|
+
test('take only first N', () => {
|
|
482
|
+
const results = [];
|
|
483
|
+
|
|
484
|
+
const app = flow([
|
|
485
|
+
['input', take({ count: 2 }), 'output'],
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
489
|
+
|
|
490
|
+
app.emit('input', { payload: 1 });
|
|
491
|
+
app.emit('input', { payload: 2 });
|
|
492
|
+
app.emit('input', { payload: 3 });
|
|
493
|
+
|
|
494
|
+
assertEqual(results.length, 2);
|
|
495
|
+
assertDeepEqual(results, [1, 2]);
|
|
496
|
+
});
|
|
355
497
|
|
|
498
|
+
test('skip first N', () => {
|
|
499
|
+
const results = [];
|
|
500
|
+
|
|
356
501
|
const app = flow([
|
|
357
|
-
['input',
|
|
358
|
-
template: 'Hello {{name}}, you have {{count}} messages!'
|
|
359
|
-
}), collector],
|
|
502
|
+
['input', skip({ count: 2 }), 'output'],
|
|
360
503
|
]);
|
|
504
|
+
|
|
505
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
506
|
+
|
|
507
|
+
app.emit('input', { payload: 1 });
|
|
508
|
+
app.emit('input', { payload: 2 });
|
|
509
|
+
app.emit('input', { payload: 3 });
|
|
510
|
+
|
|
511
|
+
assertEqual(results.length, 1);
|
|
512
|
+
assertEqual(results[0], 3);
|
|
513
|
+
});
|
|
361
514
|
|
|
362
|
-
|
|
515
|
+
test('distinct filters duplicates', () => {
|
|
516
|
+
const results = [];
|
|
517
|
+
|
|
518
|
+
const app = flow([
|
|
519
|
+
['input', distinct(), 'output'],
|
|
520
|
+
]);
|
|
521
|
+
|
|
522
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
523
|
+
|
|
524
|
+
app.emit('input', { payload: 1 });
|
|
525
|
+
app.emit('input', { payload: 2 });
|
|
526
|
+
app.emit('input', { payload: 1 });
|
|
527
|
+
app.emit('input', { payload: 3 });
|
|
528
|
+
|
|
529
|
+
assertDeepEqual(results, [1, 2, 3]);
|
|
530
|
+
});
|
|
363
531
|
|
|
364
|
-
|
|
532
|
+
test('pairwise emits pairs', () => {
|
|
533
|
+
const results = [];
|
|
534
|
+
|
|
535
|
+
const app = flow([
|
|
536
|
+
['input', pairwise(), 'output'],
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
540
|
+
|
|
541
|
+
app.emit('input', { payload: 'a' });
|
|
542
|
+
app.emit('input', { payload: 'b' });
|
|
543
|
+
app.emit('input', { payload: 'c' });
|
|
544
|
+
|
|
545
|
+
assertEqual(results.length, 2);
|
|
546
|
+
assertDeepEqual(results[0], ['a', 'b']);
|
|
547
|
+
assertDeepEqual(results[1], ['b', 'c']);
|
|
365
548
|
});
|
|
366
549
|
|
|
367
|
-
test('
|
|
550
|
+
test('tap performs side effect', () => {
|
|
551
|
+
const sideEffects = [];
|
|
368
552
|
const results = [];
|
|
553
|
+
|
|
554
|
+
const app = flow([
|
|
555
|
+
['input', tap({ fn: (p) => sideEffects.push(p.payload) }), 'output'],
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
app.on('output', (packet) => results.push(packet.payload));
|
|
559
|
+
app.emit('input', { payload: 'test' });
|
|
560
|
+
|
|
561
|
+
assertEqual(sideEffects[0], 'test');
|
|
562
|
+
assertEqual(results[0], 'test');
|
|
563
|
+
});
|
|
369
564
|
|
|
370
|
-
function collector(options) {
|
|
371
|
-
return (send, packet) => {
|
|
372
|
-
results.push(packet.payload);
|
|
373
|
-
send(packet);
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
565
|
|
|
566
|
+
// ─────────────────────────────────────────────────────────────
|
|
567
|
+
// Node-RED Tutorial 1: Inject → Debug
|
|
568
|
+
// ─────────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
console.log('\n═══ Node-RED Tutorial 1 ═══\n');
|
|
571
|
+
|
|
572
|
+
await asyncTest('Tutorial 1: Inject timestamp to Debug', async () => {
|
|
573
|
+
const logs = [];
|
|
574
|
+
|
|
377
575
|
const app = flow([
|
|
378
|
-
['
|
|
576
|
+
[inject({ payload: () => Date.now(), once: true }), debug({ name: 'output', logger: (...args) => logs.push(args) })],
|
|
379
577
|
]);
|
|
578
|
+
|
|
579
|
+
app.start();
|
|
580
|
+
|
|
581
|
+
await new Promise(r => setTimeout(r, 50));
|
|
582
|
+
|
|
583
|
+
assert(logs.length >= 1, 'Should have logged');
|
|
584
|
+
assert(typeof logs[0][1] === 'number', 'Should be timestamp');
|
|
585
|
+
});
|
|
380
586
|
|
|
381
|
-
app.inject('input', { payload: [1, 2, 3] });
|
|
382
587
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
588
|
+
// ─────────────────────────────────────────────────────────────
|
|
589
|
+
// Node-RED Tutorial 2: Inject → Function → Debug
|
|
590
|
+
// ─────────────────────────────────────────────────────────────
|
|
386
591
|
|
|
387
|
-
|
|
388
|
-
const logs = [];
|
|
389
|
-
const customLogger = (...args) => logs.push(args);
|
|
592
|
+
console.log('\n═══ Node-RED Tutorial 2 ═══\n');
|
|
390
593
|
|
|
391
|
-
|
|
594
|
+
await asyncTest('Tutorial 2: Modify payload with Function node', async () => {
|
|
595
|
+
const logs = [];
|
|
596
|
+
|
|
392
597
|
const app = flow([
|
|
393
|
-
[
|
|
598
|
+
[inject({ payload: 'Hello World!', once: true }), 'msg'],
|
|
599
|
+
['msg', func({ func: (msg) => ({ ...msg, payload: msg.payload.toLowerCase() }) }), debug({ name: 'output', logger: (...args) => logs.push(args) })],
|
|
394
600
|
]);
|
|
395
|
-
|
|
601
|
+
|
|
396
602
|
app.start();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}, 50);
|
|
603
|
+
|
|
604
|
+
await new Promise(r => setTimeout(r, 50));
|
|
605
|
+
|
|
606
|
+
assert(logs.length >= 1, 'Should have logged');
|
|
607
|
+
assertEqual(logs[0][1], 'hello world!');
|
|
403
608
|
});
|
|
404
609
|
|
|
405
|
-
test('Tutorial 2: Modify payload with Function node', (t, done) => {
|
|
406
|
-
const logs = [];
|
|
407
|
-
const customLogger = (...args) => logs.push(args);
|
|
408
610
|
|
|
409
|
-
|
|
611
|
+
// ─────────────────────────────────────────────────────────────
|
|
612
|
+
// Complex Pipeline
|
|
613
|
+
// ─────────────────────────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
console.log('\n═══ Complex Pipeline ═══\n');
|
|
616
|
+
|
|
617
|
+
test('Series with parallel block', () => {
|
|
618
|
+
const order = [];
|
|
619
|
+
|
|
620
|
+
function step(name) {
|
|
621
|
+
return function(options) {
|
|
622
|
+
return (send, packet) => {
|
|
623
|
+
order.push(name);
|
|
624
|
+
send(packet);
|
|
625
|
+
};
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// This should: A → (B,C parallel) → D
|
|
410
630
|
const app = flow([
|
|
411
|
-
[
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}), 'msg'],
|
|
415
|
-
['msg', FunctionNode({
|
|
416
|
-
func: (msg) => ({
|
|
417
|
-
...msg,
|
|
418
|
-
payload: msg.payload.toLowerCase()
|
|
419
|
-
})
|
|
420
|
-
}), Debug({ name: 'output', logger: customLogger })],
|
|
631
|
+
['input', step('A'), 'stage1'],
|
|
632
|
+
['stage1', [step('B'), step('C')], 'stage2'],
|
|
633
|
+
['stage2', step('D'), 'output'],
|
|
421
634
|
]);
|
|
635
|
+
|
|
636
|
+
app.emit('input', { payload: 'test' });
|
|
637
|
+
|
|
638
|
+
assertEqual(order[0], 'A', 'A should be first');
|
|
639
|
+
assert(order.includes('B') && order.includes('C'), 'B and C should run');
|
|
640
|
+
// D runs twice (once for each parallel output)
|
|
641
|
+
});
|
|
422
642
|
|
|
423
|
-
app.start();
|
|
424
643
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
});
|
|
644
|
+
// ─────────────────────────────────────────────────────────────
|
|
645
|
+
// Results
|
|
646
|
+
// ─────────────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
console.log(`\n═══ Results: ${passCount}/${testCount} tests passed ═══\n`);
|
|
650
|
+
if (passCount === testCount) {
|
|
651
|
+
console.log('All tests passed! ✓\n');
|
|
652
|
+
} else {
|
|
653
|
+
console.log(`${testCount - passCount} test(s) failed.\n`);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
}, 300);
|