@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.
@@ -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
+ ![Array-Out Demo](assets/nodes/io/array-out-demo.gif)
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