@inteli.city/node-red-contrib-exec-collection 1.0.3 → 1.0.5
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 +836 -5
- package/async.pg.js +1 -0
- package/exec.queue.html +228 -38
- package/exec.queue.js +554 -538
- package/exec.service.html +342 -229
- package/exec.service.js +325 -487
- package/node.queue.html +359 -0
- package/node.queue.js +569 -0
- package/package.json +19 -19
- package/python.config.html +55 -0
- package/python.config.js +24 -0
- package/python.queue.html +360 -0
- package/python.queue.js +555 -0
- package/utils/context.js +54 -0
- package/async.gpt.html +0 -327
- package/async.gpt.js +0 -615
- package/async.latex.html +0 -319
- package/async.latex.js +0 -618
- package/module.njk.html +0 -45
- package/module.njk.js +0 -12
- package/template.njk.html +0 -201
- package/template.njk.js +0 -138
- package/thrd.function.html +0 -312
- package/thrd.function.js +0 -586
- package/thread.queue.html +0 -311
- package/thread.queue.js +0 -586
package/node.queue.html
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!--* header -->
|
|
2
|
+
<script type="text/markdown" data-help-name="node.queue">
|
|
3
|
+
## node.queue
|
|
4
|
+
|
|
5
|
+
Sends code to a pool of persistent Node.js workers. Each message renders the Nunjucks template into JavaScript, dispatches it to a free worker, and returns whatever `console.log()` prints as `msg.payload`.
|
|
6
|
+
|
|
7
|
+
**Mental model:** you are not running a script — you are sending code to a running Node.js engine. State persists between messages on the same worker.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Queue
|
|
12
|
+
|
|
13
|
+
Controls how many Node.js workers run concurrently. The status badge shows `waiting (executing/limit)`:
|
|
14
|
+
|
|
15
|
+
| Status | Meaning |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Blue dot `0 (0/2)` | Workers running, all idle |
|
|
18
|
+
| Blue ring `0 (2/2)` | All workers executing |
|
|
19
|
+
| Blue ring `3 (2/2)` | 3 messages waiting, both workers busy |
|
|
20
|
+
| Grey dot `0 (0/2)` | No workers (idle timeout or not started) |
|
|
21
|
+
|
|
22
|
+
Workers start lazily on the first message. After 20 minutes idle, all workers are killed and restart on the next message.
|
|
23
|
+
|
|
24
|
+
The **⏹ button** in the node editor header kills all workers immediately — no redeploy needed. A confirmation dialog appears whenever workers are alive (executing or idle).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Writing templates
|
|
29
|
+
|
|
30
|
+
Nunjucks renders `{{ }}` expressions before the worker sees the code. All `msg` values are strings after rendering.
|
|
31
|
+
|
|
32
|
+
**Always wrap string variables in JS quotes:**
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const name = "{{ payload }}"; // correct
|
|
36
|
+
const name = {{ payload }}; // wrong — syntax error if payload is a string
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Numbers can be injected directly:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const x = {{ payload }}; // safe when payload is a number
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Warning:** Nunjucks evaluates `{{ }}` everywhere — including inside `//` and `/* */` comments. Do not put Nunjucks expressions inside comments. Comments are stripped before rendering, so they have no effect on output anyway.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Output
|
|
50
|
+
|
|
51
|
+
Use `console.log()` to produce output. Each call emits one message (in Delimited mode with `\n` delimiter).
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
console.log("hello"); // → one message: "hello"
|
|
55
|
+
console.log("world"); // → one message: "world"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`console.warn()` and `console.error()` go to stderr and become node warnings — they do not affect `msg.payload`.
|
|
59
|
+
|
|
60
|
+
The bottom row controls how output is handled:
|
|
61
|
+
|
|
62
|
+
**Output format** — how each segment is parsed (plain text, JSON, YAML, XML).
|
|
63
|
+
|
|
64
|
+
**Mode** — how output is split into messages:
|
|
65
|
+
- **Delimited** (default): buffers output and splits on the delimiter (`\n` by default). Each `console.log()` produces one message.
|
|
66
|
+
- **Raw**: emits each output chunk immediately with no buffering. Chunks may not align with `console.log()` calls.
|
|
67
|
+
|
|
68
|
+
**Delimiter** — only visible in Delimited mode. Supports escape sequences: `\n`, `\t`, `\r`, or any literal string.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Persistent state
|
|
73
|
+
|
|
74
|
+
Each worker's context persists across all messages it handles:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
if (!global.counter) {
|
|
78
|
+
global.counter = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
global.counter++;
|
|
82
|
+
console.log(global.counter);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
With Queue > 1, state is **per-worker** — no shared state between workers.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## require()
|
|
90
|
+
|
|
91
|
+
`require` is available in the worker context and resolves from Node-RED's environment:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const fs = require('fs');
|
|
95
|
+
const path = require('path');
|
|
96
|
+
const os = require('os');
|
|
97
|
+
|
|
98
|
+
console.log(JSON.stringify({ platform: os.platform(), home: os.homedir() }));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Output modes
|
|
104
|
+
|
|
105
|
+
| Mode | Result |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Plain text | stdout as string (trimmed) |
|
|
108
|
+
| Parsed JSON | `JSON.parse(stdout)` |
|
|
109
|
+
| Parsed YAML | YAML-parsed stdout |
|
|
110
|
+
| Parsed XML | XML-parsed stdout |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
[Full documentation](https://www.npmjs.com/package/@inteli.city/node-red-contrib-exec-collection) · [Nunjucks templating](https://mozilla.github.io/nunjucks/templating.html)
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<!--* styles -->
|
|
118
|
+
<style>
|
|
119
|
+
#node-nq-kill-btn:hover { color: #d9534f !important; }
|
|
120
|
+
#node-nq-kill-btn.is-executing { color: #d9534f !important; }
|
|
121
|
+
</style>
|
|
122
|
+
|
|
123
|
+
<!--* node-design -->
|
|
124
|
+
<script type="text/html" data-template-name="node.queue">
|
|
125
|
+
<div class="form-row" style="position:relative;">
|
|
126
|
+
<label for="node-input-name">
|
|
127
|
+
<i class="fa fa-tag"></i>
|
|
128
|
+
<span data-i18n="node-red:common.label.name"></span>
|
|
129
|
+
</label>
|
|
130
|
+
<div style="display: inline-block; width: calc(100% - 140px)">
|
|
131
|
+
<input type="text" id="node-input-name" style="width:100%;" data-i18n="[placeholder]node-red:common.label.name">
|
|
132
|
+
</div>
|
|
133
|
+
<button id="node-nq-kill-btn"
|
|
134
|
+
title="Kill running workers"
|
|
135
|
+
style="position:absolute; right:5px; top:50%; transform:translateY(-50%); z-index:5;
|
|
136
|
+
background:transparent; border:none; padding:6px 8px; margin-left:10px; cursor:pointer;
|
|
137
|
+
color:#888; font-size:13px; line-height:1;">
|
|
138
|
+
<i class="fa fa-stop"></i>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="form-row">
|
|
143
|
+
<label>
|
|
144
|
+
<i class="fa fa-sign-out"></i>
|
|
145
|
+
<span>Queue</span>
|
|
146
|
+
</label>
|
|
147
|
+
<input style="width:60px" type="number" id="node-input-queue" value="1">
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="form-row" style="position: relative;">
|
|
151
|
+
<label>
|
|
152
|
+
<i class="fa fa-code"></i>
|
|
153
|
+
<span>Template</span>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
<div style="position: absolute; right:0; display:inline-block; text-align: right; font-size: 0.8em;">
|
|
157
|
+
<input type="hidden" id="node-input-template" autofocus="autofocus">
|
|
158
|
+
<button id="node-template-expand-editor" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="form-row node-text-editor-row">
|
|
162
|
+
<div style="height: 300px; min-height:150px;" class="node-text-editor" id="node-input-template-editor"></div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="form-row" style="margin-bottom:0px; white-space:nowrap;">
|
|
166
|
+
<label for="node-input-output"><i class="fa fa-long-arrow-right"></i> <span data-i18n="node-red:template.label.output"></span></label>
|
|
167
|
+
<select id="node-input-output" style="width:90px;">
|
|
168
|
+
<option value="str">Plain text</option>
|
|
169
|
+
<option value="parsedJSON">Parsed JSON</option>
|
|
170
|
+
<option value="parsedYAML">Parsed YAML</option>
|
|
171
|
+
<option value="parsedXML">Parsed XML</option>
|
|
172
|
+
</select>
|
|
173
|
+
|
|
174
|
+
<label style="margin-left:14px;" for="node-input-field"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
|
|
175
|
+
<input style="margin-left:-14px; width:118px;" type="text" id="node-input-field" placeholder="payload">
|
|
176
|
+
<input style="margin-left:-14px; width:118px;" type="hidden" id="node-input-fieldType">
|
|
177
|
+
|
|
178
|
+
<label style="margin-left:14px;">
|
|
179
|
+
<i class="fa fa-random"></i>
|
|
180
|
+
<span>Parsing</span>
|
|
181
|
+
</label>
|
|
182
|
+
<select id="node-input-streamMode" style="width:90px; margin-left:-20px;">
|
|
183
|
+
<option value="delimited">Delimited</option>
|
|
184
|
+
<option value="raw">Raw</option>
|
|
185
|
+
</select>
|
|
186
|
+
<input type="text" id="node-input-delimiter" style="margin-left:4px; width:46px;" placeholder="\n">
|
|
187
|
+
</div>
|
|
188
|
+
<div id="node-nq-raw-hint" style="display:none; text-align:right; margin-top:3px; font-size:0.75em; color:#aaa;">
|
|
189
|
+
Raw output may produce incomplete messages.
|
|
190
|
+
</div>
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<!--* javascript -->
|
|
194
|
+
<script type="text/javascript">
|
|
195
|
+
RED.nodes.registerType('node.queue', {
|
|
196
|
+
color: "rgb(100, 180, 140)",
|
|
197
|
+
category: 'function',
|
|
198
|
+
defaults: {
|
|
199
|
+
name: { value: "" },
|
|
200
|
+
queue: { value: 1 },
|
|
201
|
+
streamMode: { value: "delimited" },
|
|
202
|
+
delimiter: { value: "\\n", validate: function(v) { return typeof v === 'string' && v.length > 0; } },
|
|
203
|
+
template: { value: "// All msg values render as strings. Wrap in quotes for JS string literals.\n// WARNING: Nunjucks evaluates {{ }} everywhere, including inside // comments.\n\nconst data = \"{{ payload }}\"\nconsole.log(data)" },
|
|
204
|
+
output: { value: "str" },
|
|
205
|
+
field: { value: "payload", validate: RED.validators.typedInput("fieldType") },
|
|
206
|
+
fieldType: { value: "msg" },
|
|
207
|
+
currentLine: { value: { row: 0, column: 0 } }
|
|
208
|
+
},
|
|
209
|
+
inputs: 1,
|
|
210
|
+
outputs: 1,
|
|
211
|
+
icon: "cog.png",
|
|
212
|
+
label: function() {
|
|
213
|
+
return this.name || "node.queue";
|
|
214
|
+
},
|
|
215
|
+
labelStyle: function() {
|
|
216
|
+
return this.name ? "node_label_italic" : "";
|
|
217
|
+
},
|
|
218
|
+
oneditprepare: function() {
|
|
219
|
+
var that = this;
|
|
220
|
+
|
|
221
|
+
if (!this.field) { this.field = 'payload'; }
|
|
222
|
+
if (!this.fieldType) { this.fieldType = 'msg'; }
|
|
223
|
+
|
|
224
|
+
$("#node-input-field").typedInput({
|
|
225
|
+
default: 'msg',
|
|
226
|
+
types: ['msg', 'flow', 'global'],
|
|
227
|
+
typeField: $("#node-input-fieldType")
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
function updateStreamUI() {
|
|
231
|
+
var mode = $("#node-input-streamMode").val();
|
|
232
|
+
var $sel = $("#node-input-streamMode");
|
|
233
|
+
var $rawOpt = $sel.find("option[value='raw']");
|
|
234
|
+
if (mode === 'raw') {
|
|
235
|
+
$("#node-input-delimiter").hide();
|
|
236
|
+
$("#node-nq-raw-hint").show();
|
|
237
|
+
$sel.css("color", "#b8860b");
|
|
238
|
+
$rawOpt.text("⚠ Raw");
|
|
239
|
+
$sel.attr("title", "Raw mode emits unprocessed chunks — messages may be incomplete.");
|
|
240
|
+
} else {
|
|
241
|
+
$("#node-input-delimiter").show();
|
|
242
|
+
$("#node-nq-raw-hint").hide();
|
|
243
|
+
$sel.css("color", "");
|
|
244
|
+
$rawOpt.text("Raw");
|
|
245
|
+
$sel.removeAttr("title");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
$("#node-input-streamMode").on("change", updateStreamUI);
|
|
249
|
+
updateStreamUI();
|
|
250
|
+
|
|
251
|
+
this.editor = RED.editor.createEditor({
|
|
252
|
+
id: 'node-input-template-editor',
|
|
253
|
+
mode: 'ace/mode/javascript',
|
|
254
|
+
value: $("#node-input-template").val()
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (this.currentLine === undefined) { this.currentLine = { row: 0, column: 0 }; }
|
|
258
|
+
this.editor.resize(true);
|
|
259
|
+
this.editor.scrollToLine(this.currentLine.row + 1, true, true, function() {});
|
|
260
|
+
this.editor.gotoLine(this.currentLine.row + 1, this.currentLine.column + 1, true);
|
|
261
|
+
|
|
262
|
+
RED.popover.tooltip(
|
|
263
|
+
$("#node-template-expand-editor"),
|
|
264
|
+
RED._("node-red:common.label.expand")
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
$("#node-template-expand-editor").on("click", function(e) {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
var value = that.editor.getValue();
|
|
270
|
+
RED.editor.editText({
|
|
271
|
+
mode: 'javascript',
|
|
272
|
+
value: value,
|
|
273
|
+
width: "Infinity",
|
|
274
|
+
cursor: that.editor.getCursorPosition(),
|
|
275
|
+
complete: function(v, cursor) {
|
|
276
|
+
that.editor.setValue(v, -1);
|
|
277
|
+
that.editor.gotoLine(cursor.row + 1, cursor.column, false);
|
|
278
|
+
setTimeout(function() { that.editor.focus(); }, 300);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
setTimeout(function() {
|
|
284
|
+
var nodeId = that.id;
|
|
285
|
+
|
|
286
|
+
function refreshKillBtn() {
|
|
287
|
+
$.get("node-queue/" + nodeId + "/status", function(data) {
|
|
288
|
+
var executing = parseInt(data.executing, 10) || 0;
|
|
289
|
+
$("#node-nq-kill-btn").toggleClass("is-executing", executing > 0);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
refreshKillBtn();
|
|
293
|
+
var killBtnInterval = setInterval(refreshKillBtn, 1000);
|
|
294
|
+
|
|
295
|
+
$("#node-nq-kill-btn").on("click", function(e) {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
|
|
299
|
+
function doKill() {
|
|
300
|
+
$.post("node-queue/" + nodeId + "/kill");
|
|
301
|
+
setTimeout(refreshKillBtn, 300);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
$.get("node-queue/" + nodeId + "/status", function(data) {
|
|
305
|
+
var executing = parseInt(data.executing, 10) || 0;
|
|
306
|
+
var workers = parseInt(data.workers, 10) || 0;
|
|
307
|
+
console.log("[node.queue kill] click — nodeId:", nodeId, "executing:", executing, "workers:", workers);
|
|
308
|
+
if (workers > 0) {
|
|
309
|
+
var msg = executing > 0
|
|
310
|
+
? "Kill <strong>" + executing + "</strong> running worker" + (executing > 1 ? "s" : "") + "?"
|
|
311
|
+
: "Kill idle workers?";
|
|
312
|
+
var $dlg = $("<div>")
|
|
313
|
+
.html(msg)
|
|
314
|
+
.dialog({
|
|
315
|
+
modal: true,
|
|
316
|
+
title: "Kill running workers",
|
|
317
|
+
closeOnEscape: true,
|
|
318
|
+
width: 320,
|
|
319
|
+
buttons: {
|
|
320
|
+
"Cancel": function() { $(this).dialog("close"); },
|
|
321
|
+
"Kill": function() { $(this).dialog("close"); doKill(); }
|
|
322
|
+
},
|
|
323
|
+
close: function() { $(this).dialog("destroy").remove(); }
|
|
324
|
+
});
|
|
325
|
+
$dlg.closest(".ui-dialog").css("z-index", 9999);
|
|
326
|
+
} else {
|
|
327
|
+
doKill();
|
|
328
|
+
}
|
|
329
|
+
}).fail(function() { doKill(); });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
that._killBtnInterval = killBtnInterval;
|
|
333
|
+
}, 0);
|
|
334
|
+
},
|
|
335
|
+
oneditsave: function() {
|
|
336
|
+
this.currentLine = this.editor.getCursorPosition();
|
|
337
|
+
$("#node-input-template").val(this.editor.getValue());
|
|
338
|
+
this.editor.destroy();
|
|
339
|
+
delete this.editor;
|
|
340
|
+
if (this._killBtnInterval) { clearInterval(this._killBtnInterval); delete this._killBtnInterval; }
|
|
341
|
+
},
|
|
342
|
+
oneditcancel: function() {
|
|
343
|
+
this.editor.destroy();
|
|
344
|
+
delete this.editor;
|
|
345
|
+
if (this._killBtnInterval) { clearInterval(this._killBtnInterval); delete this._killBtnInterval; }
|
|
346
|
+
},
|
|
347
|
+
oneditresize: function(size) {
|
|
348
|
+
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
|
|
349
|
+
var height = $("#dialog-form").height();
|
|
350
|
+
for (var i = 0; i < rows.length; i++) {
|
|
351
|
+
height -= $(rows[i]).outerHeight(true);
|
|
352
|
+
}
|
|
353
|
+
var editorRow = $("#dialog-form>div.node-text-editor-row");
|
|
354
|
+
height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
|
|
355
|
+
$(".node-text-editor").css("height", height + "px");
|
|
356
|
+
this.editor.resize();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
</script>
|