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/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
- };