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/src/nodes.js
DELETED
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node-RED style core nodes
|
|
3
|
-
*
|
|
4
|
-
* Each node is an "outer" function that takes options and returns
|
|
5
|
-
* an "inner" arrow function that processes packets.
|
|
6
|
-
*
|
|
7
|
-
* Inner function signature: (send, packet) => { ... }
|
|
8
|
-
* - send: function to emit packet to next node
|
|
9
|
-
* - packet: incoming data (called 'msg' in Node-RED)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Inject node - Produces packets on a schedule or trigger
|
|
14
|
-
*
|
|
15
|
-
* Options:
|
|
16
|
-
* - payload: value to inject (default: timestamp)
|
|
17
|
-
* - topic: message topic
|
|
18
|
-
* - interval: repeat interval in ms (optional)
|
|
19
|
-
* - once: inject once on start (default: true)
|
|
20
|
-
* - onceDelay: delay before first inject in ms (default: 0)
|
|
21
|
-
*/
|
|
22
|
-
function Inject(options = {}) {
|
|
23
|
-
const {
|
|
24
|
-
payload = () => Date.now(),
|
|
25
|
-
topic = '',
|
|
26
|
-
interval = null,
|
|
27
|
-
once = true,
|
|
28
|
-
onceDelay = 0,
|
|
29
|
-
} = options;
|
|
30
|
-
|
|
31
|
-
// Return producer function (only takes send, no packet)
|
|
32
|
-
return (send) => {
|
|
33
|
-
const timers = [];
|
|
34
|
-
|
|
35
|
-
function emit() {
|
|
36
|
-
const value = typeof payload === 'function' ? payload() : payload;
|
|
37
|
-
send({ payload: value, topic });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (once) {
|
|
41
|
-
if (onceDelay > 0) {
|
|
42
|
-
timers.push(setTimeout(emit, onceDelay));
|
|
43
|
-
} else {
|
|
44
|
-
// Use setImmediate to ensure flow is fully constructed
|
|
45
|
-
setImmediate(emit);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (interval && interval > 0) {
|
|
50
|
-
timers.push(setInterval(emit, interval));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Return cleanup function
|
|
54
|
-
return () => {
|
|
55
|
-
for (const t of timers) {
|
|
56
|
-
clearTimeout(t);
|
|
57
|
-
clearInterval(t);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Debug node - Logs packets to console
|
|
65
|
-
*
|
|
66
|
-
* Options:
|
|
67
|
-
* - name: label for output
|
|
68
|
-
* - active: whether to output (default: true)
|
|
69
|
-
* - complete: show complete msg object (default: false, shows payload only)
|
|
70
|
-
* - console: output to console (default: true)
|
|
71
|
-
* - sidebar: would output to sidebar in Node-RED (not implemented)
|
|
72
|
-
* - logger: custom logger function (default: console.log)
|
|
73
|
-
*/
|
|
74
|
-
function Debug(options = {}) {
|
|
75
|
-
const {
|
|
76
|
-
name = 'debug',
|
|
77
|
-
active = true,
|
|
78
|
-
complete = false,
|
|
79
|
-
console: useConsole = true,
|
|
80
|
-
logger = console.log,
|
|
81
|
-
} = options;
|
|
82
|
-
|
|
83
|
-
return (send, packet) => {
|
|
84
|
-
if (active && useConsole) {
|
|
85
|
-
const output = complete ? packet : packet?.payload;
|
|
86
|
-
logger(`[${name}]`, output);
|
|
87
|
-
}
|
|
88
|
-
send(packet);
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Function node - Execute custom JavaScript
|
|
94
|
-
*
|
|
95
|
-
* Options:
|
|
96
|
-
* - func: function(msg, context) that returns msg or array of msgs
|
|
97
|
-
* - outputs: number of outputs (default: 1)
|
|
98
|
-
* - initialize: setup function called once
|
|
99
|
-
* - finalize: cleanup function
|
|
100
|
-
*
|
|
101
|
-
* The func receives:
|
|
102
|
-
* - msg: the incoming packet
|
|
103
|
-
* - context: { global, flow, node } storage objects
|
|
104
|
-
*/
|
|
105
|
-
function Function(options = {}) {
|
|
106
|
-
const {
|
|
107
|
-
func = (msg) => msg,
|
|
108
|
-
outputs = 1,
|
|
109
|
-
initialize = null,
|
|
110
|
-
finalize = null,
|
|
111
|
-
} = options;
|
|
112
|
-
|
|
113
|
-
// Context storage (simplified)
|
|
114
|
-
const nodeContext = {};
|
|
115
|
-
const flowContext = {};
|
|
116
|
-
const globalContext = {};
|
|
117
|
-
|
|
118
|
-
if (initialize) {
|
|
119
|
-
initialize({ global: globalContext, flow: flowContext, node: nodeContext });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return (send, packet) => {
|
|
123
|
-
const context = {
|
|
124
|
-
global: globalContext,
|
|
125
|
-
flow: flowContext,
|
|
126
|
-
node: nodeContext,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const result = func(packet, context);
|
|
131
|
-
|
|
132
|
-
if (result === null || result === undefined) {
|
|
133
|
-
// Don't send anything
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (Array.isArray(result) && outputs > 1) {
|
|
138
|
-
// Multiple outputs - each array element goes to different output
|
|
139
|
-
// For now, we just send the first non-null
|
|
140
|
-
for (const msg of result) {
|
|
141
|
-
if (msg !== null && msg !== undefined) {
|
|
142
|
-
send(msg);
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
send(result);
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error('[Function] Error:', err);
|
|
151
|
-
send({ ...packet, error: err.message });
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Change node - Set, change, delete, or move message properties
|
|
158
|
-
*
|
|
159
|
-
* Options:
|
|
160
|
-
* - rules: array of rule objects
|
|
161
|
-
*
|
|
162
|
-
* Rule types:
|
|
163
|
-
* - { type: 'set', prop: 'payload', to: value }
|
|
164
|
-
* - { type: 'change', prop: 'payload', from: /regex/, to: 'replacement' }
|
|
165
|
-
* - { type: 'delete', prop: 'payload' }
|
|
166
|
-
* - { type: 'move', prop: 'payload', to: 'newProp' }
|
|
167
|
-
*/
|
|
168
|
-
function Change(options = {}) {
|
|
169
|
-
const { rules = [] } = options;
|
|
170
|
-
|
|
171
|
-
return (send, packet) => {
|
|
172
|
-
let msg = { ...packet };
|
|
173
|
-
|
|
174
|
-
for (const rule of rules) {
|
|
175
|
-
const { type, prop, to, from } = rule;
|
|
176
|
-
|
|
177
|
-
switch (type) {
|
|
178
|
-
case 'set':
|
|
179
|
-
setProperty(msg, prop, typeof to === 'function' ? to(msg) : to);
|
|
180
|
-
break;
|
|
181
|
-
|
|
182
|
-
case 'change':
|
|
183
|
-
const current = getProperty(msg, prop);
|
|
184
|
-
if (typeof current === 'string') {
|
|
185
|
-
setProperty(msg, prop, current.replace(from, to));
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
188
|
-
|
|
189
|
-
case 'delete':
|
|
190
|
-
deleteProperty(msg, prop);
|
|
191
|
-
break;
|
|
192
|
-
|
|
193
|
-
case 'move':
|
|
194
|
-
const value = getProperty(msg, prop);
|
|
195
|
-
deleteProperty(msg, prop);
|
|
196
|
-
setProperty(msg, to, value);
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
send(msg);
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Switch node - Route messages based on property values
|
|
207
|
-
*
|
|
208
|
-
* Options:
|
|
209
|
-
* - property: property to test (default: 'payload')
|
|
210
|
-
* - rules: array of test rules
|
|
211
|
-
* - checkall: check all rules or stop at first match (default: false)
|
|
212
|
-
*
|
|
213
|
-
* Rule types:
|
|
214
|
-
* - { type: 'eq', value: x } - equals
|
|
215
|
-
* - { type: 'neq', value: x } - not equals
|
|
216
|
-
* - { type: 'lt', value: x } - less than
|
|
217
|
-
* - { type: 'gt', value: x } - greater than
|
|
218
|
-
* - { type: 'lte', value: x } - less than or equal
|
|
219
|
-
* - { type: 'gte', value: x } - greater than or equal
|
|
220
|
-
* - { type: 'regex', value: /x/ } - regex match
|
|
221
|
-
* - { type: 'true' } - is true
|
|
222
|
-
* - { type: 'false' } - is false
|
|
223
|
-
* - { type: 'null' } - is null
|
|
224
|
-
* - { type: 'nnull' } - is not null
|
|
225
|
-
* - { type: 'else' } - no previous match
|
|
226
|
-
*/
|
|
227
|
-
function Switch(options = {}) {
|
|
228
|
-
const {
|
|
229
|
-
property = 'payload',
|
|
230
|
-
rules = [],
|
|
231
|
-
checkall = false,
|
|
232
|
-
} = options;
|
|
233
|
-
|
|
234
|
-
return (send, packet) => {
|
|
235
|
-
const value = getProperty(packet, property);
|
|
236
|
-
let matched = false;
|
|
237
|
-
|
|
238
|
-
for (const rule of rules) {
|
|
239
|
-
let isMatch = false;
|
|
240
|
-
|
|
241
|
-
switch (rule.type) {
|
|
242
|
-
case 'eq':
|
|
243
|
-
isMatch = value === rule.value;
|
|
244
|
-
break;
|
|
245
|
-
case 'neq':
|
|
246
|
-
isMatch = value !== rule.value;
|
|
247
|
-
break;
|
|
248
|
-
case 'lt':
|
|
249
|
-
isMatch = value < rule.value;
|
|
250
|
-
break;
|
|
251
|
-
case 'gt':
|
|
252
|
-
isMatch = value > rule.value;
|
|
253
|
-
break;
|
|
254
|
-
case 'lte':
|
|
255
|
-
isMatch = value <= rule.value;
|
|
256
|
-
break;
|
|
257
|
-
case 'gte':
|
|
258
|
-
isMatch = value >= rule.value;
|
|
259
|
-
break;
|
|
260
|
-
case 'regex':
|
|
261
|
-
isMatch = rule.value.test(String(value));
|
|
262
|
-
break;
|
|
263
|
-
case 'true':
|
|
264
|
-
isMatch = value === true;
|
|
265
|
-
break;
|
|
266
|
-
case 'false':
|
|
267
|
-
isMatch = value === false;
|
|
268
|
-
break;
|
|
269
|
-
case 'null':
|
|
270
|
-
isMatch = value === null || value === undefined;
|
|
271
|
-
break;
|
|
272
|
-
case 'nnull':
|
|
273
|
-
isMatch = value !== null && value !== undefined;
|
|
274
|
-
break;
|
|
275
|
-
case 'else':
|
|
276
|
-
isMatch = !matched;
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (isMatch) {
|
|
281
|
-
matched = true;
|
|
282
|
-
|
|
283
|
-
// If rule has a 'send' property, use that, otherwise send original
|
|
284
|
-
if (rule.send !== undefined) {
|
|
285
|
-
send(typeof rule.send === 'function' ? rule.send(packet) : { ...packet, ...rule.send });
|
|
286
|
-
} else {
|
|
287
|
-
send(packet);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!checkall) break;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Template node - Generate output using a template
|
|
298
|
-
*
|
|
299
|
-
* Options:
|
|
300
|
-
* - template: template string with {{mustache}} placeholders
|
|
301
|
-
* - field: output field (default: 'payload')
|
|
302
|
-
* - format: 'mustache' | 'plain' (default: 'mustache')
|
|
303
|
-
*/
|
|
304
|
-
function Template(options = {}) {
|
|
305
|
-
const {
|
|
306
|
-
template = '{{payload}}',
|
|
307
|
-
field = 'payload',
|
|
308
|
-
format = 'mustache',
|
|
309
|
-
} = options;
|
|
310
|
-
|
|
311
|
-
return (send, packet) => {
|
|
312
|
-
let output;
|
|
313
|
-
|
|
314
|
-
if (format === 'mustache') {
|
|
315
|
-
output = template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
316
|
-
const value = getProperty(packet, path.trim());
|
|
317
|
-
return value !== undefined ? String(value) : '';
|
|
318
|
-
});
|
|
319
|
-
} else {
|
|
320
|
-
output = template;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const result = { ...packet };
|
|
324
|
-
setProperty(result, field, output);
|
|
325
|
-
send(result);
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Delay node - Delay or rate-limit messages
|
|
331
|
-
*
|
|
332
|
-
* Options:
|
|
333
|
-
* - delay: delay in ms (default: 1000)
|
|
334
|
-
* - rate: rate limit (msgs per second, optional)
|
|
335
|
-
* - drop: drop messages when rate limited (default: false, queues instead)
|
|
336
|
-
*/
|
|
337
|
-
function Delay(options = {}) {
|
|
338
|
-
const {
|
|
339
|
-
delay = 1000,
|
|
340
|
-
rate = null,
|
|
341
|
-
drop = false,
|
|
342
|
-
} = options;
|
|
343
|
-
|
|
344
|
-
const queue = [];
|
|
345
|
-
let processing = false;
|
|
346
|
-
|
|
347
|
-
return (send, packet) => {
|
|
348
|
-
if (rate) {
|
|
349
|
-
// Rate limiting mode
|
|
350
|
-
const interval = 1000 / rate;
|
|
351
|
-
|
|
352
|
-
if (drop && processing) {
|
|
353
|
-
return; // Drop the message
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
queue.push(packet);
|
|
357
|
-
|
|
358
|
-
if (!processing) {
|
|
359
|
-
processing = true;
|
|
360
|
-
|
|
361
|
-
const processQueue = () => {
|
|
362
|
-
if (queue.length > 0) {
|
|
363
|
-
const msg = queue.shift();
|
|
364
|
-
send(msg);
|
|
365
|
-
setTimeout(processQueue, interval);
|
|
366
|
-
} else {
|
|
367
|
-
processing = false;
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
processQueue();
|
|
372
|
-
}
|
|
373
|
-
} else {
|
|
374
|
-
// Simple delay mode
|
|
375
|
-
setTimeout(() => send(packet), delay);
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Join node - Join sequences of messages
|
|
382
|
-
*
|
|
383
|
-
* Options:
|
|
384
|
-
* - mode: 'auto' | 'manual' | 'reduce'
|
|
385
|
-
* - count: number of messages to join
|
|
386
|
-
* - property: property to join (default: 'payload')
|
|
387
|
-
* - key: property to use as key for object mode
|
|
388
|
-
* - reducer: function(acc, msg) for reduce mode
|
|
389
|
-
* - initial: initial value for reduce
|
|
390
|
-
*/
|
|
391
|
-
function Join(options = {}) {
|
|
392
|
-
const {
|
|
393
|
-
mode = 'manual',
|
|
394
|
-
count = 0,
|
|
395
|
-
property = 'payload',
|
|
396
|
-
key = null,
|
|
397
|
-
reducer = null,
|
|
398
|
-
initial = [],
|
|
399
|
-
} = options;
|
|
400
|
-
|
|
401
|
-
let buffer = Array.isArray(initial) ? [...initial] : initial;
|
|
402
|
-
let msgCount = 0;
|
|
403
|
-
|
|
404
|
-
return (send, packet) => {
|
|
405
|
-
const value = getProperty(packet, property);
|
|
406
|
-
|
|
407
|
-
if (mode === 'reduce' && reducer) {
|
|
408
|
-
buffer = reducer(buffer, packet);
|
|
409
|
-
} else if (key) {
|
|
410
|
-
// Object mode
|
|
411
|
-
if (!buffer || typeof buffer !== 'object') buffer = {};
|
|
412
|
-
const k = getProperty(packet, key);
|
|
413
|
-
buffer[k] = value;
|
|
414
|
-
} else {
|
|
415
|
-
// Array mode
|
|
416
|
-
if (!Array.isArray(buffer)) buffer = [];
|
|
417
|
-
buffer.push(value);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
msgCount++;
|
|
421
|
-
|
|
422
|
-
if (count > 0 && msgCount >= count) {
|
|
423
|
-
send({ payload: buffer });
|
|
424
|
-
buffer = Array.isArray(initial) ? [...initial] : initial;
|
|
425
|
-
msgCount = 0;
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Split node - Split a message into a sequence
|
|
432
|
-
*
|
|
433
|
-
* Options:
|
|
434
|
-
* - property: property to split (default: 'payload')
|
|
435
|
-
* - delimiter: string delimiter for strings, or 'array' for arrays
|
|
436
|
-
*/
|
|
437
|
-
function Split(options = {}) {
|
|
438
|
-
const {
|
|
439
|
-
property = 'payload',
|
|
440
|
-
delimiter = 'array',
|
|
441
|
-
} = options;
|
|
442
|
-
|
|
443
|
-
return (send, packet) => {
|
|
444
|
-
const value = getProperty(packet, property);
|
|
445
|
-
|
|
446
|
-
if (Array.isArray(value)) {
|
|
447
|
-
value.forEach((item, index) => {
|
|
448
|
-
send({
|
|
449
|
-
...packet,
|
|
450
|
-
payload: item,
|
|
451
|
-
parts: { index, count: value.length }
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
} else if (typeof value === 'string' && delimiter !== 'array') {
|
|
455
|
-
const parts = value.split(delimiter);
|
|
456
|
-
parts.forEach((item, index) => {
|
|
457
|
-
send({
|
|
458
|
-
...packet,
|
|
459
|
-
payload: item,
|
|
460
|
-
parts: { index, count: parts.length }
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
} else {
|
|
464
|
-
send(packet);
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// === Helper functions for property access ===
|
|
470
|
-
|
|
471
|
-
function getProperty(obj, path) {
|
|
472
|
-
if (!path) return obj;
|
|
473
|
-
const parts = path.split('.');
|
|
474
|
-
let current = obj;
|
|
475
|
-
for (const part of parts) {
|
|
476
|
-
if (current === null || current === undefined) return undefined;
|
|
477
|
-
current = current[part];
|
|
478
|
-
}
|
|
479
|
-
return current;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function setProperty(obj, path, value) {
|
|
483
|
-
if (!path) return;
|
|
484
|
-
const parts = path.split('.');
|
|
485
|
-
let current = obj;
|
|
486
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
487
|
-
const part = parts[i];
|
|
488
|
-
if (current[part] === undefined) current[part] = {};
|
|
489
|
-
current = current[part];
|
|
490
|
-
}
|
|
491
|
-
current[parts[parts.length - 1]] = value;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function deleteProperty(obj, path) {
|
|
495
|
-
if (!path) return;
|
|
496
|
-
const parts = path.split('.');
|
|
497
|
-
let current = obj;
|
|
498
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
499
|
-
const part = parts[i];
|
|
500
|
-
if (current[part] === undefined) return;
|
|
501
|
-
current = current[part];
|
|
502
|
-
}
|
|
503
|
-
delete current[parts[parts.length - 1]];
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
export {
|
|
507
|
-
Inject,
|
|
508
|
-
Debug,
|
|
509
|
-
Function,
|
|
510
|
-
Change,
|
|
511
|
-
Switch,
|
|
512
|
-
Template,
|
|
513
|
-
Delay,
|
|
514
|
-
Join,
|
|
515
|
-
Split,
|
|
516
|
-
// Helpers
|
|
517
|
-
getProperty,
|
|
518
|
-
setProperty,
|
|
519
|
-
deleteProperty,
|
|
520
|
-
};
|