@rosepetal/node-red-contrib-async-function 1.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/LICENSE +13 -0
- package/README.md +213 -0
- package/assets/example.png +0 -0
- package/nodes/async-function.html +600 -0
- package/nodes/async-function.js +351 -0
- package/nodes/lib/message-serializer.js +407 -0
- package/nodes/lib/module-installer.js +105 -0
- package/nodes/lib/shared-memory-manager.js +311 -0
- package/nodes/lib/timeout-manager.js +139 -0
- package/nodes/lib/worker-pool.js +533 -0
- package/nodes/lib/worker-script.js +192 -0
- package/package.json +41 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
<!-- Async Function Node - Node-RED Editor Configuration -->
|
|
2
|
+
|
|
3
|
+
<script type="text/javascript">
|
|
4
|
+
RED.nodes.registerType('async-function', {
|
|
5
|
+
category: 'function',
|
|
6
|
+
color: '#d7d9dc',
|
|
7
|
+
defaults: {
|
|
8
|
+
name: { value: '' },
|
|
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;'
|
|
11
|
+
},
|
|
12
|
+
outputs: { value: 1, validate: RED.validators.number() },
|
|
13
|
+
timeout: { value: 30000, validate: RED.validators.number() },
|
|
14
|
+
numWorkers: { value: 3, validate: RED.validators.number() },
|
|
15
|
+
maxQueueSize: { value: 100, validate: RED.validators.number() },
|
|
16
|
+
libs: { value: [] },
|
|
17
|
+
noerr: { value: 0 }
|
|
18
|
+
},
|
|
19
|
+
inputs: 1,
|
|
20
|
+
outputs: 1,
|
|
21
|
+
icon: 'function.svg',
|
|
22
|
+
label: function() {
|
|
23
|
+
return this.name || 'async function';
|
|
24
|
+
},
|
|
25
|
+
labelStyle: function() {
|
|
26
|
+
return this.name ? 'node_label_italic' : '';
|
|
27
|
+
},
|
|
28
|
+
oneditprepare: function() {
|
|
29
|
+
const node = this;
|
|
30
|
+
|
|
31
|
+
// Create tabs system
|
|
32
|
+
const tabs = RED.tabs.create({
|
|
33
|
+
id: "async-func-tabs",
|
|
34
|
+
onchange: function(tab) {
|
|
35
|
+
// Hide all tabs
|
|
36
|
+
$("#async-func-tabs-content").children().hide();
|
|
37
|
+
// Show selected tab
|
|
38
|
+
$("#" + tab.id).show();
|
|
39
|
+
|
|
40
|
+
// Handle editor focus for Function tab
|
|
41
|
+
if (tab.id === "async-func-tab-function") {
|
|
42
|
+
const editor = $("#" + tab.id).find('.node-text-editor').first();
|
|
43
|
+
if (editor.length && node.editor) {
|
|
44
|
+
RED.tray.resize();
|
|
45
|
+
node.editor.focus();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Add tabs
|
|
52
|
+
tabs.addTab({
|
|
53
|
+
id: "async-func-tab-setup",
|
|
54
|
+
iconClass: "fa fa-cog",
|
|
55
|
+
label: "Setup"
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
tabs.addTab({
|
|
59
|
+
id: "async-func-tab-function",
|
|
60
|
+
label: "Function"
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Activate Function tab by default
|
|
64
|
+
tabs.activateTab("async-func-tab-function");
|
|
65
|
+
|
|
66
|
+
// Build extraLibs for Monaco editor (format: [{var, module}])
|
|
67
|
+
var extraLibs = (this.libs || []).map(function(lib) {
|
|
68
|
+
return { var: lib.var, module: lib.module };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Setup editor with extraLibs for Monaco IntelliSense
|
|
72
|
+
this.editor = RED.editor.createEditor({
|
|
73
|
+
id: 'node-input-func-editor',
|
|
74
|
+
mode: 'ace/mode/nrjavascript',
|
|
75
|
+
value: $('#node-input-func').val(),
|
|
76
|
+
globals: {
|
|
77
|
+
msg: true,
|
|
78
|
+
require: true,
|
|
79
|
+
console: true,
|
|
80
|
+
setTimeout: true,
|
|
81
|
+
clearTimeout: true,
|
|
82
|
+
setInterval: true,
|
|
83
|
+
clearInterval: true,
|
|
84
|
+
Buffer: true,
|
|
85
|
+
process: true
|
|
86
|
+
},
|
|
87
|
+
extraLibs: extraLibs
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Refresh Monaco module libs after editor is ready
|
|
91
|
+
var that = this;
|
|
92
|
+
if (extraLibs.length > 0) {
|
|
93
|
+
setTimeout(function() {
|
|
94
|
+
if (that.editor && that.editor.nodered && that.editor.nodered.refreshModuleLibs) {
|
|
95
|
+
that.editor.nodered.refreshModuleLibs(extraLibs);
|
|
96
|
+
}
|
|
97
|
+
}, 200);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Setup outputs spinner
|
|
101
|
+
$('#node-input-outputs').spinner({
|
|
102
|
+
min: 0,
|
|
103
|
+
max: 10
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Setup timeout input
|
|
107
|
+
$('#node-input-timeout').spinner({
|
|
108
|
+
min: 1000,
|
|
109
|
+
max: 300000,
|
|
110
|
+
step: 1000
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Setup worker pool spinners
|
|
114
|
+
$('#node-input-numWorkers').spinner({ min: 1, max: 16 });
|
|
115
|
+
$('#node-input-maxQueueSize').spinner({ min: 10, max: 1000, step: 10 });
|
|
116
|
+
|
|
117
|
+
// Setup modules editable list
|
|
118
|
+
$('#node-input-libs-container').css('min-height','120px').editableList({
|
|
119
|
+
addButton: true,
|
|
120
|
+
removable: true,
|
|
121
|
+
sortable: false,
|
|
122
|
+
header: $('<div class="async-func-libs-header"><div>Module</div><div>Import as</div></div>'),
|
|
123
|
+
addItem: function(container, i, opt) {
|
|
124
|
+
var row = $('<div class="async-func-libs-entry"/>').appendTo(container);
|
|
125
|
+
$('<input/>', {
|
|
126
|
+
class: 'node-input-libs-module',
|
|
127
|
+
type: 'text',
|
|
128
|
+
placeholder: 'e.g. lodash'
|
|
129
|
+
}).val(opt.module || '').appendTo(row);
|
|
130
|
+
$('<input/>', {
|
|
131
|
+
class: 'node-input-libs-var',
|
|
132
|
+
type: 'text',
|
|
133
|
+
placeholder: 'e.g. _'
|
|
134
|
+
}).val(opt.var || '').appendTo(row);
|
|
135
|
+
|
|
136
|
+
// Auto-generate variable name from module name
|
|
137
|
+
row.find('.node-input-libs-module').on('change keyup', function() {
|
|
138
|
+
var varInput = row.find('.node-input-libs-var');
|
|
139
|
+
if (!varInput.val()) {
|
|
140
|
+
var moduleName = $(this).val().trim();
|
|
141
|
+
// Generate variable name: strip scope, replace dashes/dots with camelCase
|
|
142
|
+
var varName = moduleName
|
|
143
|
+
.replace(/^@[^/]+\//, '') // Remove npm scope
|
|
144
|
+
.replace(/[-_.]/g, ' ')
|
|
145
|
+
.replace(/\s+(.)/g, function(m, c) { return c.toUpperCase(); })
|
|
146
|
+
.replace(/\s/g, '');
|
|
147
|
+
varInput.val(varName);
|
|
148
|
+
varInput.trigger('change');
|
|
149
|
+
}
|
|
150
|
+
}).on('change', function() {
|
|
151
|
+
// Update editor modules when module name changes
|
|
152
|
+
if (typeof updateEditorModules === 'function') {
|
|
153
|
+
updateEditorModules();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Validate variable name is a valid JS identifier and update editor
|
|
158
|
+
row.find('.node-input-libs-var').on('change keyup', function() {
|
|
159
|
+
var val = $(this).val().trim();
|
|
160
|
+
var isValid = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(val);
|
|
161
|
+
var reserved = ['msg', 'require', 'console', 'setTimeout', 'setInterval',
|
|
162
|
+
'clearTimeout', 'clearInterval', 'Buffer', 'process'];
|
|
163
|
+
if (reserved.indexOf(val) !== -1) {
|
|
164
|
+
isValid = false;
|
|
165
|
+
}
|
|
166
|
+
if (val && !isValid) {
|
|
167
|
+
$(this).addClass('input-error');
|
|
168
|
+
} else {
|
|
169
|
+
$(this).removeClass('input-error');
|
|
170
|
+
}
|
|
171
|
+
}).on('change', function() {
|
|
172
|
+
// Update editor modules when variable name changes
|
|
173
|
+
if (typeof updateEditorModules === 'function') {
|
|
174
|
+
updateEditorModules();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Populate existing libs
|
|
181
|
+
if (this.libs && this.libs.length > 0) {
|
|
182
|
+
var libsList = this.libs;
|
|
183
|
+
for (var i = 0; i < libsList.length; i++) {
|
|
184
|
+
$('#node-input-libs-container').editableList('addItem', libsList[i]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get current libs list from editableList
|
|
189
|
+
function getLibsList() {
|
|
190
|
+
var libs = [];
|
|
191
|
+
$('#node-input-libs-container').editableList('items').each(function() {
|
|
192
|
+
var moduleName = $(this).find('.node-input-libs-module').val().trim();
|
|
193
|
+
var varName = $(this).find('.node-input-libs-var').val().trim();
|
|
194
|
+
if (moduleName && varName) {
|
|
195
|
+
libs.push({ var: varName, module: moduleName });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return libs;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Update editor when modules change (supports both Monaco and ACE)
|
|
202
|
+
function updateEditorModules() {
|
|
203
|
+
if (node.editor) {
|
|
204
|
+
var libs = getLibsList();
|
|
205
|
+
|
|
206
|
+
// For Monaco editor: use refreshModuleLibs
|
|
207
|
+
if (node.editor.nodered && node.editor.nodered.refreshModuleLibs) {
|
|
208
|
+
node.editor.nodered.refreshModuleLibs(libs);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// For ACE editor: update globals
|
|
212
|
+
if (node.editor.session && node.editor.session.$worker) {
|
|
213
|
+
var globals = {
|
|
214
|
+
msg: true,
|
|
215
|
+
require: true,
|
|
216
|
+
console: true,
|
|
217
|
+
setTimeout: true,
|
|
218
|
+
clearTimeout: true,
|
|
219
|
+
setInterval: true,
|
|
220
|
+
clearInterval: true,
|
|
221
|
+
Buffer: true,
|
|
222
|
+
process: true
|
|
223
|
+
};
|
|
224
|
+
libs.forEach(function(lib) {
|
|
225
|
+
if (lib.var) {
|
|
226
|
+
globals[lib.var] = true;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
node.editor.session.$worker.send("setOptions", [{
|
|
230
|
+
globals: globals,
|
|
231
|
+
maxerr: 1000
|
|
232
|
+
}]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Hook into editableList add/remove events and variable name changes
|
|
238
|
+
$('#node-input-libs-container').on('editablelistitemadd editablelistitemremove', updateEditorModules);
|
|
239
|
+
// Ensure configured module vars are registered in the editor on load
|
|
240
|
+
updateEditorModules();
|
|
241
|
+
|
|
242
|
+
// Resize editor on dialog resize
|
|
243
|
+
$('#dialog-form').on('dialogresize', function() {
|
|
244
|
+
const rows = $('#dialog-form>div:not(#async-func-tabs-content)');
|
|
245
|
+
let height = $('#dialog-form').height();
|
|
246
|
+
for (let i = 0; i < rows.length; i++) {
|
|
247
|
+
height -= $(rows[i]).outerHeight(true);
|
|
248
|
+
}
|
|
249
|
+
height -= 20; // Tab container margin adjustment
|
|
250
|
+
|
|
251
|
+
// Calculate editor height (only in Function tab)
|
|
252
|
+
const editorRow = $('#async-func-tab-function .node-text-editor-row');
|
|
253
|
+
const editorMargins = parseInt(editorRow.css('marginTop')) +
|
|
254
|
+
parseInt(editorRow.css('marginBottom'));
|
|
255
|
+
|
|
256
|
+
// Subtract Function tab's non-editor rows
|
|
257
|
+
const functionTabRows = $('#async-func-tab-function>div:not(.node-text-editor-row)');
|
|
258
|
+
let editorHeight = height;
|
|
259
|
+
for (let i = 0; i < functionTabRows.length; i++) {
|
|
260
|
+
editorHeight -= $(functionTabRows[i]).outerHeight(true);
|
|
261
|
+
}
|
|
262
|
+
editorHeight -= editorMargins;
|
|
263
|
+
|
|
264
|
+
$('.node-text-editor').css('height', editorHeight + 'px');
|
|
265
|
+
node.editor.resize();
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
oneditsave: function() {
|
|
269
|
+
// Save editor content
|
|
270
|
+
$('#node-input-func').val(this.editor.getValue());
|
|
271
|
+
this.editor.destroy();
|
|
272
|
+
delete this.editor;
|
|
273
|
+
|
|
274
|
+
// Save libs
|
|
275
|
+
var libs = [];
|
|
276
|
+
$('#node-input-libs-container').editableList('items').each(function() {
|
|
277
|
+
var module = $(this).find('.node-input-libs-module').val().trim();
|
|
278
|
+
var varName = $(this).find('.node-input-libs-var').val().trim();
|
|
279
|
+
if (module && varName) {
|
|
280
|
+
libs.push({ module: module, var: varName });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
this.libs = libs;
|
|
284
|
+
},
|
|
285
|
+
oneditcancel: function() {
|
|
286
|
+
this.editor.destroy();
|
|
287
|
+
delete this.editor;
|
|
288
|
+
},
|
|
289
|
+
oneditresize: function(size) {
|
|
290
|
+
// Calculate available height excluding non-tab rows
|
|
291
|
+
const rows = $('#dialog-form>div:not(#async-func-tabs-content)');
|
|
292
|
+
let height = size.height;
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < rows.length; i++) {
|
|
295
|
+
height -= $(rows[i]).outerHeight(true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
height -= 20; // Tab container margin adjustment
|
|
299
|
+
|
|
300
|
+
// Calculate editor height (only in Function tab)
|
|
301
|
+
const editorRow = $('#async-func-tab-function .node-text-editor-row');
|
|
302
|
+
const editorMargins = parseInt(editorRow.css('marginTop')) +
|
|
303
|
+
parseInt(editorRow.css('marginBottom'));
|
|
304
|
+
|
|
305
|
+
// Subtract Function tab's non-editor rows
|
|
306
|
+
const functionTabRows = $('#async-func-tab-function>div:not(.node-text-editor-row)');
|
|
307
|
+
let editorHeight = height;
|
|
308
|
+
for (let i = 0; i < functionTabRows.length; i++) {
|
|
309
|
+
editorHeight -= $(functionTabRows[i]).outerHeight(true);
|
|
310
|
+
}
|
|
311
|
+
editorHeight -= editorMargins;
|
|
312
|
+
|
|
313
|
+
$('.node-text-editor').css('height', editorHeight + 'px');
|
|
314
|
+
|
|
315
|
+
if (this.editor) {
|
|
316
|
+
this.editor.resize();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
</script>
|
|
321
|
+
|
|
322
|
+
<script type="text/html" data-template-name="async-function">
|
|
323
|
+
<style>
|
|
324
|
+
.async-func-tabs-row {
|
|
325
|
+
margin-bottom: 0;
|
|
326
|
+
}
|
|
327
|
+
.async-func-section {
|
|
328
|
+
border: 1px solid #ddd;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
padding: 10px 15px;
|
|
331
|
+
margin-bottom: 15px;
|
|
332
|
+
background: #fafafa;
|
|
333
|
+
}
|
|
334
|
+
.async-func-section-header {
|
|
335
|
+
font-weight: 500;
|
|
336
|
+
margin-bottom: 10px;
|
|
337
|
+
color: #333;
|
|
338
|
+
border-bottom: 1px solid #eee;
|
|
339
|
+
padding-bottom: 5px;
|
|
340
|
+
}
|
|
341
|
+
.async-func-section .form-row {
|
|
342
|
+
margin-bottom: 8px;
|
|
343
|
+
}
|
|
344
|
+
.async-func-section .form-row:last-child {
|
|
345
|
+
margin-bottom: 0;
|
|
346
|
+
}
|
|
347
|
+
/* Modules editableList styles */
|
|
348
|
+
#node-input-libs-container {
|
|
349
|
+
min-height: 120px;
|
|
350
|
+
}
|
|
351
|
+
#node-input-libs-container .red-ui-editableList-header,
|
|
352
|
+
.async-func-libs-header {
|
|
353
|
+
display: flex;
|
|
354
|
+
gap: 10px;
|
|
355
|
+
padding: 6px 8px;
|
|
356
|
+
font-weight: 500;
|
|
357
|
+
color: #666;
|
|
358
|
+
background: var(--red-ui-tertiary-background, #f7f7f7);
|
|
359
|
+
border-bottom: 1px solid #ddd;
|
|
360
|
+
padding-right: 35px; /* Space for delete button column */
|
|
361
|
+
}
|
|
362
|
+
#node-input-libs-container .red-ui-editableList-header > div,
|
|
363
|
+
.async-func-libs-header > div {
|
|
364
|
+
flex: 1;
|
|
365
|
+
}
|
|
366
|
+
#node-input-libs-container .red-ui-editableList-container {
|
|
367
|
+
padding: 0;
|
|
368
|
+
}
|
|
369
|
+
#node-input-libs-container .red-ui-editableList-item-content {
|
|
370
|
+
padding: 4px 0;
|
|
371
|
+
}
|
|
372
|
+
.async-func-libs-entry {
|
|
373
|
+
display: flex;
|
|
374
|
+
gap: 10px;
|
|
375
|
+
align-items: center;
|
|
376
|
+
}
|
|
377
|
+
.async-func-libs-entry input {
|
|
378
|
+
flex: 1;
|
|
379
|
+
}
|
|
380
|
+
.async-func-libs-entry input.input-error {
|
|
381
|
+
border-color: #d9534f;
|
|
382
|
+
background-color: #fff5f5;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
385
|
+
|
|
386
|
+
<div class="form-row">
|
|
387
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
388
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- Tab Navigation Row -->
|
|
392
|
+
<div class="form-row async-func-tabs-row">
|
|
393
|
+
<ul style="min-width: 600px; margin-bottom: 20px;" id="async-func-tabs"></ul>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Tab Content Container -->
|
|
397
|
+
<div id="async-func-tabs-content" style="min-height: calc(100% - 95px);">
|
|
398
|
+
|
|
399
|
+
<!-- Setup Tab -->
|
|
400
|
+
<div id="async-func-tab-setup" style="display:none">
|
|
401
|
+
<!-- Behavior Section -->
|
|
402
|
+
<div class="async-func-section">
|
|
403
|
+
<div class="async-func-section-header">
|
|
404
|
+
<i class="fa fa-sliders"></i> Behavior
|
|
405
|
+
</div>
|
|
406
|
+
<div class="form-row">
|
|
407
|
+
<label for="node-input-outputs"><i class="fa fa-random"></i> Outputs</label>
|
|
408
|
+
<input id="node-input-outputs" style="width:60px;" value="1">
|
|
409
|
+
</div>
|
|
410
|
+
<div class="form-row">
|
|
411
|
+
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
412
|
+
<input id="node-input-timeout" style="width:120px;" value="30000">
|
|
413
|
+
<span style="margin-left:5px;">ms</span>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<!-- Worker Pool Section -->
|
|
418
|
+
<div class="async-func-section">
|
|
419
|
+
<div class="async-func-section-header">
|
|
420
|
+
<i class="fa fa-cogs"></i> Worker Pool
|
|
421
|
+
</div>
|
|
422
|
+
<div class="form-row">
|
|
423
|
+
<label for="node-input-numWorkers">Workers</label>
|
|
424
|
+
<input type="number" id="node-input-numWorkers" style="width: 80px;" min="1" max="16" value="3">
|
|
425
|
+
<span style="margin-left: 10px; color: #999;">Fixed worker count (1-16)</span>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="form-row">
|
|
428
|
+
<label for="node-input-maxQueueSize">Queue Size</label>
|
|
429
|
+
<input type="number" id="node-input-maxQueueSize" style="width: 80px;" min="10" max="1000" step="10" value="100">
|
|
430
|
+
<span style="margin-left: 10px; color: #999;">Max queued messages</span>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<!-- Modules Section -->
|
|
435
|
+
<div class="async-func-section">
|
|
436
|
+
<div class="async-func-section-header">
|
|
437
|
+
<i class="fa fa-cubes"></i> Modules
|
|
438
|
+
</div>
|
|
439
|
+
<div class="form-row" style="margin-bottom: 0;">
|
|
440
|
+
<ol id="node-input-libs-container"></ol>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<!-- Function Tab -->
|
|
446
|
+
<div id="async-func-tab-function" style="display:none">
|
|
447
|
+
<div class="form-row" style="margin-bottom:0;">
|
|
448
|
+
<label for="node-input-func"><i class="fa fa-wrench"></i> Function</label>
|
|
449
|
+
<input type="hidden" id="node-input-func">
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="form-row node-text-editor-row">
|
|
453
|
+
<div style="height:250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor"></div>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div class="form-tips">
|
|
457
|
+
<b>Tips:</b>
|
|
458
|
+
<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>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
</div>
|
|
469
|
+
</script>
|
|
470
|
+
|
|
471
|
+
<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>
|
|
473
|
+
|
|
474
|
+
<h3>Inputs</h3>
|
|
475
|
+
<dl class="message-properties">
|
|
476
|
+
<dt>payload <span class="property-type">any</span></dt>
|
|
477
|
+
<dd>The message payload to process</dd>
|
|
478
|
+
<dt class="optional">topic <span class="property-type">string</span></dt>
|
|
479
|
+
<dd>The message topic</dd>
|
|
480
|
+
</dl>
|
|
481
|
+
|
|
482
|
+
<h3>Outputs</h3>
|
|
483
|
+
<dl class="message-properties">
|
|
484
|
+
<dt>payload <span class="property-type">any</span></dt>
|
|
485
|
+
<dd>The processed message payload</dd>
|
|
486
|
+
</dl>
|
|
487
|
+
|
|
488
|
+
<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>
|
|
491
|
+
|
|
492
|
+
<h4>Available in Code</h4>
|
|
493
|
+
<ul>
|
|
494
|
+
<li><code>msg</code> - The incoming message object</li>
|
|
495
|
+
<li><code>return</code> - Return modified message or array of messages</li>
|
|
496
|
+
<li><code>async/await</code> - Use async operations</li>
|
|
497
|
+
<li><code>require()</code> - Import Node.js modules</li>
|
|
498
|
+
<li><code>console</code> - Logging functions</li>
|
|
499
|
+
<li><code>setTimeout</code>, <code>setInterval</code> - Timers</li>
|
|
500
|
+
</ul>
|
|
501
|
+
|
|
502
|
+
<h4>Not Available</h4>
|
|
503
|
+
<ul>
|
|
504
|
+
<li><code>context</code>, <code>flow</code>, <code>global</code> - Context storage (v2.0 feature)</li>
|
|
505
|
+
<li><code>node</code> - Node instance methods</li>
|
|
506
|
+
<li>Non-serializable objects in <code>msg</code></li>
|
|
507
|
+
</ul>
|
|
508
|
+
|
|
509
|
+
<h4>Example: Simple Transformation</h4>
|
|
510
|
+
<pre>msg.payload = msg.payload * 2;
|
|
511
|
+
return msg;</pre>
|
|
512
|
+
|
|
513
|
+
<h4>Example: Async Operation</h4>
|
|
514
|
+
<pre>const crypto = require('crypto');
|
|
515
|
+
|
|
516
|
+
msg.hash = crypto.createHash('sha256')
|
|
517
|
+
.update(msg.payload)
|
|
518
|
+
.digest('hex');
|
|
519
|
+
|
|
520
|
+
return msg;</pre>
|
|
521
|
+
|
|
522
|
+
<h4>Example: Multiple Outputs</h4>
|
|
523
|
+
<pre>if (msg.payload > 100) {
|
|
524
|
+
return [msg, null]; // Send to first output
|
|
525
|
+
} else {
|
|
526
|
+
return [null, msg]; // Send to second output
|
|
527
|
+
}</pre>
|
|
528
|
+
|
|
529
|
+
<h4>Example: CPU-Intensive Task</h4>
|
|
530
|
+
<pre>// Calculate prime numbers (won't block Node-RED!)
|
|
531
|
+
function isPrime(n) {
|
|
532
|
+
if (n <= 1) return false;
|
|
533
|
+
for (let i = 2; i * i <= n; i++) {
|
|
534
|
+
if (n % i === 0) return false;
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const limit = msg.payload;
|
|
540
|
+
const primes = [];
|
|
541
|
+
|
|
542
|
+
for (let i = 2; i <= limit; i++) {
|
|
543
|
+
if (isPrime(i)) {
|
|
544
|
+
primes.push(i);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
msg.payload = primes;
|
|
549
|
+
return msg;</pre>
|
|
550
|
+
|
|
551
|
+
<h3>Configuration</h3>
|
|
552
|
+
<dl class="message-properties">
|
|
553
|
+
<dt>Outputs</dt>
|
|
554
|
+
<dd>Number of output ports (0-10). Use array return for multiple outputs.</dd>
|
|
555
|
+
<dt>Timeout</dt>
|
|
556
|
+
<dd>Maximum execution time in milliseconds (1000-300000). Worker is terminated if exceeded. Default: 30000 (30s).</dd>
|
|
557
|
+
<dt>Workers</dt>
|
|
558
|
+
<dd>Fixed number of worker threads (1-16). Each node maintains exactly this many workers. Default: 3.</dd>
|
|
559
|
+
<dt>Queue Size</dt>
|
|
560
|
+
<dd>Maximum number of messages queued when all workers are busy (10-1000). Default: 100.</dd>
|
|
561
|
+
<dt>Buffers</dt>
|
|
562
|
+
<dd>All Buffers are transferred via shared memory when possible (falls back to base64 if needed).</dd>
|
|
563
|
+
</dl>
|
|
564
|
+
|
|
565
|
+
<h3>Status Display</h3>
|
|
566
|
+
<p>The node status shows real-time worker pool statistics:</p>
|
|
567
|
+
<ul>
|
|
568
|
+
<li><b>Active: X/Y</b> - X workers currently processing out of Y total workers</li>
|
|
569
|
+
<li><b>Queue: Z</b> - Z messages waiting in queue</li>
|
|
570
|
+
<li><b>SHM: N</b> - N shared memory files active (shown only when >0, indicates large buffers being processed)</li>
|
|
571
|
+
<li><b>Green dot</b> - Normal operation</li>
|
|
572
|
+
<li><b>Yellow dot</b> - Queue getting full (>50 messages)</li>
|
|
573
|
+
<li><b>Red dot</b> - Queue almost full (>90%) or error</li>
|
|
574
|
+
<li><b>Ring</b> - All workers busy with queued messages</li>
|
|
575
|
+
</ul>
|
|
576
|
+
|
|
577
|
+
<h3>Error Handling</h3>
|
|
578
|
+
<p>Errors in your code will be caught and sent to a Catch node if configured:</p>
|
|
579
|
+
<pre>if (!msg.payload) {
|
|
580
|
+
throw new Error('Payload is required');
|
|
581
|
+
}</pre>
|
|
582
|
+
|
|
583
|
+
<h3>Performance Notes</h3>
|
|
584
|
+
<ul>
|
|
585
|
+
<li>Worker threads add ~5-10ms overhead per message</li>
|
|
586
|
+
<li>Best for CPU-intensive operations (>10ms execution time)</li>
|
|
587
|
+
<li>For simple operations, use the standard function node</li>
|
|
588
|
+
<li>Workers are pooled and reused for efficiency</li>
|
|
589
|
+
<li><b>Buffer Transfer:</b> Buffers are streamed through shared memory (base64 fallback)</li>
|
|
590
|
+
<li>Event loop never blocks, even when processing multi-MB binary data (images, files, etc.)</li>
|
|
591
|
+
<li>Message transfer is optimized by copying only referenced <code>msg.*</code> keys when possible</li>
|
|
592
|
+
<li>Timing is recorded on <code>msg.performance</code> under the node label</li>
|
|
593
|
+
</ul>
|
|
594
|
+
|
|
595
|
+
<h3>References</h3>
|
|
596
|
+
<ul>
|
|
597
|
+
<li><a href="https://nodejs.org/api/worker_threads.html" target="_blank">Node.js Worker Threads</a></li>
|
|
598
|
+
<li><a href="https://nodered.org/docs/user-guide/writing-functions" target="_blank">Node-RED Function Nodes</a></li>
|
|
599
|
+
</ul>
|
|
600
|
+
</script>
|