@rosepetal/node-red-contrib-async-function 1.0.2 → 1.1.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 +13 -9
- package/nodes/lib/child-process-pool.js +33 -6
- package/nodes/lib/child-process-script.js +180 -6
- package/nodes/lib/message-serializer.js +171 -16
- package/nodes/lib/module-installer.js +55 -32
- package/nodes/lib/shared-memory-manager.js +50 -12
- package/nodes/lib/worker-pool.js +47 -10
- package/nodes/lib/worker-script.js +207 -11
- package/nodes/{async-function.html → worker-function.html} +15 -13
- package/nodes/{async-function.js → worker-function.js} +326 -33
- package/package.json +3 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- Worker Function Node - Node-RED Editor Configuration -->
|
|
2
2
|
|
|
3
3
|
<script type="text/javascript">
|
|
4
|
-
RED.nodes.registerType('
|
|
4
|
+
RED.nodes.registerType('worker-function', {
|
|
5
5
|
category: 'function',
|
|
6
6
|
color: '#d7d9dc',
|
|
7
7
|
defaults: {
|
|
8
8
|
name: { value: '' },
|
|
9
9
|
func: {
|
|
10
|
-
value: '// Write your
|
|
10
|
+
value: '// Write your 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() },
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
outputs: 1,
|
|
22
22
|
icon: 'function.svg',
|
|
23
23
|
label: function() {
|
|
24
|
-
return this.name || '
|
|
24
|
+
return this.name || 'worker function';
|
|
25
25
|
},
|
|
26
26
|
labelStyle: function() {
|
|
27
27
|
return this.name ? 'node_label_italic' : '';
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
status.text('Restarting...').css('color', '#666');
|
|
251
251
|
|
|
252
252
|
$.ajax({
|
|
253
|
-
url: '
|
|
253
|
+
url: 'worker-function/' + nodeId + '/restart',
|
|
254
254
|
type: 'POST',
|
|
255
255
|
success: function(data) {
|
|
256
256
|
status.text('✓ Workers restarted').css('color', 'green');
|
|
@@ -346,7 +346,7 @@
|
|
|
346
346
|
});
|
|
347
347
|
</script>
|
|
348
348
|
|
|
349
|
-
<script type="text/html" data-template-name="
|
|
349
|
+
<script type="text/html" data-template-name="worker-function">
|
|
350
350
|
<style>
|
|
351
351
|
.async-func-tabs-row {
|
|
352
352
|
margin-bottom: 0;
|
|
@@ -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>
|
|
@@ -519,7 +519,7 @@
|
|
|
519
519
|
</div>
|
|
520
520
|
</script>
|
|
521
521
|
|
|
522
|
-
<script type="text/html" data-help-name="
|
|
522
|
+
<script type="text/html" data-help-name="worker-function">
|
|
523
523
|
<p>Execute custom JavaScript code in a worker thread or child process to prevent event loop blocking.</p>
|
|
524
524
|
|
|
525
525
|
<h3>Inputs</h3>
|
|
@@ -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
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Worker Function Node
|
|
3
3
|
*
|
|
4
4
|
* A Node-RED function node that executes user code in worker threads
|
|
5
|
-
* to prevent event loop blocking.
|
|
5
|
+
* or child processes to prevent event loop blocking.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
@@ -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,12 +84,310 @@ 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;
|
|
90
388
|
}
|
|
91
389
|
|
|
92
|
-
const label = (typeof node.name === 'string' && node.name.trim()) ? node.name.trim() : '
|
|
390
|
+
const label = (typeof node.name === 'string' && node.name.trim()) ? node.name.trim() : 'worker function';
|
|
93
391
|
if (!label) {
|
|
94
392
|
return;
|
|
95
393
|
}
|
|
@@ -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,8 +648,15 @@ 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
|
-
node.error(`
|
|
659
|
+
node.error(`Worker function error: ${err.message}`, msg);
|
|
367
660
|
|
|
368
661
|
// Propagate error to Catch node
|
|
369
662
|
done(err);
|
|
@@ -410,12 +703,12 @@ module.exports = function(RED) {
|
|
|
410
703
|
}
|
|
411
704
|
|
|
412
705
|
// Register the node type
|
|
413
|
-
RED.nodes.registerType('
|
|
706
|
+
RED.nodes.registerType('worker-function', AsyncFunctionNode, {
|
|
414
707
|
dynamicModuleList: 'libs'
|
|
415
708
|
});
|
|
416
709
|
|
|
417
710
|
// HTTP endpoint to restart workers for a specific node
|
|
418
|
-
RED.httpAdmin.post('/
|
|
711
|
+
RED.httpAdmin.post('/worker-function/:id/restart', async function(req, res) {
|
|
419
712
|
const node = RED.nodes.getNode(req.params.id);
|
|
420
713
|
if (!node || !node.pool) {
|
|
421
714
|
return res.status(404).json({ error: 'Node not found or pool not initialized' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rosepetal/node-red-contrib-async-function",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A Node-RED function node that runs code in worker threads to keep your flows responsive",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"keywords": [
|
|
10
10
|
"node-red",
|
|
11
11
|
"function",
|
|
12
|
-
"
|
|
12
|
+
"worker-function",
|
|
13
13
|
"worker-threads",
|
|
14
14
|
"non-blocking",
|
|
15
15
|
"performance",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"node-red": {
|
|
35
35
|
"version": ">=2.0.0",
|
|
36
36
|
"nodes": {
|
|
37
|
-
"
|
|
37
|
+
"worker-function": "nodes/worker-function.js"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {}
|