@rosepetal/node-red-contrib-utils 1.1.1
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/.github/workflows/publish.yml +54 -0
- package/assets/nodes/io/array-in-demo.gif +0 -0
- package/assets/nodes/io/array-out-demo.gif +0 -0
- package/assets/nodes/io/array-select-demo.gif +0 -0
- package/docs/nodes/io/array-in.md +119 -0
- package/docs/nodes/io/array-out.md +150 -0
- package/docs/nodes/io/array-select.md +157 -0
- package/docs/nodes/io/queue.md +57 -0
- package/docs/nodes/io/save-file.md +30 -0
- package/docs/nodes/promise-reader/promise-reader.md +60 -0
- package/docs/nodes/util/block-detect.md +46 -0
- package/docs/nodes/util/clean-debug.md +58 -0
- package/lib/node-utils.js +429 -0
- package/nodes/io/array-in.html +110 -0
- package/nodes/io/array-in.js +113 -0
- package/nodes/io/array-out.html +125 -0
- package/nodes/io/array-out.js +213 -0
- package/nodes/io/array-select.html +156 -0
- package/nodes/io/array-select.js +187 -0
- package/nodes/io/queue.html +119 -0
- package/nodes/io/queue.js +191 -0
- package/nodes/io/save-file.html +243 -0
- package/nodes/io/save-file.js +440 -0
- package/nodes/promise-reader/promise-reader.html +100 -0
- package/nodes/promise-reader/promise-reader.js +118 -0
- package/nodes/util/block-detect.html +85 -0
- package/nodes/util/block-detect.js +136 -0
- package/nodes/util/clean-debug.html +201 -0
- package/nodes/util/clean-debug.js +393 -0
- package/package.json +55 -0
- package/readme.md +72 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const util = require("util");
|
|
4
|
+
|
|
5
|
+
module.exports = function registerCleanDebugNode(RED) {
|
|
6
|
+
const DEFAULT_DEBUG_MAX_LENGTH = (RED.settings && RED.settings.debugMaxLength) || 1000;
|
|
7
|
+
const BUFFER_PREVIEW_BYTES = Math.max(DEFAULT_DEBUG_MAX_LENGTH, 512);
|
|
8
|
+
|
|
9
|
+
function CleanDebugNode(config) {
|
|
10
|
+
RED.nodes.createNode(this, config);
|
|
11
|
+
const node = this;
|
|
12
|
+
|
|
13
|
+
node.name = config.name;
|
|
14
|
+
node.active = config.active !== false;
|
|
15
|
+
node.tosidebar = config.tosidebar !== false;
|
|
16
|
+
node.console = !!config.console;
|
|
17
|
+
node.complete = (config.complete && config.complete !== "") ? config.complete : "payload";
|
|
18
|
+
node.targetType = config.targetType || "msg";
|
|
19
|
+
node.clean = config.clean !== false; // default true
|
|
20
|
+
|
|
21
|
+
node.on("input", function onInput(msg, send, done) {
|
|
22
|
+
if (!node.active) {
|
|
23
|
+
if (done) {
|
|
24
|
+
done();
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolveDebugValue(msg)
|
|
30
|
+
.then((resolved) => handleDebugOutput(msg, resolved))
|
|
31
|
+
.then(() => {
|
|
32
|
+
if (done) {
|
|
33
|
+
done();
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.catch((err) => {
|
|
37
|
+
node.error(err && err.message ? err.message : err, msg);
|
|
38
|
+
if (done) {
|
|
39
|
+
done(err);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the configured value the node should display.
|
|
46
|
+
* @param {object} msg
|
|
47
|
+
* @returns {Promise<*>}
|
|
48
|
+
*/
|
|
49
|
+
function resolveDebugValue(msg) {
|
|
50
|
+
if (node.targetType === "full" || node.targetType === "complete") {
|
|
51
|
+
return Promise.resolve(RED.util.cloneMessage(msg));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node.targetType === "msg" || node.targetType === "msgProperty") {
|
|
55
|
+
const property = node.complete || "payload";
|
|
56
|
+
try {
|
|
57
|
+
return Promise.resolve(RED.util.getMessageProperty(msg, property));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return Promise.reject(err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const property = node.complete || "";
|
|
65
|
+
RED.util.evaluateNodeProperty(property, node.targetType, node, msg, (err, value) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
reject(err);
|
|
68
|
+
} else {
|
|
69
|
+
resolve(value);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emit to the configured outputs (sidebar/console).
|
|
77
|
+
* @param {object} msg
|
|
78
|
+
* @param {*} value
|
|
79
|
+
*/
|
|
80
|
+
function handleDebugOutput(msg, value) {
|
|
81
|
+
const mode = node.clean ? "clean" : "full";
|
|
82
|
+
const preparedValue = sanitiseValue(value, { mode });
|
|
83
|
+
|
|
84
|
+
if (node.console) {
|
|
85
|
+
const consoleValue = node.clean ? preparedValue : value;
|
|
86
|
+
const formatted = formatForConsole(consoleValue);
|
|
87
|
+
node.log(formatted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (node.tosidebar !== false) {
|
|
91
|
+
publishToSidebar(msg, preparedValue);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Publish the debug payload to the editor sidebar.
|
|
97
|
+
* @param {object} msg
|
|
98
|
+
* @param {*} value
|
|
99
|
+
*/
|
|
100
|
+
function publishToSidebar(msg, value) {
|
|
101
|
+
if (!RED.comms || typeof RED.comms.publish !== "function") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const format = detectValueType(value);
|
|
106
|
+
const debugMessage = {
|
|
107
|
+
id: node.id,
|
|
108
|
+
z: node.z,
|
|
109
|
+
_alias: node._alias,
|
|
110
|
+
name: node.name,
|
|
111
|
+
topic: msg && msg.topic,
|
|
112
|
+
property: node.targetType === "full" ? "msg" : node.complete,
|
|
113
|
+
propertyType: node.targetType,
|
|
114
|
+
clean: node.clean,
|
|
115
|
+
format,
|
|
116
|
+
msg: value,
|
|
117
|
+
_msgid: msg && msg._msgid
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
RED.comms.publish("debug", debugMessage);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Produce a lightweight clone that is safe to ship to the editor.
|
|
126
|
+
* @param {*} input
|
|
127
|
+
* @param {object} [options]
|
|
128
|
+
* @param {"clean"|"full"} [options.mode="clean"]
|
|
129
|
+
* @returns {*}
|
|
130
|
+
*/
|
|
131
|
+
function sanitiseValue(input, options = {}) {
|
|
132
|
+
const mode = options.mode === "full" ? "full" : "clean";
|
|
133
|
+
const opts = {
|
|
134
|
+
maxDepth: options.maxDepth || 6,
|
|
135
|
+
maxArrayLength: options.maxArrayLength || (mode === "clean" ? 50 : 100),
|
|
136
|
+
maxObjectKeys: options.maxObjectKeys || (mode === "clean" ? 60 : 120),
|
|
137
|
+
maxStringLength: options.maxStringLength || (mode === "clean" ? 2048 : DEFAULT_DEBUG_MAX_LENGTH),
|
|
138
|
+
bufferPreviewLength: options.bufferPreviewLength || (mode === "clean" ? 0 : BUFFER_PREVIEW_BYTES),
|
|
139
|
+
mode
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const seen = new WeakMap();
|
|
143
|
+
|
|
144
|
+
function helper(value, depth, path) {
|
|
145
|
+
if (value === null || value === undefined) {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const valueType = typeof value;
|
|
150
|
+
if (valueType === "number" || valueType === "boolean") {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
if (valueType === "bigint") {
|
|
154
|
+
return `${value.toString()}n`;
|
|
155
|
+
}
|
|
156
|
+
if (valueType === "string") {
|
|
157
|
+
return sanitiseString(value, opts);
|
|
158
|
+
}
|
|
159
|
+
if (valueType === "symbol") {
|
|
160
|
+
return value.toString();
|
|
161
|
+
}
|
|
162
|
+
if (valueType === "function") {
|
|
163
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Buffer.isBuffer(value)) {
|
|
167
|
+
return encodeBuffer(value, opts);
|
|
168
|
+
}
|
|
169
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
170
|
+
return encodeTypedArray(value, opts);
|
|
171
|
+
}
|
|
172
|
+
if (value instanceof ArrayBuffer) {
|
|
173
|
+
return encodeTypedArray(new Uint8Array(value), opts);
|
|
174
|
+
}
|
|
175
|
+
if (value instanceof Date) {
|
|
176
|
+
return value.toISOString();
|
|
177
|
+
}
|
|
178
|
+
if (value instanceof RegExp) {
|
|
179
|
+
return value.toString();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof value === "object") {
|
|
183
|
+
if (seen.has(value)) {
|
|
184
|
+
return `[Circular ~${seen.get(value)}]`;
|
|
185
|
+
}
|
|
186
|
+
seen.set(value, path || "~");
|
|
187
|
+
|
|
188
|
+
if (value.type === "Buffer" && Array.isArray(value.data)) {
|
|
189
|
+
if (opts.mode === "clean") {
|
|
190
|
+
return createPlaceholder("Buffer", `${value.data.length} bytes`);
|
|
191
|
+
}
|
|
192
|
+
return encodeBuffer(Buffer.from(value.data), opts);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (depth >= opts.maxDepth) {
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
return `[Array(${value.length})]`;
|
|
198
|
+
}
|
|
199
|
+
return `[Object with ${Object.keys(value).length} keys]`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
const result = [];
|
|
204
|
+
const len = Math.min(value.length, opts.maxArrayLength);
|
|
205
|
+
for (let i = 0; i < len; i += 1) {
|
|
206
|
+
const childPath = `${path || "~"}[${i}]`;
|
|
207
|
+
result.push(helper(value[i], depth + 1, childPath));
|
|
208
|
+
}
|
|
209
|
+
if (value.length > opts.maxArrayLength) {
|
|
210
|
+
result.push(`… ${value.length - opts.maxArrayLength} more items`);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const keys = Object.keys(value);
|
|
216
|
+
const clone = {};
|
|
217
|
+
const limit = Math.min(keys.length, opts.maxObjectKeys);
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < limit; i += 1) {
|
|
220
|
+
const key = keys[i];
|
|
221
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
222
|
+
clone[key] = helper(value[key], depth + 1, childPath);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (keys.length > opts.maxObjectKeys) {
|
|
226
|
+
clone.__cleanDebugTruncated = true;
|
|
227
|
+
clone.__cleanDebugNote = `${keys.length - opts.maxObjectKeys} more keys not shown`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return clone;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return helper(input, 0, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Sanitise long/base64 strings with heuristics.
|
|
241
|
+
* @param {string} value
|
|
242
|
+
* @param {object} opts
|
|
243
|
+
*/
|
|
244
|
+
function sanitiseString(value, opts) {
|
|
245
|
+
const trimmed = value.trim();
|
|
246
|
+
if (opts.mode === "clean" && trimmed.startsWith("data:image/")) {
|
|
247
|
+
return createPlaceholder("DataImage", `${value.length} chars`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (opts.mode === "clean" && isLikelyBase64(trimmed)) {
|
|
251
|
+
return createPlaceholder("Base64", `${value.length} chars`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (value.length > opts.maxStringLength) {
|
|
255
|
+
return `${value.slice(0, opts.maxStringLength)}… [truncated ${value.length - opts.maxStringLength} chars]`;
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Encode a buffer so the UI can inspect it without loading all bytes.
|
|
262
|
+
* @param {Buffer} buffer
|
|
263
|
+
* @param {object} opts
|
|
264
|
+
*/
|
|
265
|
+
function encodeBuffer(buffer, opts) {
|
|
266
|
+
if (opts.mode === "clean" || opts.bufferPreviewLength <= 0) {
|
|
267
|
+
return createPlaceholder("Buffer", `${buffer.length} bytes`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const previewLength = Math.min(buffer.length, opts.bufferPreviewLength);
|
|
271
|
+
const slice = buffer.slice(0, previewLength).toJSON();
|
|
272
|
+
slice.length = buffer.length;
|
|
273
|
+
if (previewLength < buffer.length) {
|
|
274
|
+
slice.__cleanDebugTruncated = true;
|
|
275
|
+
slice.__cleanDebugNote = `${buffer.length - previewLength} more bytes not shown`;
|
|
276
|
+
}
|
|
277
|
+
return slice;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Encode typed arrays safely.
|
|
282
|
+
* @param {TypedArray} view
|
|
283
|
+
* @param {object} opts
|
|
284
|
+
*/
|
|
285
|
+
function encodeTypedArray(view, opts) {
|
|
286
|
+
if (opts.mode === "clean") {
|
|
287
|
+
const ctor = view.constructor && view.constructor.name ? view.constructor.name : "TypedArray";
|
|
288
|
+
return createPlaceholder(ctor, `${view.length} items`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const limit = Math.min(view.length, opts.maxArrayLength);
|
|
292
|
+
const data = [];
|
|
293
|
+
for (let i = 0; i < limit; i += 1) {
|
|
294
|
+
data.push(view[i]);
|
|
295
|
+
}
|
|
296
|
+
const descriptor = {
|
|
297
|
+
type: view.constructor && view.constructor.name ? view.constructor.name : "TypedArray",
|
|
298
|
+
length: view.length,
|
|
299
|
+
data
|
|
300
|
+
};
|
|
301
|
+
if (limit < view.length) {
|
|
302
|
+
descriptor.__cleanDebugTruncated = true;
|
|
303
|
+
descriptor.__cleanDebugNote = `${view.length - limit} more items not shown`;
|
|
304
|
+
}
|
|
305
|
+
return descriptor;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Detect base64-ish strings that are long enough to be heavy payloads.
|
|
310
|
+
* @param {string} value
|
|
311
|
+
*/
|
|
312
|
+
function isLikelyBase64(value) {
|
|
313
|
+
if (value.length < 512) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (value.length % 4 !== 0) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (!/^[A-Za-z0-9+/=\s]+$/.test(value)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const paddingRatio = (value.match(/=/g) || []).length / value.length;
|
|
323
|
+
return paddingRatio < 0.1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate placeholder string.
|
|
328
|
+
*/
|
|
329
|
+
function createPlaceholder(type, detail) {
|
|
330
|
+
return `[${type} withheld: ${detail}]`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Detect broad value type to help the editor render icons.
|
|
335
|
+
* @param {*} value
|
|
336
|
+
*/
|
|
337
|
+
function detectValueType(value) {
|
|
338
|
+
if (value === null) {
|
|
339
|
+
return "null";
|
|
340
|
+
}
|
|
341
|
+
if (value && typeof value === "object" && value.type === "Buffer" && Array.isArray(value.data)) {
|
|
342
|
+
return "buffer";
|
|
343
|
+
}
|
|
344
|
+
const t = typeof value;
|
|
345
|
+
if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
|
|
346
|
+
return t;
|
|
347
|
+
}
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
return "array";
|
|
350
|
+
}
|
|
351
|
+
if (value instanceof Date) {
|
|
352
|
+
return "date";
|
|
353
|
+
}
|
|
354
|
+
return "object";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Format for console/log output.
|
|
359
|
+
* @param {*} value
|
|
360
|
+
*/
|
|
361
|
+
function formatForConsole(value) {
|
|
362
|
+
return util.inspect(value, {
|
|
363
|
+
depth: 5,
|
|
364
|
+
maxArrayLength: 10,
|
|
365
|
+
breakLength: 120,
|
|
366
|
+
maxStringLength: DEFAULT_DEBUG_MAX_LENGTH
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
RED.nodes.registerType("rp-clean-debug", CleanDebugNode);
|
|
371
|
+
|
|
372
|
+
RED.httpAdmin.post("/rp-clean-debug/:id", RED.auth.needsPermission("flows.write"), function(req, res) {
|
|
373
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
374
|
+
if (!node) {
|
|
375
|
+
res.sendStatus(404);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (typeof node.active === "undefined") {
|
|
380
|
+
node.active = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let desiredState;
|
|
384
|
+
if (req.body && Object.prototype.hasOwnProperty.call(req.body, "active")) {
|
|
385
|
+
desiredState = !!req.body.active;
|
|
386
|
+
} else {
|
|
387
|
+
desiredState = !node.active;
|
|
388
|
+
}
|
|
389
|
+
node.active = desiredState;
|
|
390
|
+
|
|
391
|
+
res.json({ active: node.active });
|
|
392
|
+
});
|
|
393
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rosepetal/node-red-contrib-utils",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Utility and I/O nodes for Node-RED, including array helpers and file saving.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rosepetal-ai/node-red-contrib-utils.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"node-red",
|
|
14
|
+
"utilities",
|
|
15
|
+
"array",
|
|
16
|
+
"file",
|
|
17
|
+
"image",
|
|
18
|
+
"io"
|
|
19
|
+
],
|
|
20
|
+
"author": "Rosepetal SL (https://www.rosepetal.ai)",
|
|
21
|
+
"contributors": [
|
|
22
|
+
{
|
|
23
|
+
"name": "Nil Allue",
|
|
24
|
+
"email": "nil.allue@rosepetal.ai"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Víctor González Bentué",
|
|
28
|
+
"email": "victor.gonzalez@rosepetal.ai"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"license": "Apache-2.0",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/rosepetal-ai/node-red-contrib-utils/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/rosepetal-ai/node-red-contrib-utils#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=16.0"
|
|
38
|
+
},
|
|
39
|
+
"node-red": {
|
|
40
|
+
"version": ">=1.0.0",
|
|
41
|
+
"nodes": {
|
|
42
|
+
"rp-array-in": "nodes/io/array-in.js",
|
|
43
|
+
"rp-array-out": "nodes/io/array-out.js",
|
|
44
|
+
"rp-array-select": "nodes/io/array-select.js",
|
|
45
|
+
"rp-queue": "nodes/io/queue.js",
|
|
46
|
+
"rp-save-file": "nodes/io/save-file.js",
|
|
47
|
+
"rp-clean-debug": "nodes/util/clean-debug.js",
|
|
48
|
+
"rp-block-detect": "nodes/util/block-detect.js",
|
|
49
|
+
"rp-promise-reader": "nodes/promise-reader/promise-reader.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"sharp": "^0.34.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @rosepetal/node-red-contrib-utils
|
|
2
|
+
|
|
3
|
+
Utility and I/O nodes for Node-RED, focused on array assembly, queueing, safe debugging, promise aggregation, event loop monitoring, and file saving.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Installation (Palette Manager)
|
|
10
|
+
- Open Palette Manager -> Install -> search `@rosepetal/node-red-contrib-utils`.
|
|
11
|
+
|
|
12
|
+
### Installation (npm)
|
|
13
|
+
```bash
|
|
14
|
+
cd ~/.node-red
|
|
15
|
+
npm install @rosepetal/node-red-contrib-utils
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Node.js >= 16
|
|
21
|
+
- Node-RED >= 1.0.0
|
|
22
|
+
- `sharp` native addon (fetched via npm; builds from source if no prebuilt is available)
|
|
23
|
+
|
|
24
|
+
## Node Type Prefix
|
|
25
|
+
|
|
26
|
+
All node types are prefixed with `rp-` to avoid collisions with other packages. In flow JSON you will see types like `rp-array-in`. In the editor, nodes appear under the "RP Utils" category with human-friendly labels.
|
|
27
|
+
|
|
28
|
+
## Nodes
|
|
29
|
+
|
|
30
|
+
### I/O and Array Tools
|
|
31
|
+
|
|
32
|
+
| Node | Purpose |
|
|
33
|
+
|------|---------|
|
|
34
|
+
| **[rp-array-in](docs/nodes/io/array-in.md)** | Tag incoming data with a position index for ordered array assembly. |
|
|
35
|
+
| **[rp-array-out](docs/nodes/io/array-out.md)** | Collect indexed data into ordered arrays with timeout handling. |
|
|
36
|
+
| **[rp-array-select](docs/nodes/io/array-select.md)** | Select array elements using index and slice syntax. |
|
|
37
|
+
| **[rp-queue](docs/nodes/io/queue.md)** | Buffer messages, enforce output intervals, and drop stale or overflow items. |
|
|
38
|
+
| **[rp-save-file](docs/nodes/io/save-file.md)** | Save payloads to disk as image, JSON, text, or binary with auto-detection. |
|
|
39
|
+
|
|
40
|
+
### Async and Workflow
|
|
41
|
+
|
|
42
|
+
| Node | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| **[rp-promise-reader](docs/nodes/promise-reader/promise-reader.md)** | Resolve arrays of promises, merge results, and record timing. |
|
|
45
|
+
|
|
46
|
+
### Utilities
|
|
47
|
+
|
|
48
|
+
| Node | Purpose |
|
|
49
|
+
|------|---------|
|
|
50
|
+
| **[rp-clean-debug](docs/nodes/util/clean-debug.md)** | Debug output that sanitizes large payloads to keep the sidebar responsive. |
|
|
51
|
+
| **[rp-block-detect](docs/nodes/util/block-detect.md)** | Event loop delay watchdog with live status and log warnings. |
|
|
52
|
+
|
|
53
|
+
## Project Structure
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
node-red-contrib-utils/
|
|
57
|
+
├── docs/nodes/ # Node documentation
|
|
58
|
+
├── nodes/ # Node-RED node implementations
|
|
59
|
+
│ ├── io/ # Array, queue, save-file nodes
|
|
60
|
+
│ ├── util/ # clean-debug, block-detect
|
|
61
|
+
│ └── promise-reader/ # promise-reader node
|
|
62
|
+
├── lib/ # Shared utilities
|
|
63
|
+
└── assets/ # Documentation assets
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
Apache-2.0
|
|
69
|
+
|
|
70
|
+
## Author
|
|
71
|
+
|
|
72
|
+
Rosepetal SL - https://www.rosepetal.ai
|