@rosepetal/node-red-contrib-async-function 1.0.2 → 1.0.3
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 +8 -6
- package/nodes/async-function.html +9 -7
- package/nodes/async-function.js +320 -27
- package/nodes/lib/child-process-pool.js +33 -6
- package/nodes/lib/child-process-script.js +179 -5
- package/nodes/lib/message-serializer.js +171 -16
- package/nodes/lib/module-installer.js +48 -25
- package/nodes/lib/shared-memory-manager.js +50 -12
- package/nodes/lib/worker-pool.js +47 -10
- package/nodes/lib/worker-script.js +206 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Drop an **async function** node into your flow. Write your code just like you wo
|
|
|
31
31
|
|
|
32
32
|
**Skip It For:**
|
|
33
33
|
- Simple math or quick transformations (the regular function node is faster).
|
|
34
|
-
-
|
|
34
|
+
- Flows that require live context reads/writes during execution (context is snapshot-based).
|
|
35
35
|
|
|
36
36
|
## Node Options
|
|
37
37
|
|
|
@@ -62,7 +62,7 @@ return msg;
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
### Buffer Handling
|
|
65
|
-
- **Buffers** –
|
|
65
|
+
- **Buffers** – Worker Threads use zero-copy transfer when possible. Child Process mode and non-transferable buffers fall back to shared memory (`/dev/shm` on Linux, otherwise `os.tmpdir()`), with base64 fallback if needed.
|
|
66
66
|
|
|
67
67
|
## Typical Flow
|
|
68
68
|
|
|
@@ -86,9 +86,11 @@ return msg;
|
|
|
86
86
|
- `console` – Logging functions
|
|
87
87
|
- `setTimeout`, `setInterval` – Timers
|
|
88
88
|
|
|
89
|
-
**
|
|
90
|
-
- `context`, `flow`, `global`
|
|
91
|
-
- `
|
|
89
|
+
**Notes:**
|
|
90
|
+
- `context`, `flow`, `global` are snapshot-based: reads are from the snapshot, writes are applied after the function completes
|
|
91
|
+
- Snapshot includes only literal keys found in `flow.get("key")` / `global.get("key")` / `context.get("key")`
|
|
92
|
+
- Context store selection is not supported (default store only)
|
|
93
|
+
- `node.warn/error/log` are collected and forwarded to the main thread
|
|
92
94
|
- Non-serializable objects (functions, symbols, etc.)
|
|
93
95
|
|
|
94
96
|
## Code Examples
|
|
@@ -170,7 +172,7 @@ The node shows you what's happening in real time:
|
|
|
170
172
|
- Best for operations taking more than 10ms to run.
|
|
171
173
|
- Each node maintains a fixed pool of workers—no startup delay or dynamic scaling overhead.
|
|
172
174
|
- Workers are dedicated per-node, ensuring predictable performance.
|
|
173
|
-
- **Binary Fast Path**:
|
|
175
|
+
- **Binary Fast Path**: Worker threads use zero-copy transfer when possible; shared memory is the fallback.
|
|
174
176
|
- Event loop never blocks, even when processing multi-MB binary data (images, files, etc.).
|
|
175
177
|
|
|
176
178
|
## Error Handling
|
|
@@ -7,7 +7,7 @@
|
|
|
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 or child process\n// Available: msg, return, async/await, require()
|
|
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(), node.warn/error/log,\n// flow/global/context (snapshot-based)\n\nreturn msg;'
|
|
11
11
|
},
|
|
12
12
|
outputs: { value: 1, validate: RED.validators.number() },
|
|
13
13
|
timeout: { value: 30000, validate: RED.validators.number() },
|
|
@@ -507,8 +507,8 @@
|
|
|
507
507
|
<b>Tips:</b>
|
|
508
508
|
<ul style="margin-top:5px; margin-bottom:0;">
|
|
509
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>
|
|
510
|
+
<li>Available: <code>msg</code>, <code>return</code>, <code>async/await</code>, <code>require()</code>, <code>node</code>, <code>flow</code>, <code>global</code>, <code>context</code></li>
|
|
511
|
+
<li>Context access is snapshot-based (writes apply after execution)</li>
|
|
512
512
|
<li>Use "Child Process" runtime for native modules that require main thread (e.g., <code>gl</code>)</li>
|
|
513
513
|
<li>Return <code>msg</code> for single output, or <code>[msg1, msg2]</code> for multiple outputs</li>
|
|
514
514
|
<li>Return <code>null</code> to stop the flow</li>
|
|
@@ -546,14 +546,14 @@
|
|
|
546
546
|
<li><code>return</code> - Return modified message or array of messages</li>
|
|
547
547
|
<li><code>async/await</code> - Use async operations</li>
|
|
548
548
|
<li><code>require()</code> - Import Node.js modules</li>
|
|
549
|
+
<li><code>node</code> - Node helpers (<code>warn</code>, <code>error</code>, <code>log</code>)</li>
|
|
550
|
+
<li><code>flow</code>, <code>global</code>, <code>context</code> - Snapshot-based context access (writes apply after execution)</li>
|
|
549
551
|
<li><code>console</code> - Logging functions</li>
|
|
550
552
|
<li><code>setTimeout</code>, <code>setInterval</code> - Timers</li>
|
|
551
553
|
</ul>
|
|
552
554
|
|
|
553
555
|
<h4>Not Available</h4>
|
|
554
556
|
<ul>
|
|
555
|
-
<li><code>context</code>, <code>flow</code>, <code>global</code> - Context storage (v2.0 feature)</li>
|
|
556
|
-
<li><code>node</code> - Node instance methods</li>
|
|
557
557
|
<li>Non-serializable objects in <code>msg</code></li>
|
|
558
558
|
</ul>
|
|
559
559
|
|
|
@@ -612,7 +612,7 @@ return msg;</pre>
|
|
|
612
612
|
<dt>Queue Size</dt>
|
|
613
613
|
<dd>Maximum number of messages queued when all workers are busy (10-1000). Default: 100.</dd>
|
|
614
614
|
<dt>Buffers</dt>
|
|
615
|
-
<dd>
|
|
615
|
+
<dd>Worker threads use zero-copy transfer when possible; shared memory is used as a fallback.</dd>
|
|
616
616
|
</dl>
|
|
617
617
|
|
|
618
618
|
<h3>Status Display</h3>
|
|
@@ -639,9 +639,11 @@ return msg;</pre>
|
|
|
639
639
|
<li>Best for CPU-intensive operations (>10ms execution time)</li>
|
|
640
640
|
<li>For simple operations, use the standard function node</li>
|
|
641
641
|
<li>Workers are pooled and reused for efficiency</li>
|
|
642
|
-
<li><b>Buffer Transfer:</b>
|
|
642
|
+
<li><b>Buffer Transfer:</b> Worker threads use zero-copy transfer when possible; shared memory is the fallback</li>
|
|
643
643
|
<li>Event loop never blocks, even when processing multi-MB binary data (images, files, etc.)</li>
|
|
644
644
|
<li>Message transfer is optimized by copying only referenced <code>msg.*</code> keys when possible</li>
|
|
645
|
+
<li>Context snapshots include only literal keys found in <code>flow.get("key")</code> etc.</li>
|
|
646
|
+
<li>Context snapshots use the default context store only</li>
|
|
645
647
|
<li>Timing is recorded on <code>msg.performance</code> under the node label</li>
|
|
646
648
|
</ul>
|
|
647
649
|
|
package/nodes/async-function.js
CHANGED
|
@@ -44,7 +44,7 @@ function extractMsgKeysFromCode(code) {
|
|
|
44
44
|
return keys;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function
|
|
47
|
+
function buildWorkerInputMsgCore(originalMsg, code) {
|
|
48
48
|
if (!originalMsg || typeof originalMsg !== 'object') {
|
|
49
49
|
return originalMsg;
|
|
50
50
|
}
|
|
@@ -84,6 +84,304 @@ function hrtimeDiffToMs(start) {
|
|
|
84
84
|
return Number(diff) / 1e6;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function normalizeTransferMode(mode, executionMode) {
|
|
88
|
+
const normalized = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
|
|
89
|
+
if (normalized === 'transfer' || normalized === 'shared' || normalized === 'copy') {
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
return executionMode === 'worker_threads' ? 'transfer' : 'shared';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MSG_WRAPPER_KEY = '__rosepetal_msg';
|
|
96
|
+
const CONTEXT_WRAPPER_KEY = '__rosepetal_context';
|
|
97
|
+
|
|
98
|
+
function extractContextKeysFromCode(code) {
|
|
99
|
+
const flowKeys = new Set();
|
|
100
|
+
const globalKeys = new Set();
|
|
101
|
+
const contextKeys = new Set();
|
|
102
|
+
|
|
103
|
+
if (typeof code !== 'string' || !code.trim()) {
|
|
104
|
+
return { flow: flowKeys, global: globalKeys, context: contextKeys, usesContext: false };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const flowRegex = /flow\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
|
|
108
|
+
const flowBracket = /flow\[['"]([^'"]+)['"]\]/g;
|
|
109
|
+
const globalRegex = /global\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
|
|
110
|
+
const globalBracket = /global\[['"]([^'"]+)['"]\]/g;
|
|
111
|
+
const contextRegex = /context\.(?:get|set)\(\s*['"]([^'"]+)['"]/g;
|
|
112
|
+
const contextBracket = /context\[['"]([^'"]+)['"]\]/g;
|
|
113
|
+
|
|
114
|
+
let match = flowRegex.exec(code);
|
|
115
|
+
while (match) {
|
|
116
|
+
flowKeys.add(match[1]);
|
|
117
|
+
match = flowRegex.exec(code);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
match = flowBracket.exec(code);
|
|
121
|
+
while (match) {
|
|
122
|
+
flowKeys.add(match[1]);
|
|
123
|
+
match = flowBracket.exec(code);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
match = globalRegex.exec(code);
|
|
127
|
+
while (match) {
|
|
128
|
+
globalKeys.add(match[1]);
|
|
129
|
+
match = globalRegex.exec(code);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
match = globalBracket.exec(code);
|
|
133
|
+
while (match) {
|
|
134
|
+
globalKeys.add(match[1]);
|
|
135
|
+
match = globalBracket.exec(code);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
match = contextRegex.exec(code);
|
|
139
|
+
while (match) {
|
|
140
|
+
contextKeys.add(match[1]);
|
|
141
|
+
match = contextRegex.exec(code);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
match = contextBracket.exec(code);
|
|
145
|
+
while (match) {
|
|
146
|
+
contextKeys.add(match[1]);
|
|
147
|
+
match = contextBracket.exec(code);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const usesContext = /\b(flow|global|context)\s*\./.test(code);
|
|
151
|
+
return { flow: flowKeys, global: globalKeys, context: contextKeys, usesContext };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function buildContextSnapshot(node, code) {
|
|
155
|
+
const empty = { flow: {}, global: {}, context: {} };
|
|
156
|
+
if (!node || typeof node.context !== 'function') {
|
|
157
|
+
return { snapshot: empty, usesContext: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const keys = extractContextKeysFromCode(code);
|
|
161
|
+
if (!keys.usesContext) {
|
|
162
|
+
return { snapshot: empty, usesContext: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ctx = node.context();
|
|
166
|
+
if (!ctx) {
|
|
167
|
+
return { snapshot: empty, usesContext: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const snapshot = { flow: {}, global: {}, context: {} };
|
|
171
|
+
const coerceBufferLike = (value) => {
|
|
172
|
+
if (!value || typeof value !== 'object') {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
if (Buffer.isBuffer(value)) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
if (value.type === 'Buffer' && Array.isArray(value.data)) {
|
|
179
|
+
return Buffer.from(value.data);
|
|
180
|
+
}
|
|
181
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
182
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getValueAsync = (scope, key) => {
|
|
188
|
+
if (!scope || typeof scope.get !== 'function') {
|
|
189
|
+
return Promise.resolve(undefined);
|
|
190
|
+
}
|
|
191
|
+
if (scope.get.length >= 2) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
try {
|
|
194
|
+
scope.get(key, (err, value) => {
|
|
195
|
+
resolve(err ? undefined : value);
|
|
196
|
+
});
|
|
197
|
+
} catch (_err) {
|
|
198
|
+
resolve(undefined);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const value = scope.get(key);
|
|
204
|
+
if (value && typeof value.then === 'function') {
|
|
205
|
+
return value.then((resolved) => resolved).catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
return Promise.resolve(value);
|
|
208
|
+
} catch (_err) {
|
|
209
|
+
return Promise.resolve(undefined);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const readKeys = async (scope, keySet, target) => {
|
|
214
|
+
if (!scope || typeof scope.get !== 'function') {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const entries = Array.from(keySet);
|
|
218
|
+
await Promise.all(entries.map(async (key) => {
|
|
219
|
+
try {
|
|
220
|
+
const value = await getValueAsync(scope, key);
|
|
221
|
+
target[key] = coerceBufferLike(value);
|
|
222
|
+
} catch (_err) {
|
|
223
|
+
// Ignore context read errors to avoid blocking execution
|
|
224
|
+
}
|
|
225
|
+
}));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await Promise.all([
|
|
229
|
+
readKeys(ctx.flow, keys.flow, snapshot.flow),
|
|
230
|
+
readKeys(ctx.global, keys.global, snapshot.global),
|
|
231
|
+
readKeys(ctx, keys.context, snapshot.context)
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
return { snapshot, usesContext: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildWorkerInputMsg(originalMsg, code, contextSnapshot, useContextWrapper) {
|
|
238
|
+
if (!useContextWrapper) {
|
|
239
|
+
return buildWorkerInputMsgCore(originalMsg, code);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const msgPayload = buildWorkerInputMsgCore(originalMsg, code);
|
|
243
|
+
return {
|
|
244
|
+
[MSG_WRAPPER_KEY]: msgPayload,
|
|
245
|
+
[CONTEXT_WRAPPER_KEY]: contextSnapshot || { flow: {}, global: {}, context: {} }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function rehydrateContextValue(value) {
|
|
250
|
+
if (Buffer.isBuffer(value)) {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
255
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return value.map((item) => rehydrateContextValue(item));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (value && typeof value === 'object') {
|
|
263
|
+
if (value.type === 'Buffer' && Array.isArray(value.data)) {
|
|
264
|
+
return Buffer.from(value.data);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const obj = Array.isArray(value) ? [] : {};
|
|
268
|
+
Object.keys(value).forEach((key) => {
|
|
269
|
+
obj[key] = rehydrateContextValue(value[key]);
|
|
270
|
+
});
|
|
271
|
+
return obj;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function applyContextUpdates(node, updates, msg) {
|
|
278
|
+
if (!node || !updates || typeof updates !== 'object' || typeof node.context !== 'function') {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const context = node.context();
|
|
283
|
+
if (!context) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const flowUpdates = updates.flow && typeof updates.flow === 'object' ? updates.flow : {};
|
|
288
|
+
const globalUpdates = updates.global && typeof updates.global === 'object' ? updates.global : {};
|
|
289
|
+
const contextUpdates = updates.context && typeof updates.context === 'object' ? updates.context : {};
|
|
290
|
+
|
|
291
|
+
const applyUpdates = async (scope, scopeLabel, scopeUpdates) => {
|
|
292
|
+
if (!scope || typeof scope.set !== 'function') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const setValueAsync = (key, value) => {
|
|
296
|
+
if (scope.set.length >= 3) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
try {
|
|
299
|
+
scope.set(key, value, (err) => {
|
|
300
|
+
if (err && typeof node.warn === 'function') {
|
|
301
|
+
node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
|
|
302
|
+
}
|
|
303
|
+
resolve();
|
|
304
|
+
});
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (typeof node.warn === 'function') {
|
|
307
|
+
node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
|
|
308
|
+
}
|
|
309
|
+
resolve();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = scope.set(key, value);
|
|
315
|
+
if (result && typeof result.then === 'function') {
|
|
316
|
+
return result.catch((err) => {
|
|
317
|
+
if (typeof node.warn === 'function') {
|
|
318
|
+
node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (typeof node.warn === 'function') {
|
|
324
|
+
node.warn(`Failed to set ${scopeLabel} context "${key}": ${err.message || err}`, msg);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
};
|
|
329
|
+
const entries = Object.keys(scopeUpdates);
|
|
330
|
+
await Promise.all(entries.map(async (key) => {
|
|
331
|
+
const value = rehydrateContextValue(scopeUpdates[key]);
|
|
332
|
+
await setValueAsync(key, value);
|
|
333
|
+
}));
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
await Promise.all([
|
|
337
|
+
applyUpdates(context.flow, 'flow', flowUpdates),
|
|
338
|
+
applyUpdates(context.global, 'global', globalUpdates),
|
|
339
|
+
applyUpdates(context, 'context', contextUpdates)
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function applyWorkerLogs(node, logs, msg) {
|
|
344
|
+
if (!node || !Array.isArray(logs)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logs.forEach((entry) => {
|
|
349
|
+
if (!entry) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const level = typeof entry === 'object' && entry.level ? String(entry.level) : 'warn';
|
|
354
|
+
const message = typeof entry === 'object' && entry.message !== undefined
|
|
355
|
+
? entry.message
|
|
356
|
+
: entry;
|
|
357
|
+
let text = '';
|
|
358
|
+
if (typeof message === 'string') {
|
|
359
|
+
text = message;
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
text = JSON.stringify(message);
|
|
363
|
+
} catch (_err) {
|
|
364
|
+
text = String(message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (level === 'error' && typeof node.error === 'function') {
|
|
369
|
+
node.error(text, msg);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (level === 'log' && typeof node.log === 'function') {
|
|
374
|
+
node.log(text);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (typeof node.warn === 'function') {
|
|
379
|
+
node.warn(text, msg);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
87
385
|
function applyPerformanceMetrics(node, originalMsg, targetMsg, performance) {
|
|
88
386
|
if (!performance || typeof performance !== 'object') {
|
|
89
387
|
return;
|
|
@@ -241,32 +539,9 @@ module.exports = function(RED) {
|
|
|
241
539
|
}
|
|
242
540
|
|
|
243
541
|
const nodeRedUserDir = resolveNodeRedUserDir(RED);
|
|
542
|
+
const transferMode = normalizeTransferMode(config.transferMode, node.executionMode);
|
|
244
543
|
|
|
245
|
-
//
|
|
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'));
|
|
251
|
-
|
|
252
|
-
for (const lib of node.libs) {
|
|
253
|
-
if (!lib || !lib.module || !lib.var) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
// Only resolve the path - don't actually load the module
|
|
258
|
-
nodeRedRequire.resolve(lib.module);
|
|
259
|
-
} catch (err) {
|
|
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;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
544
|
+
// Module resolution happens inside workers to avoid blocking the main thread.
|
|
270
545
|
|
|
271
546
|
const startPool = () => {
|
|
272
547
|
try {
|
|
@@ -276,6 +551,7 @@ module.exports = function(RED) {
|
|
|
276
551
|
maxQueueSize: config.maxQueueSize || 100,
|
|
277
552
|
taskTimeout: node.timeout,
|
|
278
553
|
shmThreshold: 0,
|
|
554
|
+
transferMode,
|
|
279
555
|
libs: node.libs,
|
|
280
556
|
nodeRedUserDir
|
|
281
557
|
});
|
|
@@ -324,22 +600,29 @@ module.exports = function(RED) {
|
|
|
324
600
|
}
|
|
325
601
|
|
|
326
602
|
const timing = { start: process.hrtime.bigint() };
|
|
327
|
-
const
|
|
603
|
+
const contextInfo = await buildContextSnapshot(node, node.func);
|
|
604
|
+
const workerMsg = buildWorkerInputMsg(msg, node.func, contextInfo.snapshot, contextInfo.usesContext);
|
|
328
605
|
|
|
329
606
|
try {
|
|
330
607
|
const payload = await node.pool.executeTask(node.func, workerMsg, node.timeout);
|
|
331
608
|
|
|
332
609
|
let resultData;
|
|
333
610
|
let performanceData = null;
|
|
611
|
+
let contextUpdates = null;
|
|
612
|
+
let logs = null;
|
|
334
613
|
|
|
335
614
|
if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'result')) {
|
|
336
615
|
resultData = payload.result;
|
|
337
616
|
performanceData = payload.performance || null;
|
|
617
|
+
contextUpdates = payload.contextUpdates || null;
|
|
618
|
+
logs = payload.logs || null;
|
|
338
619
|
} else {
|
|
339
620
|
resultData = payload;
|
|
340
621
|
}
|
|
341
622
|
|
|
342
623
|
if (resultData === null || resultData === undefined) {
|
|
624
|
+
await applyContextUpdates(node, contextUpdates, msg);
|
|
625
|
+
applyWorkerLogs(node, logs, msg);
|
|
343
626
|
done();
|
|
344
627
|
return;
|
|
345
628
|
}
|
|
@@ -348,6 +631,9 @@ module.exports = function(RED) {
|
|
|
348
631
|
const mergedPerformance = Object.assign({}, performanceData || {});
|
|
349
632
|
mergedPerformance.totalMs = totalMs;
|
|
350
633
|
|
|
634
|
+
await applyContextUpdates(node, contextUpdates, msg);
|
|
635
|
+
applyWorkerLogs(node, logs, msg);
|
|
636
|
+
|
|
351
637
|
const output = mergeResult(msg, resultData);
|
|
352
638
|
applyPerformanceToResult(node, msg, output, mergedPerformance);
|
|
353
639
|
|
|
@@ -362,6 +648,13 @@ module.exports = function(RED) {
|
|
|
362
648
|
text: `Error: ${err.message.substring(0, 20)}`
|
|
363
649
|
});
|
|
364
650
|
|
|
651
|
+
if (err && err.contextUpdates) {
|
|
652
|
+
await applyContextUpdates(node, err.contextUpdates, msg);
|
|
653
|
+
}
|
|
654
|
+
if (err && err.logs) {
|
|
655
|
+
applyWorkerLogs(node, err.logs, msg);
|
|
656
|
+
}
|
|
657
|
+
|
|
365
658
|
// Log error
|
|
366
659
|
node.error(`Async function error: ${err.message}`, msg);
|
|
367
660
|
|
|
@@ -17,6 +17,7 @@ const DEFAULT_CONFIG = {
|
|
|
17
17
|
taskTimeout: 30000, // Default task timeout: 30s
|
|
18
18
|
maxQueueSize: 100, // Max queued messages
|
|
19
19
|
shmThreshold: 0, // Always use shared memory for Buffers
|
|
20
|
+
transferMode: 'shared', // shared | copy (transfer not supported for child processes)
|
|
20
21
|
libs: [], // External modules to load in workers
|
|
21
22
|
nodeRedUserDir: null, // Node-RED user directory for module resolution
|
|
22
23
|
workerScript: path.join(__dirname, 'child-process-script.js')
|
|
@@ -166,7 +167,8 @@ class ChildProcessPool {
|
|
|
166
167
|
type: 'init',
|
|
167
168
|
libs: this.config.libs || [],
|
|
168
169
|
nodeRedUserDir: this.config.nodeRedUserDir,
|
|
169
|
-
shmThreshold: this.config.shmThreshold
|
|
170
|
+
shmThreshold: this.config.shmThreshold,
|
|
171
|
+
transferMode: this.config.transferMode
|
|
170
172
|
});
|
|
171
173
|
} catch (err) {
|
|
172
174
|
clearTimeout(readyTimeout);
|
|
@@ -298,7 +300,9 @@ class ChildProcessPool {
|
|
|
298
300
|
workerState.state = WorkerState.BUSY;
|
|
299
301
|
workerState.taskId = taskId;
|
|
300
302
|
|
|
301
|
-
this.serializer.sanitizeMessage(msg, null, taskId
|
|
303
|
+
this.serializer.sanitizeMessage(msg, null, taskId, {
|
|
304
|
+
transferMode: this.config.transferMode
|
|
305
|
+
}).then(sanitizedMsg => {
|
|
302
306
|
this.timeoutManager.startTimeout(taskId, timeout, () => {
|
|
303
307
|
this.handleTimeout(workerState, taskId);
|
|
304
308
|
});
|
|
@@ -329,7 +333,7 @@ class ChildProcessPool {
|
|
|
329
333
|
* @param {object} message - Message from worker
|
|
330
334
|
*/
|
|
331
335
|
handleWorkerMessage(workerState, message) {
|
|
332
|
-
const { type, taskId, result, error, performance } = message || {};
|
|
336
|
+
const { type, taskId, result, error, performance, contextUpdates, logs } = message || {};
|
|
333
337
|
|
|
334
338
|
if (type === 'result') {
|
|
335
339
|
this.timeoutManager.cancelTimeout(taskId);
|
|
@@ -339,8 +343,18 @@ class ChildProcessPool {
|
|
|
339
343
|
this.callbacks.delete(taskId);
|
|
340
344
|
this.recycleWorker(workerState);
|
|
341
345
|
|
|
342
|
-
this.serializer.restoreBuffers(result)
|
|
343
|
-
|
|
346
|
+
const restoreResult = this.serializer.restoreBuffers(result);
|
|
347
|
+
const restoreContext = contextUpdates
|
|
348
|
+
? this.serializer.restoreBuffers(contextUpdates)
|
|
349
|
+
: Promise.resolve(contextUpdates);
|
|
350
|
+
|
|
351
|
+
Promise.all([restoreResult, restoreContext]).then(([restoredResult, restoredContext]) => {
|
|
352
|
+
callback(null, {
|
|
353
|
+
result: restoredResult,
|
|
354
|
+
performance: performance || null,
|
|
355
|
+
contextUpdates: restoredContext || null,
|
|
356
|
+
logs: Array.isArray(logs) ? logs : null
|
|
357
|
+
});
|
|
344
358
|
}).catch(restoreErr => {
|
|
345
359
|
callback(restoreErr instanceof Error ? restoreErr : new Error(String(restoreErr)), null);
|
|
346
360
|
});
|
|
@@ -362,7 +376,20 @@ class ChildProcessPool {
|
|
|
362
376
|
if (error && error.name) {
|
|
363
377
|
err.name = error.name;
|
|
364
378
|
}
|
|
365
|
-
|
|
379
|
+
|
|
380
|
+
if (contextUpdates) {
|
|
381
|
+
this.serializer.restoreBuffers(contextUpdates).then(restoredContext => {
|
|
382
|
+
err.contextUpdates = restoredContext;
|
|
383
|
+
err.logs = Array.isArray(logs) ? logs : null;
|
|
384
|
+
callback(err, null);
|
|
385
|
+
}).catch(() => {
|
|
386
|
+
err.logs = Array.isArray(logs) ? logs : null;
|
|
387
|
+
callback(err, null);
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
err.logs = Array.isArray(logs) ? logs : null;
|
|
391
|
+
callback(err, null);
|
|
392
|
+
}
|
|
366
393
|
}
|
|
367
394
|
|
|
368
395
|
this.recycleWorker(workerState);
|