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/red-lib.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lulz - Node-RED Style Library
|
|
3
|
+
*
|
|
4
|
+
* Core nodes inspired by Node-RED.
|
|
5
|
+
*
|
|
6
|
+
* Naming Convention:
|
|
7
|
+
* - Factory functions: lowercase (e.g., delay, inject, debug)
|
|
8
|
+
* - Called with options: delay({ delay: 1000 })
|
|
9
|
+
* - Called without: delay → uses sane defaults or warns
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────────
|
|
14
|
+
// Property Helpers
|
|
15
|
+
// ─────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export const getProperty = (obj, path) => {
|
|
18
|
+
if (!path) return obj;
|
|
19
|
+
const parts = path.split('.');
|
|
20
|
+
let current = obj;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (current === null || current === undefined) return undefined;
|
|
23
|
+
current = current[part];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const setProperty = (obj, path, value) => {
|
|
29
|
+
if (!path) return;
|
|
30
|
+
const parts = path.split('.');
|
|
31
|
+
let current = obj;
|
|
32
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
33
|
+
const part = parts[i];
|
|
34
|
+
if (current[part] === undefined) current[part] = {};
|
|
35
|
+
current = current[part];
|
|
36
|
+
}
|
|
37
|
+
current[parts[parts.length - 1]] = value;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const deleteProperty = (obj, path) => {
|
|
41
|
+
if (!path) return;
|
|
42
|
+
const parts = path.split('.');
|
|
43
|
+
let current = obj;
|
|
44
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
45
|
+
const part = parts[i];
|
|
46
|
+
if (current[part] === undefined) return;
|
|
47
|
+
current = current[part];
|
|
48
|
+
}
|
|
49
|
+
delete current[parts[parts.length - 1]];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────
|
|
54
|
+
// Inject - Produces packets
|
|
55
|
+
// ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Inject node - Produces packets on schedule or trigger
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @param {*} options.payload - Value to inject (or function for dynamic)
|
|
62
|
+
* @param {string} options.topic - Message topic
|
|
63
|
+
* @param {number} options.interval - Repeat interval in ms
|
|
64
|
+
* @param {boolean} options.once - Inject once on start (default: true)
|
|
65
|
+
* @param {number} options.onceDelay - Delay before first inject
|
|
66
|
+
*/
|
|
67
|
+
export function inject(options = {}) {
|
|
68
|
+
const {
|
|
69
|
+
payload = () => Date.now(),
|
|
70
|
+
topic = '',
|
|
71
|
+
interval = null,
|
|
72
|
+
once = true,
|
|
73
|
+
onceDelay = 0,
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
return (send) => {
|
|
77
|
+
const timers = [];
|
|
78
|
+
|
|
79
|
+
const emit = () => {
|
|
80
|
+
const value = typeof payload === 'function' ? payload() : payload;
|
|
81
|
+
send({ payload: value, topic });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (once) {
|
|
85
|
+
if (onceDelay > 0) {
|
|
86
|
+
timers.push(setTimeout(emit, onceDelay));
|
|
87
|
+
} else {
|
|
88
|
+
setImmediate(emit);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (interval && interval > 0) {
|
|
93
|
+
timers.push(setInterval(emit, interval));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cleanup function
|
|
97
|
+
return () => {
|
|
98
|
+
for (const t of timers) {
|
|
99
|
+
clearTimeout(t);
|
|
100
|
+
clearInterval(t);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────
|
|
108
|
+
// Debug - Logs packets
|
|
109
|
+
// ─────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Debug node - Logs packets to console
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} options
|
|
115
|
+
* @param {string} options.name - Label for output
|
|
116
|
+
* @param {boolean} options.active - Whether to output (default: true)
|
|
117
|
+
* @param {boolean} options.complete - Show complete msg (default: false)
|
|
118
|
+
* @param {Function} options.logger - Custom logger (default: console.log)
|
|
119
|
+
*/
|
|
120
|
+
export function debug(options = {}) {
|
|
121
|
+
const {
|
|
122
|
+
name = 'debug',
|
|
123
|
+
active = true,
|
|
124
|
+
complete = false,
|
|
125
|
+
logger = console.log,
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
128
|
+
return (send, packet) => {
|
|
129
|
+
if (active) {
|
|
130
|
+
const output = complete ? packet : packet?.payload;
|
|
131
|
+
logger(`[${name}]`, output);
|
|
132
|
+
}
|
|
133
|
+
send(packet);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────
|
|
139
|
+
// Function - Execute custom code
|
|
140
|
+
// ─────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Function node - Execute custom JavaScript
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} options
|
|
146
|
+
* @param {Function} options.func - Function(msg, context) → msg
|
|
147
|
+
* @param {Function} options.initialize - Setup function
|
|
148
|
+
* @param {Function} options.finalize - Cleanup function
|
|
149
|
+
*/
|
|
150
|
+
export function func(options = {}) {
|
|
151
|
+
const {
|
|
152
|
+
func: fn = (msg) => msg,
|
|
153
|
+
initialize = null,
|
|
154
|
+
finalize = null,
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
// Context storage
|
|
158
|
+
const nodeContext = {};
|
|
159
|
+
const flowContext = {};
|
|
160
|
+
const globalContext = {};
|
|
161
|
+
|
|
162
|
+
if (initialize) {
|
|
163
|
+
initialize({ global: globalContext, flow: flowContext, node: nodeContext });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (send, packet) => {
|
|
167
|
+
const context = {
|
|
168
|
+
global: globalContext,
|
|
169
|
+
flow: flowContext,
|
|
170
|
+
node: nodeContext,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const result = fn(packet, context);
|
|
175
|
+
|
|
176
|
+
if (result === null || result === undefined) {
|
|
177
|
+
return; // Drop message
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (Array.isArray(result)) {
|
|
181
|
+
for (const msg of result) {
|
|
182
|
+
if (msg !== null && msg !== undefined) {
|
|
183
|
+
send(msg);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
send(result);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error('[func] Error:', err);
|
|
191
|
+
send({ ...packet, error: err.message });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// ─────────────────────────────────────────────────────────────
|
|
198
|
+
// Change - Modify message properties
|
|
199
|
+
// ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Change node - Set, change, delete, or move message properties
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} options
|
|
205
|
+
* @param {Array} options.rules - Array of transformation rules
|
|
206
|
+
*
|
|
207
|
+
* Rule types:
|
|
208
|
+
* { type: 'set', prop: 'payload', to: value }
|
|
209
|
+
* { type: 'change', prop: 'payload', from: /regex/, to: 'replacement' }
|
|
210
|
+
* { type: 'delete', prop: 'payload' }
|
|
211
|
+
* { type: 'move', prop: 'payload', to: 'newProp' }
|
|
212
|
+
*/
|
|
213
|
+
export function change(options = {}) {
|
|
214
|
+
const { rules = [] } = options;
|
|
215
|
+
|
|
216
|
+
if (rules.length === 0) {
|
|
217
|
+
console.warn('[change] No rules configured - pass-through mode');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (send, packet) => {
|
|
221
|
+
let msg = { ...packet };
|
|
222
|
+
|
|
223
|
+
for (const rule of rules) {
|
|
224
|
+
const { type, prop, to, from } = rule;
|
|
225
|
+
|
|
226
|
+
switch (type) {
|
|
227
|
+
case 'set':
|
|
228
|
+
setProperty(msg, prop, typeof to === 'function' ? to(msg) : to);
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'change':
|
|
232
|
+
const current = getProperty(msg, prop);
|
|
233
|
+
if (typeof current === 'string') {
|
|
234
|
+
setProperty(msg, prop, current.replace(from, to));
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case 'delete':
|
|
239
|
+
deleteProperty(msg, prop);
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'move':
|
|
243
|
+
const value = getProperty(msg, prop);
|
|
244
|
+
deleteProperty(msg, prop);
|
|
245
|
+
setProperty(msg, to, value);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
send(msg);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
// ─────────────────────────────────────────────────────────────
|
|
256
|
+
// Switch - Route messages
|
|
257
|
+
// ─────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Switch node - Route messages based on conditions
|
|
261
|
+
*
|
|
262
|
+
* @param {Object} options
|
|
263
|
+
* @param {string} options.property - Property to test (default: 'payload')
|
|
264
|
+
* @param {Array} options.rules - Array of test rules
|
|
265
|
+
* @param {boolean} options.checkall - Check all rules (default: false)
|
|
266
|
+
*
|
|
267
|
+
* Rule types: eq, neq, lt, gt, lte, gte, regex, true, false, null, nnull, else
|
|
268
|
+
*/
|
|
269
|
+
export function switchNode(options = {}) {
|
|
270
|
+
const {
|
|
271
|
+
property = 'payload',
|
|
272
|
+
rules = [],
|
|
273
|
+
checkall = false,
|
|
274
|
+
} = options;
|
|
275
|
+
|
|
276
|
+
if (rules.length === 0) {
|
|
277
|
+
console.warn('[switch] No rules configured - pass-through mode');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (send, packet) => {
|
|
281
|
+
const value = getProperty(packet, property);
|
|
282
|
+
let matched = false;
|
|
283
|
+
|
|
284
|
+
for (const rule of rules) {
|
|
285
|
+
let isMatch = false;
|
|
286
|
+
|
|
287
|
+
switch (rule.type) {
|
|
288
|
+
case 'eq': isMatch = value === rule.value; break;
|
|
289
|
+
case 'neq': isMatch = value !== rule.value; break;
|
|
290
|
+
case 'lt': isMatch = value < rule.value; break;
|
|
291
|
+
case 'gt': isMatch = value > rule.value; break;
|
|
292
|
+
case 'lte': isMatch = value <= rule.value; break;
|
|
293
|
+
case 'gte': isMatch = value >= rule.value; break;
|
|
294
|
+
case 'regex': isMatch = rule.value.test(String(value)); break;
|
|
295
|
+
case 'true': isMatch = value === true; break;
|
|
296
|
+
case 'false': isMatch = value === false; break;
|
|
297
|
+
case 'null': isMatch = value === null || value === undefined; break;
|
|
298
|
+
case 'nnull': isMatch = value !== null && value !== undefined; break;
|
|
299
|
+
case 'else': isMatch = !matched; break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isMatch) {
|
|
303
|
+
matched = true;
|
|
304
|
+
send(rule.send !== undefined
|
|
305
|
+
? (typeof rule.send === 'function' ? rule.send(packet) : { ...packet, ...rule.send })
|
|
306
|
+
: packet
|
|
307
|
+
);
|
|
308
|
+
if (!checkall) break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
// ─────────────────────────────────────────────────────────────
|
|
316
|
+
// Template - Render templates
|
|
317
|
+
// ─────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Template node - Render mustache-style templates
|
|
321
|
+
*
|
|
322
|
+
* @param {Object} options
|
|
323
|
+
* @param {string} options.template - Template string with {{placeholders}}
|
|
324
|
+
* @param {string} options.field - Output field (default: 'payload')
|
|
325
|
+
*/
|
|
326
|
+
export function template(options = {}) {
|
|
327
|
+
const {
|
|
328
|
+
template: tmpl = '{{payload}}',
|
|
329
|
+
field = 'payload',
|
|
330
|
+
} = options;
|
|
331
|
+
|
|
332
|
+
return (send, packet) => {
|
|
333
|
+
const output = tmpl.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
334
|
+
const value = getProperty(packet, path.trim());
|
|
335
|
+
return value !== undefined ? String(value) : '';
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const result = { ...packet };
|
|
339
|
+
setProperty(result, field, output);
|
|
340
|
+
send(result);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
// ─────────────────────────────────────────────────────────────
|
|
346
|
+
// Delay - Delay or rate-limit
|
|
347
|
+
// ─────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Delay node - Delay or rate-limit messages
|
|
351
|
+
*
|
|
352
|
+
* @param {Object} options
|
|
353
|
+
* @param {number} options.delay - Delay in ms (default: 1000)
|
|
354
|
+
* @param {number} options.rate - Rate limit (msgs/sec)
|
|
355
|
+
* @param {boolean} options.drop - Drop when rate limited (default: false)
|
|
356
|
+
*/
|
|
357
|
+
export function delay(options = {}) {
|
|
358
|
+
const {
|
|
359
|
+
delay: delayMs = 1000,
|
|
360
|
+
rate = null,
|
|
361
|
+
drop = false,
|
|
362
|
+
} = options;
|
|
363
|
+
|
|
364
|
+
const queue = [];
|
|
365
|
+
let processing = false;
|
|
366
|
+
|
|
367
|
+
return (send, packet) => {
|
|
368
|
+
if (rate) {
|
|
369
|
+
const interval = 1000 / rate;
|
|
370
|
+
|
|
371
|
+
if (drop && processing) return;
|
|
372
|
+
|
|
373
|
+
queue.push(packet);
|
|
374
|
+
|
|
375
|
+
if (!processing) {
|
|
376
|
+
processing = true;
|
|
377
|
+
|
|
378
|
+
const processQueue = () => {
|
|
379
|
+
if (queue.length > 0) {
|
|
380
|
+
send(queue.shift());
|
|
381
|
+
setTimeout(processQueue, interval);
|
|
382
|
+
} else {
|
|
383
|
+
processing = false;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
processQueue();
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
setTimeout(() => send(packet), delayMs);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// ─────────────────────────────────────────────────────────────
|
|
397
|
+
// Split - Split messages
|
|
398
|
+
// ─────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Split node - Split arrays/strings into sequences
|
|
402
|
+
*
|
|
403
|
+
* @param {Object} options
|
|
404
|
+
* @param {string} options.property - Property to split (default: 'payload')
|
|
405
|
+
* @param {string} options.delimiter - Delimiter for strings
|
|
406
|
+
*/
|
|
407
|
+
export function split(options = {}) {
|
|
408
|
+
const {
|
|
409
|
+
property = 'payload',
|
|
410
|
+
delimiter = null,
|
|
411
|
+
} = options;
|
|
412
|
+
|
|
413
|
+
return (send, packet) => {
|
|
414
|
+
const value = getProperty(packet, property);
|
|
415
|
+
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
value.forEach((item, index) => {
|
|
418
|
+
send({
|
|
419
|
+
...packet,
|
|
420
|
+
payload: item,
|
|
421
|
+
parts: { index, count: value.length }
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
} else if (typeof value === 'string' && delimiter) {
|
|
425
|
+
const parts = value.split(delimiter);
|
|
426
|
+
parts.forEach((item, index) => {
|
|
427
|
+
send({
|
|
428
|
+
...packet,
|
|
429
|
+
payload: item,
|
|
430
|
+
parts: { index, count: parts.length }
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
} else {
|
|
434
|
+
send(packet);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
// ─────────────────────────────────────────────────────────────
|
|
441
|
+
// Join - Join messages
|
|
442
|
+
// ─────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Join node - Join sequences of messages
|
|
446
|
+
*
|
|
447
|
+
* @param {Object} options
|
|
448
|
+
* @param {number} options.count - Number of messages to join
|
|
449
|
+
* @param {string} options.property - Property to join (default: 'payload')
|
|
450
|
+
* @param {Function} options.reducer - Custom reducer function
|
|
451
|
+
* @param {*} options.initial - Initial value
|
|
452
|
+
*/
|
|
453
|
+
export function join(options = {}) {
|
|
454
|
+
const {
|
|
455
|
+
count = 0,
|
|
456
|
+
property = 'payload',
|
|
457
|
+
reducer = null,
|
|
458
|
+
initial = [],
|
|
459
|
+
} = options;
|
|
460
|
+
|
|
461
|
+
let buffer = Array.isArray(initial) ? [...initial] : initial;
|
|
462
|
+
let msgCount = 0;
|
|
463
|
+
|
|
464
|
+
if (count === 0) {
|
|
465
|
+
console.warn('[join] count=0 - messages will accumulate but never emit');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return (send, packet) => {
|
|
469
|
+
const value = getProperty(packet, property);
|
|
470
|
+
|
|
471
|
+
if (reducer) {
|
|
472
|
+
buffer = reducer(buffer, packet);
|
|
473
|
+
} else if (Array.isArray(buffer)) {
|
|
474
|
+
buffer.push(value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
msgCount++;
|
|
478
|
+
|
|
479
|
+
if (count > 0 && msgCount >= count) {
|
|
480
|
+
send({ payload: buffer });
|
|
481
|
+
buffer = Array.isArray(initial) ? [...initial] : initial;
|
|
482
|
+
msgCount = 0;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
// ─────────────────────────────────────────────────────────────
|
|
489
|
+
// Filter - Filter messages
|
|
490
|
+
// ─────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Filter node - Filter messages based on condition
|
|
494
|
+
*
|
|
495
|
+
* @param {Object} options
|
|
496
|
+
* @param {Function} options.condition - Function(msg) → boolean
|
|
497
|
+
*/
|
|
498
|
+
export function filter(options = {}) {
|
|
499
|
+
const { condition = () => true } = options;
|
|
500
|
+
|
|
501
|
+
return (send, packet) => {
|
|
502
|
+
if (condition(packet)) {
|
|
503
|
+
send(packet);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
// ─────────────────────────────────────────────────────────────
|
|
510
|
+
// Link - Named links for connecting flows
|
|
511
|
+
// ─────────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
const linkRegistry = new Map();
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Link Out - Send to named link
|
|
517
|
+
*/
|
|
518
|
+
export function linkOut(options = {}) {
|
|
519
|
+
const { name = '' } = options;
|
|
520
|
+
|
|
521
|
+
if (!name) {
|
|
522
|
+
console.warn('[linkOut] No name configured');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return (send, packet) => {
|
|
526
|
+
const handlers = linkRegistry.get(name) || [];
|
|
527
|
+
for (const handler of handlers) {
|
|
528
|
+
handler(packet);
|
|
529
|
+
}
|
|
530
|
+
send(packet);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Link In - Receive from named link
|
|
536
|
+
*/
|
|
537
|
+
export function linkIn(options = {}) {
|
|
538
|
+
const { name = '' } = options;
|
|
539
|
+
|
|
540
|
+
if (!name) {
|
|
541
|
+
console.warn('[linkIn] No name configured');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return (send) => {
|
|
545
|
+
const handlers = linkRegistry.get(name) || [];
|
|
546
|
+
handlers.push(send);
|
|
547
|
+
linkRegistry.set(name, handlers);
|
|
548
|
+
|
|
549
|
+
return () => {
|
|
550
|
+
const h = linkRegistry.get(name) || [];
|
|
551
|
+
linkRegistry.set(name, h.filter(x => x !== send));
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
// ─────────────────────────────────────────────────────────────
|
|
558
|
+
// Catch - Error handling
|
|
559
|
+
// ─────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Catch node - Catches errors from other nodes
|
|
563
|
+
*/
|
|
564
|
+
export function catchError(options = {}) {
|
|
565
|
+
const { scope = 'all' } = options;
|
|
566
|
+
|
|
567
|
+
return (send, packet) => {
|
|
568
|
+
if (packet.error) {
|
|
569
|
+
send(packet);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
// ─────────────────────────────────────────────────────────────
|
|
576
|
+
// Status - Status reporting
|
|
577
|
+
// ─────────────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Status node - Reports node status
|
|
581
|
+
*/
|
|
582
|
+
export function status(options = {}) {
|
|
583
|
+
const {
|
|
584
|
+
fill = 'green', // 'red', 'green', 'yellow', 'blue', 'grey'
|
|
585
|
+
shape = 'dot', // 'ring', 'dot'
|
|
586
|
+
text = ''
|
|
587
|
+
} = options;
|
|
588
|
+
|
|
589
|
+
return (send, packet) => {
|
|
590
|
+
send({
|
|
591
|
+
...packet,
|
|
592
|
+
status: { fill, shape, text: typeof text === 'function' ? text(packet) : text }
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
}
|