@rosepetal/node-red-contrib-async-function 1.0.0 → 1.0.2
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 +4 -3
- package/nodes/async-function.html +65 -12
- package/nodes/async-function.js +132 -48
- package/nodes/lib/child-process-pool.js +610 -0
- package/nodes/lib/child-process-script.js +264 -0
- package/nodes/lib/message-serializer.js +1 -1
- package/nodes/lib/worker-pool.js +12 -0
- package/nodes/lib/worker-script.js +132 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Run heavy computations in Node-RED without slowing down your flows. This node wo
|
|
|
20
20
|
|
|
21
21
|
## How It Works
|
|
22
22
|
|
|
23
|
-
Drop an **async function** node into your flow. Write your code just like you would in a regular function node. The difference? Your code runs in a separate worker thread, so heavy operations won't freeze Node-RED.
|
|
23
|
+
Drop an **async function** node into your flow. Write your code just like you would in a regular function node. The difference? Your code runs in a separate worker thread by default (or a child process if configured), so heavy operations won't freeze Node-RED.
|
|
24
24
|
|
|
25
25
|
## When to Use This
|
|
26
26
|
|
|
@@ -40,9 +40,10 @@ Drop an **async function** node into your flow. Write your code just like you wo
|
|
|
40
40
|
- **Function** – Your JavaScript code. Works with `async/await`, `return`, and `require()`.
|
|
41
41
|
- **Outputs** – How many output wires (0-10). Return an array for multiple outputs.
|
|
42
42
|
- **Timeout** – Maximum seconds to wait before killing the worker. Default: 30 seconds.
|
|
43
|
+
- **Runtime** – Worker Threads (default, fastest) or Child Process (for native modules like `gl`).
|
|
43
44
|
|
|
44
45
|
### Worker Pool
|
|
45
|
-
- **Workers** – Fixed number of
|
|
46
|
+
- **Workers** – Fixed number of workers (1-16). Each node maintains exactly this many workers. Default: 3.
|
|
46
47
|
- **Queue Size** – Messages to queue when all workers are occupied. Default: 100.
|
|
47
48
|
|
|
48
49
|
### Modules
|
|
@@ -165,7 +166,7 @@ The node shows you what's happening in real time:
|
|
|
165
166
|
|
|
166
167
|
## Performance Notes
|
|
167
168
|
|
|
168
|
-
- Worker threads add about 5-10ms overhead per message.
|
|
169
|
+
- Worker threads add about 5-10ms overhead per message (child process mode is higher).
|
|
169
170
|
- Best for operations taking more than 10ms to run.
|
|
170
171
|
- Each node maintains a fixed pool of workers—no startup delay or dynamic scaling overhead.
|
|
171
172
|
- Workers are dedicated per-node, ensuring predictable performance.
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
defaults: {
|
|
8
8
|
name: { value: '' },
|
|
9
9
|
func: {
|
|
10
|
-
value: '// Write your async code here\n// The code runs in a worker thread\n// Available: msg, return, async/await, require()\n// Not available: context, flow, global, node\n\nreturn msg;'
|
|
10
|
+
value: '// Write your async code here\n// The code runs in a worker thread or child process\n// Available: msg, return, async/await, require()\n// Not available: context, flow, global, node\n\nreturn msg;'
|
|
11
11
|
},
|
|
12
12
|
outputs: { value: 1, validate: RED.validators.number() },
|
|
13
13
|
timeout: { value: 30000, validate: RED.validators.number() },
|
|
14
|
+
executionMode: { value: 'worker_threads' },
|
|
14
15
|
numWorkers: { value: 3, validate: RED.validators.number() },
|
|
15
16
|
maxQueueSize: { value: 100, validate: RED.validators.number() },
|
|
16
17
|
libs: { value: [] },
|
|
@@ -239,6 +240,32 @@
|
|
|
239
240
|
// Ensure configured module vars are registered in the editor on load
|
|
240
241
|
updateEditorModules();
|
|
241
242
|
|
|
243
|
+
// Restart Workers button click handler
|
|
244
|
+
$('#node-restart-workers').on('click', function() {
|
|
245
|
+
var btn = $(this);
|
|
246
|
+
var status = $('#node-restart-status');
|
|
247
|
+
var nodeId = node.id;
|
|
248
|
+
|
|
249
|
+
btn.prop('disabled', true);
|
|
250
|
+
status.text('Restarting...').css('color', '#666');
|
|
251
|
+
|
|
252
|
+
$.ajax({
|
|
253
|
+
url: 'async-function/' + nodeId + '/restart',
|
|
254
|
+
type: 'POST',
|
|
255
|
+
success: function(data) {
|
|
256
|
+
status.text('✓ Workers restarted').css('color', 'green');
|
|
257
|
+
setTimeout(function() { status.text(''); }, 3000);
|
|
258
|
+
},
|
|
259
|
+
error: function(xhr) {
|
|
260
|
+
var msg = xhr.responseJSON ? xhr.responseJSON.error : 'Restart failed';
|
|
261
|
+
status.text('✗ ' + msg).css('color', 'red');
|
|
262
|
+
},
|
|
263
|
+
complete: function() {
|
|
264
|
+
btn.prop('disabled', false);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
242
269
|
// Resize editor on dialog resize
|
|
243
270
|
$('#dialog-form').on('dialogresize', function() {
|
|
244
271
|
const rows = $('#dialog-form>div:not(#async-func-tabs-content)');
|
|
@@ -412,6 +439,13 @@
|
|
|
412
439
|
<input id="node-input-timeout" style="width:120px;" value="30000">
|
|
413
440
|
<span style="margin-left:5px;">ms</span>
|
|
414
441
|
</div>
|
|
442
|
+
<div class="form-row">
|
|
443
|
+
<label for="node-input-executionMode"><i class="fa fa-bolt"></i> Runtime</label>
|
|
444
|
+
<select id="node-input-executionMode" style="width: 260px;">
|
|
445
|
+
<option value="worker_threads">Worker Threads (fastest)</option>
|
|
446
|
+
<option value="child_process">Child Process (native modules)</option>
|
|
447
|
+
</select>
|
|
448
|
+
</div>
|
|
415
449
|
</div>
|
|
416
450
|
|
|
417
451
|
<!-- Worker Pool Section -->
|
|
@@ -440,6 +474,22 @@
|
|
|
440
474
|
<ol id="node-input-libs-container"></ol>
|
|
441
475
|
</div>
|
|
442
476
|
</div>
|
|
477
|
+
|
|
478
|
+
<!-- Actions Section -->
|
|
479
|
+
<div class="async-func-section">
|
|
480
|
+
<div class="async-func-section-header">
|
|
481
|
+
<i class="fa fa-refresh"></i> Actions
|
|
482
|
+
</div>
|
|
483
|
+
<div class="form-row" style="margin-bottom: 0;">
|
|
484
|
+
<button type="button" class="red-ui-button" id="node-restart-workers">
|
|
485
|
+
<i class="fa fa-refresh"></i> Restart Workers
|
|
486
|
+
</button>
|
|
487
|
+
<span id="node-restart-status" style="margin-left: 10px;"></span>
|
|
488
|
+
<div style="margin-top: 8px; color: #999; font-size: 12px;">
|
|
489
|
+
Restarts all worker processes/threads to reload modules and reset state.
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
443
493
|
</div>
|
|
444
494
|
|
|
445
495
|
<!-- Function Tab -->
|
|
@@ -456,12 +506,13 @@
|
|
|
456
506
|
<div class="form-tips">
|
|
457
507
|
<b>Tips:</b>
|
|
458
508
|
<ul style="margin-top:5px; margin-bottom:0;">
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
</
|
|
509
|
+
<li>Code runs in a worker thread or child process to prevent blocking</li>
|
|
510
|
+
<li>Available: <code>msg</code>, <code>return</code>, <code>async/await</code>, <code>require()</code></li>
|
|
511
|
+
<li>Not available: <code>context</code>, <code>flow</code>, <code>global</code>, <code>node</code></li>
|
|
512
|
+
<li>Use "Child Process" runtime for native modules that require main thread (e.g., <code>gl</code>)</li>
|
|
513
|
+
<li>Return <code>msg</code> for single output, or <code>[msg1, msg2]</code> for multiple outputs</li>
|
|
514
|
+
<li>Return <code>null</code> to stop the flow</li>
|
|
515
|
+
</ul>
|
|
465
516
|
</div>
|
|
466
517
|
</div>
|
|
467
518
|
|
|
@@ -469,7 +520,7 @@
|
|
|
469
520
|
</script>
|
|
470
521
|
|
|
471
522
|
<script type="text/html" data-help-name="async-function">
|
|
472
|
-
<p>Execute custom JavaScript code in a worker thread to prevent event loop blocking.</p>
|
|
523
|
+
<p>Execute custom JavaScript code in a worker thread or child process to prevent event loop blocking.</p>
|
|
473
524
|
|
|
474
525
|
<h3>Inputs</h3>
|
|
475
526
|
<dl class="message-properties">
|
|
@@ -486,8 +537,8 @@
|
|
|
486
537
|
</dl>
|
|
487
538
|
|
|
488
539
|
<h3>Details</h3>
|
|
489
|
-
<p>This node executes JavaScript code in a worker thread, preventing CPU-intensive
|
|
490
|
-
from blocking the Node-RED event loop.</p>
|
|
540
|
+
<p>This node executes JavaScript code in a worker thread or child process, preventing CPU-intensive
|
|
541
|
+
operations from blocking the Node-RED event loop.</p>
|
|
491
542
|
|
|
492
543
|
<h4>Available in Code</h4>
|
|
493
544
|
<ul>
|
|
@@ -554,8 +605,10 @@ return msg;</pre>
|
|
|
554
605
|
<dd>Number of output ports (0-10). Use array return for multiple outputs.</dd>
|
|
555
606
|
<dt>Timeout</dt>
|
|
556
607
|
<dd>Maximum execution time in milliseconds (1000-300000). Worker is terminated if exceeded. Default: 30000 (30s).</dd>
|
|
608
|
+
<dt>Runtime</dt>
|
|
609
|
+
<dd>Worker Threads (default, fastest) or Child Process (useful for native modules like <code>gl</code>).</dd>
|
|
557
610
|
<dt>Workers</dt>
|
|
558
|
-
<dd>Fixed number of
|
|
611
|
+
<dd>Fixed number of workers (1-16). Each node maintains exactly this many workers. Default: 3.</dd>
|
|
559
612
|
<dt>Queue Size</dt>
|
|
560
613
|
<dd>Maximum number of messages queued when all workers are busy (10-1000). Default: 100.</dd>
|
|
561
614
|
<dt>Buffers</dt>
|
|
@@ -582,7 +635,7 @@ return msg;</pre>
|
|
|
582
635
|
|
|
583
636
|
<h3>Performance Notes</h3>
|
|
584
637
|
<ul>
|
|
585
|
-
<li>Worker threads add ~5-10ms overhead per message</li>
|
|
638
|
+
<li>Worker threads add ~5-10ms overhead per message (child process mode is higher)</li>
|
|
586
639
|
<li>Best for CPU-intensive operations (>10ms execution time)</li>
|
|
587
640
|
<li>For simple operations, use the standard function node</li>
|
|
588
641
|
<li>Workers are pooled and reused for efficiency</li>
|
package/nodes/async-function.js
CHANGED
|
@@ -5,8 +5,19 @@
|
|
|
5
5
|
* to prevent event loop blocking.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const path = require('path');
|
|
8
9
|
const { WorkerPool } = require('./lib/worker-pool');
|
|
9
|
-
const {
|
|
10
|
+
const { ChildProcessPool } = require('./lib/child-process-pool');
|
|
11
|
+
|
|
12
|
+
function resolveNodeRedUserDir(RED) {
|
|
13
|
+
if (RED && RED.settings && RED.settings.userDir) {
|
|
14
|
+
return path.resolve(RED.settings.userDir);
|
|
15
|
+
}
|
|
16
|
+
if (process.env.NODE_RED_HOME) {
|
|
17
|
+
return path.resolve(process.env.NODE_RED_HOME);
|
|
18
|
+
}
|
|
19
|
+
return process.cwd();
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
function extractMsgKeysFromCode(code) {
|
|
12
23
|
const keys = new Set();
|
|
@@ -97,11 +108,30 @@ function applyPerformanceMetrics(node, originalMsg, targetMsg, performance) {
|
|
|
97
108
|
}
|
|
98
109
|
copyPerformance(targetMsg.performance, collected);
|
|
99
110
|
|
|
111
|
+
const transferToWorkerMs = normalizePerformanceValue(
|
|
112
|
+
performance.transferToWorkerMs ??
|
|
113
|
+
performance.transfer_to_worker_ms ??
|
|
114
|
+
performance.transferToPythonMs ??
|
|
115
|
+
performance.transfer_to_python_ms
|
|
116
|
+
);
|
|
117
|
+
const executionMs = normalizePerformanceValue(
|
|
118
|
+
performance.executionMs ?? performance.execution_ms
|
|
119
|
+
);
|
|
120
|
+
const transferToMainMs = normalizePerformanceValue(
|
|
121
|
+
performance.transferToMainMs ??
|
|
122
|
+
performance.transfer_to_main_ms ??
|
|
123
|
+
performance.transferToJsMs ??
|
|
124
|
+
performance.transfer_to_js_ms
|
|
125
|
+
);
|
|
126
|
+
const totalMs = normalizePerformanceValue(
|
|
127
|
+
performance.totalMs ?? performance.total_ms ?? performance.total
|
|
128
|
+
);
|
|
129
|
+
|
|
100
130
|
collected[label] = {
|
|
101
|
-
|
|
102
|
-
executionMs
|
|
103
|
-
|
|
104
|
-
totalMs
|
|
131
|
+
transferToWorkerMs,
|
|
132
|
+
executionMs,
|
|
133
|
+
transferToMainMs,
|
|
134
|
+
totalMs
|
|
105
135
|
};
|
|
106
136
|
|
|
107
137
|
targetMsg.performance = collected;
|
|
@@ -188,6 +218,8 @@ module.exports = function(RED) {
|
|
|
188
218
|
node.timeout = config.timeout || 30000;
|
|
189
219
|
node.name = config.name || '';
|
|
190
220
|
node.errorRecoveryTimer = null; // Track error recovery timer
|
|
221
|
+
const modeValue = typeof config.executionMode === 'string' ? config.executionMode.trim() : '';
|
|
222
|
+
node.executionMode = modeValue.toLowerCase() === 'child_process' ? 'child_process' : 'worker_threads';
|
|
191
223
|
|
|
192
224
|
// Migrate old config to new format (backwards compatibility)
|
|
193
225
|
if (config.minWorkers !== undefined || config.maxWorkers !== undefined) {
|
|
@@ -196,59 +228,85 @@ module.exports = function(RED) {
|
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
// Store libs configuration
|
|
199
|
-
node.libs = config.libs
|
|
231
|
+
node.libs = Array.isArray(config.libs) ? config.libs : [];
|
|
232
|
+
|
|
233
|
+
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
|
|
234
|
+
node.error('External modules are disabled by Node-RED settings.');
|
|
235
|
+
node.status({
|
|
236
|
+
fill: 'red',
|
|
237
|
+
shape: 'dot',
|
|
238
|
+
text: 'Modules disabled'
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const nodeRedUserDir = resolveNodeRedUserDir(RED);
|
|
244
|
+
|
|
245
|
+
// Verify modules are resolvable WITHOUT loading them in the main thread.
|
|
246
|
+
// Loading native modules (like 'gl') in main thread prevents them from
|
|
247
|
+
// working in worker threads due to native module registration conflicts.
|
|
248
|
+
if (node.libs.length > 0) {
|
|
249
|
+
const { createRequire } = require('module');
|
|
250
|
+
const nodeRedRequire = createRequire(path.join(nodeRedUserDir, 'package.json'));
|
|
200
251
|
|
|
201
|
-
// Pre-check and install missing modules
|
|
202
|
-
if (node.libs && node.libs.length > 0) {
|
|
203
252
|
for (const lib of node.libs) {
|
|
253
|
+
if (!lib || !lib.module || !lib.var) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
204
256
|
try {
|
|
205
|
-
|
|
257
|
+
// Only resolve the path - don't actually load the module
|
|
258
|
+
nodeRedRequire.resolve(lib.module);
|
|
206
259
|
} catch (err) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
}
|
|
260
|
+
node.error(`Module "${lib.module}" not found. Install it with: cd ${nodeRedUserDir} && npm install ${lib.module}`);
|
|
261
|
+
node.status({
|
|
262
|
+
fill: 'red',
|
|
263
|
+
shape: 'dot',
|
|
264
|
+
text: `Module not found: ${lib.module}`
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
213
267
|
}
|
|
214
268
|
}
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
const startPool = () => {
|
|
272
|
+
try {
|
|
273
|
+
const PoolImpl = node.executionMode === 'child_process' ? ChildProcessPool : WorkerPool;
|
|
274
|
+
node.pool = new PoolImpl({
|
|
275
|
+
numWorkers: config.numWorkers || 3,
|
|
276
|
+
maxQueueSize: config.maxQueueSize || 100,
|
|
277
|
+
taskTimeout: node.timeout,
|
|
278
|
+
shmThreshold: 0,
|
|
279
|
+
libs: node.libs,
|
|
280
|
+
nodeRedUserDir
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
node.error('Failed to create worker pool: ' + err.message);
|
|
284
|
+
node.status({
|
|
285
|
+
fill: 'red',
|
|
286
|
+
shape: 'dot',
|
|
287
|
+
text: 'Pool creation failed'
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
236
291
|
|
|
237
|
-
|
|
238
|
-
node.pool.initialize().then(() => {
|
|
239
|
-
updateStatus(node);
|
|
240
|
-
// Start periodic status updates
|
|
241
|
-
node.statusInterval = setInterval(() => {
|
|
292
|
+
node.pool.initialize().then(() => {
|
|
242
293
|
updateStatus(node);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
// Start periodic status updates
|
|
295
|
+
node.statusInterval = setInterval(() => {
|
|
296
|
+
updateStatus(node);
|
|
297
|
+
}, 2000); // Update every 2 seconds
|
|
298
|
+
}).catch(err => {
|
|
299
|
+
node.error('Failed to initialize worker pool: ' + err.message);
|
|
300
|
+
node.status({
|
|
301
|
+
fill: 'red',
|
|
302
|
+
shape: 'dot',
|
|
303
|
+
text: 'Init failed'
|
|
304
|
+
});
|
|
250
305
|
});
|
|
251
|
-
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Start the pool directly - module validation already done synchronously above
|
|
309
|
+
startPool();
|
|
252
310
|
|
|
253
311
|
// Handle incoming messages
|
|
254
312
|
node.on('input', async function(msg, send, done) {
|
|
@@ -260,6 +318,11 @@ module.exports = function(RED) {
|
|
|
260
318
|
}
|
|
261
319
|
};
|
|
262
320
|
|
|
321
|
+
if (!node.pool) {
|
|
322
|
+
done(new Error('Worker pool is not initialized'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
263
326
|
const timing = { start: process.hrtime.bigint() };
|
|
264
327
|
const workerMsg = buildWorkerInputMsg(msg, node.func);
|
|
265
328
|
|
|
@@ -347,5 +410,26 @@ module.exports = function(RED) {
|
|
|
347
410
|
}
|
|
348
411
|
|
|
349
412
|
// Register the node type
|
|
350
|
-
RED.nodes.registerType('async-function', AsyncFunctionNode
|
|
413
|
+
RED.nodes.registerType('async-function', AsyncFunctionNode, {
|
|
414
|
+
dynamicModuleList: 'libs'
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// HTTP endpoint to restart workers for a specific node
|
|
418
|
+
RED.httpAdmin.post('/async-function/:id/restart', async function(req, res) {
|
|
419
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
420
|
+
if (!node || !node.pool) {
|
|
421
|
+
return res.status(404).json({ error: 'Node not found or pool not initialized' });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await node.pool.shutdown();
|
|
426
|
+
node.pool.initialized = false;
|
|
427
|
+
node.pool.shuttingDown = false;
|
|
428
|
+
await node.pool.initialize();
|
|
429
|
+
updateStatus(node);
|
|
430
|
+
res.json({ success: true, message: 'Workers restarted' });
|
|
431
|
+
} catch (err) {
|
|
432
|
+
res.status(500).json({ error: err.message });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
351
435
|
};
|