@rosepetal/node-red-contrib-async-function 1.0.1 → 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 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 worker threads (1-16). Each node maintains exactly this many workers. Default: 3.
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
- <li>Code runs in a worker thread to prevent blocking</li>
460
- <li>Available: <code>msg</code>, <code>return</code>, <code>async/await</code>, <code>require()</code></li>
461
- <li>Not available: <code>context</code>, <code>flow</code>, <code>global</code>, <code>node</code></li>
462
- <li>Return <code>msg</code> for single output, or <code>[msg1, msg2]</code> for multiple outputs</li>
463
- <li>Return <code>null</code> to stop the flow</li>
464
- </ul>
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 operations
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 worker threads (1-16). Each node maintains exactly this many workers. Default: 3.</dd>
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>
@@ -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 { installModule, getNodeRedUserDir } = require('./lib/module-installer');
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
- transferToPythonMs: normalizePerformanceValue(performance.transfer_to_python_ms ?? performance.transferToPythonMs),
102
- executionMs: normalizePerformanceValue(performance.execution_ms ?? performance.executionMs),
103
- transferToJsMs: normalizePerformanceValue(performance.transfer_to_js_ms ?? performance.transferToJsMs),
104
- totalMs: normalizePerformanceValue(performance.totalMs ?? performance.total_ms ?? performance.total)
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
- require.resolve(lib.module);
257
+ // Only resolve the path - don't actually load the module
258
+ nodeRedRequire.resolve(lib.module);
206
259
  } catch (err) {
207
- if (err.code === 'MODULE_NOT_FOUND') {
208
- node.warn(`Installing missing module: ${lib.module}`);
209
- if (!installModule(lib.module)) {
210
- node.error(`Failed to install module: ${lib.module}. Please install it manually in ~/.node-red`);
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
- // Create per-node worker pool
218
- try {
219
- node.pool = new WorkerPool({
220
- numWorkers: config.numWorkers || 3,
221
- maxQueueSize: config.maxQueueSize || 100,
222
- taskTimeout: node.timeout,
223
- shmThreshold: 0,
224
- libs: node.libs,
225
- nodeRedUserDir: getNodeRedUserDir()
226
- });
227
- } catch (err) {
228
- node.error('Failed to create worker pool: ' + err.message);
229
- node.status({
230
- fill: 'red',
231
- shape: 'dot',
232
- text: 'Pool creation failed'
233
- });
234
- return;
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
- // Initialize pool
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
- }, 2000); // Update every 2 seconds
244
- }).catch(err => {
245
- node.error('Failed to initialize worker pool: ' + err.message);
246
- node.status({
247
- fill: 'red',
248
- shape: 'dot',
249
- text: 'Init failed'
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
  };