@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,58 @@
1
+ # Clean Debug Node
2
+
3
+ ## Purpose & Use Cases
4
+
5
+ The `clean-debug` node is a drop-in replacement for the core Debug node that keeps the editor sidebar responsive by sanitizing heavy payloads (buffers, images, base64 strings) before display.
6
+
7
+ **Typical scenarios:**
8
+ - Inspecting image or binary flows without freezing the sidebar
9
+ - Logging large messages safely during development
10
+ - Keeping debug output readable in production flows
11
+
12
+ ## Input/Output Specification
13
+
14
+ ### Inputs
15
+ - **Incoming Message**: Any Node-RED message.
16
+
17
+ ### Outputs
18
+ - **No message output**: The node publishes to the debug sidebar and/or console only.
19
+
20
+ ## Configuration Options
21
+
22
+ ### Output
23
+ - **Type**: Typed input (msg/flow/global/env/jsonata/etc.)
24
+ - **Default**: `msg.payload`
25
+ - **Purpose**: Selects the value to display. Choose the full message object if needed.
26
+
27
+ ### Send to debug sidebar
28
+ - **Type**: Boolean
29
+ - **Default**: Enabled
30
+ - **Purpose**: Shows the sanitized value in the editor sidebar.
31
+
32
+ ### Also log to console
33
+ - **Type**: Boolean
34
+ - **Default**: Disabled
35
+ - **Purpose**: Logs the value to the Node-RED runtime log.
36
+
37
+ ### Clean heavy payloads
38
+ - **Type**: Boolean
39
+ - **Default**: Enabled
40
+ - **Purpose**: Replaces large buffers, typed arrays, data URLs, and long base64 strings with short placeholders.
41
+
42
+ ### Enabled
43
+ - **Type**: Boolean (button toggle)
44
+ - **Purpose**: Temporarily disable the node without removing it from the flow.
45
+
46
+ ## Behavior Notes
47
+
48
+ - Cleaning only affects what is displayed; the original message is not modified.
49
+ - When cleaning is disabled, the node behaves like the standard Debug node.
50
+ - Large arrays/objects are truncated for safety; circular references are handled gracefully.
51
+
52
+ ## Example
53
+
54
+ ```
55
+ [Image-In] -> [clean-debug: Output=msg.payload, Clean=on]
56
+ ```
57
+
58
+ Use clean mode to inspect image payloads without dumping full binary data.
@@ -0,0 +1,429 @@
1
+ /**
2
+ * @file Contains generic helper functions for the Node-RED nodes.
3
+ * This module is pure JavaScript and has no dependency on the RED object.
4
+ * @author Rosepetal
5
+ */
6
+
7
+ const sharp = require('sharp'); // Ensure sharp is installed in your project
8
+
9
+
10
+ module.exports = function(RED) {
11
+ const utils = {};
12
+
13
+ // Configuration constants
14
+ const CONSTANTS = {
15
+ DEFAULT_JPEG_QUALITY: 90,
16
+ DEFAULT_ARRAY_POSITION: 0,
17
+ SUPPORTED_DTYPES: ['uint8'],
18
+ SUPPORTED_COLOR_SPACES: ['GRAY', 'RGB', 'RGBA', 'BGR', 'BGRA'],
19
+ CHANNEL_MAP: { 'GRAY': 1, 'RGB': 3, 'RGBA': 4, 'BGR': 3, 'BGRA': 4 }
20
+ };
21
+
22
+ /**
23
+ * Validates and normalizes an image structure
24
+ * Supports structure: {data, width, height, channels, colorSpace, dtype}
25
+ */
26
+ utils.validateImageStructure = function(image, node) {
27
+ if (!image) {
28
+ node.warn("Input is null or undefined");
29
+ return null;
30
+ }
31
+
32
+ switch (true) {
33
+ // New Rosepetal bitmap structure
34
+ case (image.hasOwnProperty('width') &&
35
+ image.hasOwnProperty('height') &&
36
+ image.hasOwnProperty('data')):
37
+
38
+ // Validate dtype (only uint8 supported for now)
39
+ if (image.dtype && !CONSTANTS.SUPPORTED_DTYPES.includes(image.dtype)) {
40
+ node.warn(`Unsupported dtype: ${image.dtype}. Supported values: ${CONSTANTS.SUPPORTED_DTYPES.join(', ')}`);
41
+ return null;
42
+ }
43
+
44
+ // Infer channels if not provided
45
+ let channels = image.channels;
46
+ if (!channels) {
47
+ const calculatedChannels = image.data.length / (image.width * image.height);
48
+ if (!Number.isInteger(calculatedChannels)) {
49
+ node.warn(`Cannot infer channels: data.length (${image.data.length}) is not divisible by width*height (${image.width * image.height})`);
50
+ return null;
51
+ }
52
+ channels = calculatedChannels;
53
+ }
54
+
55
+ // Validate channels is a number
56
+ if (typeof channels !== 'number') {
57
+ node.warn(`Channels must be a number, got: ${typeof channels}`);
58
+ return null;
59
+ }
60
+
61
+ // Validate data length matches dimensions
62
+ if (image.width * image.height * channels !== image.data.length) {
63
+ node.warn(`Data length mismatch: expected ${image.width * image.height * channels} bytes (${image.width}x${image.height}x${channels}), got ${image.data.length} bytes`);
64
+ return null;
65
+ }
66
+
67
+ // Handle colorSpace with defaults
68
+ let colorSpace = image.colorSpace;
69
+ if (!colorSpace) {
70
+ // Default based on channel count
71
+ switch (channels) {
72
+ case 1: colorSpace = "GRAY"; break;
73
+ case 3: colorSpace = "RGB"; break;
74
+ case 4: colorSpace = "RGBA"; break;
75
+ default:
76
+ node.warn(`Cannot determine default colorSpace for ${channels} channels`);
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // Validate colorSpace matches channel count
82
+ if (!CONSTANTS.CHANNEL_MAP.hasOwnProperty(colorSpace)) {
83
+ node.warn(`Unsupported colorSpace: ${colorSpace}. Supported values: ${CONSTANTS.SUPPORTED_COLOR_SPACES.join(', ')}`);
84
+ return null;
85
+ }
86
+
87
+ if (CONSTANTS.CHANNEL_MAP[colorSpace] !== channels) {
88
+ node.warn(`ColorSpace mismatch: ${colorSpace} expects ${CONSTANTS.CHANNEL_MAP[colorSpace]} channels, got ${channels} channels`);
89
+ return null;
90
+ }
91
+
92
+ // Return normalized structure
93
+ return {
94
+ data: image.data,
95
+ width: image.width,
96
+ height: image.height,
97
+ channels: channels,
98
+ colorSpace: colorSpace,
99
+ dtype: image.dtype || 'uint8'
100
+ };
101
+
102
+ default:
103
+ // Not our format? Pass to C++ and let OpenCV handle it
104
+ return image;
105
+ }
106
+ }
107
+
108
+ utils.validateSingleImage = function(image, node) {
109
+ const normalized = utils.validateImageStructure(image, node);
110
+ return normalized !== null;
111
+ }
112
+
113
+ utils.validateListImage = function(list, node) {
114
+ if (!list || !Array.isArray(list) || list.length === 0) {
115
+ node.warn("Input is not a valid image list. Expected a non-empty Array.");
116
+ return false;
117
+ }
118
+ // Validate each item in the list
119
+ for (const image of list) {
120
+ if (!utils.validateSingleImage(image, node)) {
121
+ // The warning message is already sent by validateSingleImage
122
+ return false;
123
+ }
124
+ }
125
+ return true;
126
+ }
127
+
128
+ utils.resolveDimension = function(node, type, value, msg) {
129
+ if (!value || String(value).trim() === '') return null;
130
+ let resolvedValue;
131
+ if (type === 'msg' || type === 'flow' || type === 'global') {
132
+ resolvedValue = RED.util.evaluateNodeProperty(value, type, node, msg);
133
+ } else {
134
+ resolvedValue = value;
135
+ }
136
+ const numericValue = parseFloat(resolvedValue);
137
+ if (numericValue === undefined || isNaN(numericValue)) {
138
+ throw new Error(`Value "${resolvedValue}" from property "${value}" is not a valid number.`);
139
+ }
140
+ return numericValue;
141
+ }
142
+
143
+ /**
144
+ * Resolves array position value from various sources (msg, flow, global, or direct value)
145
+ * @param {Object} node - The Node-RED node instance
146
+ * @param {string} type - The type of source ('msg', 'flow', 'global', or 'num')
147
+ * @param {*} value - The value or property path to resolve
148
+ * @param {Object} msg - The message object for msg property resolution
149
+ * @returns {number} The resolved array position as a non-negative integer
150
+ * @throws {Error} If the resolved value is not a valid non-negative integer
151
+ */
152
+ utils.resolveArrayPosition = function(node, type, value, msg) {
153
+ if (value === null || value === undefined || String(value).trim() === '') {
154
+ return CONSTANTS.DEFAULT_ARRAY_POSITION;
155
+ }
156
+
157
+ let resolvedValue;
158
+ if (type === 'msg' || type === 'flow' || type === 'global') {
159
+ resolvedValue = RED.util.evaluateNodeProperty(value, type, node, msg);
160
+ } else {
161
+ resolvedValue = value;
162
+ }
163
+
164
+ const numericValue = parseInt(resolvedValue);
165
+ if (numericValue === undefined || isNaN(numericValue) || numericValue < 0) {
166
+ throw new Error(`Array position "${resolvedValue}" from property "${value}" must be a non-negative integer.`);
167
+ }
168
+
169
+ return numericValue;
170
+ }
171
+
172
+ utils.rawToJpeg = async function (image, quality = CONSTANTS.DEFAULT_JPEG_QUALITY) {
173
+ const normalized = utils.validateImageStructure(image, { warn: () => {} });
174
+ if (!normalized)
175
+ throw new Error('Invalid raw image object supplied to rawToJpeg');
176
+
177
+ const colorSpace = normalized.colorSpace;
178
+ const channels = normalized.channels;
179
+ let data = normalized.data;
180
+
181
+ // BGR/BGRA → RGB/RGBA for Sharp
182
+ if (colorSpace === 'BGR' || colorSpace === 'BGRA') {
183
+ data = Buffer.from(data); // copy so we don't mutate shared memory
184
+ for (let i = 0; i < data.length; i += channels) {
185
+ const t = data[i];
186
+ data[i] = data[i + 2];
187
+ data[i + 2] = t;
188
+ }
189
+ }
190
+
191
+ const sh = sharp(data, {
192
+ raw: { width: normalized.width, height: normalized.height, channels }
193
+ });
194
+
195
+ if (colorSpace === 'GRAY') sh.toColourspace('b-w');
196
+
197
+ return sh.jpeg({ quality }).toBuffer();
198
+ }
199
+
200
+ /**
201
+ * Standardized error handling for all nodes
202
+ * @param {object} node - Node-RED node instance
203
+ * @param {Error} error - The error that occurred
204
+ * @param {object} msg - The message object
205
+ * @param {function} done - The done callback
206
+ * @param {string} operation - Optional operation name for context
207
+ */
208
+ utils.handleNodeError = function(node, error, msg, done, operation = 'processing') {
209
+ const errorMessage = error.message || `Unknown error during ${operation}.`;
210
+ node.error(errorMessage, msg);
211
+ node.status({ fill: "red", shape: "ring", text: "Error" });
212
+ if (done) {
213
+ done(error);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Standardized success status formatting
219
+ * @param {object} node - Node-RED node instance
220
+ * @param {number} count - Number of items processed
221
+ * @param {number} totalTime - Total processing time in ms
222
+ * @param {object} timing - Timing breakdown object
223
+ */
224
+ utils.setSuccessStatus = function(node, count, totalTime, timing = {}) {
225
+ const { convertMs = 0, taskMs = 0, encodeMs = 0 } = timing;
226
+ node.status({
227
+ fill: 'green',
228
+ shape: 'dot',
229
+ text: `OK: ${count} img in ${totalTime.toFixed(2)} ms ` +
230
+ `(conv ${(convertMs + encodeMs).toFixed(2)} ms | ` +
231
+ `task ${taskMs.toFixed(2)} ms)`
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Debug image display utility for inline node debugging
237
+ * Converts images to displayable format and returns data URL with format message
238
+ * @param {object|Buffer} image - Image data (raw image object or encoded buffer)
239
+ * @param {string} outputFormat - The output format selected ('raw', 'jpg', 'png', 'webp')
240
+ * @param {number} quality - JPEG quality for raw conversion (default: 90)
241
+ * @param {object} node - Node-RED node instance for error reporting
242
+ * @param {boolean} debugEnabled - Whether debugging is enabled
243
+ * @param {number} debugWidth - Desired width for debug image display (default: 200)
244
+ * @returns {Promise<object|null>} Debug result object with dataUrl and formatMessage, or null if disabled
245
+ */
246
+ utils.debugImageDisplay = async function(image, outputFormat, quality, node, debugEnabled, debugWidth) {
247
+ if (!debugEnabled) return null;
248
+
249
+ // Validate and set default debug width
250
+ debugWidth = Math.max(1, parseInt(debugWidth) || 200);
251
+
252
+ try {
253
+ let imageBuffer, formatMessage;
254
+
255
+ if (outputFormat === 'raw') {
256
+ // Convert raw image to JPG for display using existing utility
257
+ imageBuffer = await utils.rawToJpeg(image, quality || CONSTANTS.DEFAULT_JPEG_QUALITY);
258
+ formatMessage = 'jpg default';
259
+ } else {
260
+ // Reuse existing converted buffer
261
+ imageBuffer = Buffer.isBuffer(image) ? image : image.data;
262
+ formatMessage = outputFormat; // 'jpg', 'png', 'webp'
263
+ }
264
+
265
+ if (!Buffer.isBuffer(imageBuffer)) {
266
+ node.warn('Debug display: Invalid image buffer format');
267
+ return null;
268
+ }
269
+
270
+ // Resize image for debug display using Sharp
271
+ let actualWidth = debugWidth;
272
+ let actualHeight = debugWidth; // Default fallback
273
+ try {
274
+ let sharpInstance = sharp(imageBuffer)
275
+ .resize(debugWidth, null, {
276
+ withoutEnlargement: false,
277
+ fit: 'inside'
278
+ });
279
+
280
+ // Only convert format if output is raw, otherwise preserve the existing format
281
+ if (outputFormat === 'raw') {
282
+ // For raw format, convert to JPEG for display
283
+ sharpInstance = sharpInstance.jpeg({ quality: quality || 90 });
284
+ formatMessage = 'jpg default';
285
+ } else {
286
+ // For other formats (jpg, png, webp), just resize without format conversion
287
+ // The C++ backend has already converted to the desired format
288
+ formatMessage = outputFormat + ' resized';
289
+ }
290
+
291
+ imageBuffer = await sharpInstance.toBuffer();
292
+
293
+ // Get actual dimensions of resized image
294
+ const metadata = await sharp(imageBuffer).metadata();
295
+ actualWidth = metadata.width || debugWidth;
296
+ actualHeight = metadata.height || debugWidth;
297
+
298
+ } catch (resizeError) {
299
+ node.warn(`Debug image resize error: ${resizeError.message}`);
300
+ // Continue with original image if resize fails
301
+ // Try to get original dimensions as fallback
302
+ try {
303
+ const originalMetadata = await sharp(imageBuffer).metadata();
304
+ actualWidth = originalMetadata.width || debugWidth;
305
+ actualHeight = originalMetadata.height || debugWidth;
306
+ } catch (metadataError) {
307
+ // Use debug width as fallback
308
+ actualWidth = debugWidth;
309
+ actualHeight = debugWidth;
310
+ }
311
+ }
312
+
313
+ // Convert to base64 for WebSocket transmission
314
+ const base64 = imageBuffer.toString('base64');
315
+ const mimeType = formatMessage.replace(' default', '').replace(' resized', '');
316
+ const dataUrl = `data:image/${mimeType};base64,${base64}`;
317
+
318
+ // Send image to frontend via WebSocket for inline display
319
+ try {
320
+ RED.comms.publish("debug-image", {
321
+ id: node.id,
322
+ data: base64,
323
+ format: formatMessage,
324
+ mimeType: mimeType,
325
+ size: imageBuffer.length,
326
+ debugWidth: actualWidth,
327
+ debugHeight: actualHeight
328
+ });
329
+ } catch (wsError) {
330
+ node.warn(`Debug WebSocket error: ${wsError.message}`);
331
+ // Continue anyway - status display will still work
332
+ }
333
+
334
+ return {
335
+ dataUrl: dataUrl,
336
+ formatMessage: formatMessage,
337
+ size: imageBuffer.length
338
+ };
339
+
340
+ } catch (error) {
341
+ node.warn(`Debug display error: ${error.message}`);
342
+ return null;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Enhanced success status formatting with debug information
348
+ * @param {object} node - Node-RED node instance
349
+ * @param {number} count - Number of items processed
350
+ * @param {number} totalTime - Total processing time in ms
351
+ * @param {object} timing - Timing breakdown object
352
+ * @param {string} debugFormat - Optional debug format message to include in status
353
+ */
354
+ utils.setSuccessStatusWithDebug = function(node, count, totalTime, timing = {}, debugFormat = null) {
355
+ const { convertMs = 0, taskMs = 0, encodeMs = 0 } = timing;
356
+ let statusText = `OK: ${count} img in ${totalTime.toFixed(2)} ms ` +
357
+ `(conv ${(convertMs + encodeMs).toFixed(2)} ms | ` +
358
+ `task ${taskMs.toFixed(2)} ms)`;
359
+
360
+ if (debugFormat) {
361
+ statusText += ` | ${debugFormat}`;
362
+ }
363
+
364
+ node.status({
365
+ fill: 'green',
366
+ shape: 'dot',
367
+ text: statusText
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Normalizes the performance metric key based on the node's display name.
373
+ * Falls back to the node type when the custom name is missing.
374
+ * @param {object} node - Node-RED node instance
375
+ * @returns {string} sanitized key
376
+ */
377
+ utils.getPerformanceKey = function(node) {
378
+ const base = (node && node.name && String(node.name).trim()) ||
379
+ (node && node.type) ||
380
+ 'node';
381
+ return String(base)
382
+ .trim()
383
+ .toLowerCase()
384
+ .replace(/[^a-z0-9]+/g, '_')
385
+ .replace(/^_+|_+$/g, '') || 'node';
386
+ }
387
+
388
+ /**
389
+ * Records performance metrics on the outgoing message under
390
+ * msg.performance.rpimage.{nodeNameKey}.
391
+ * @param {object} node - Node-RED node instance
392
+ * @param {object} msg - The message object being emitted
393
+ * @param {object} timings - Timing breakdown { convertMs, encodeMs, taskMs, conversion, task }
394
+ * @param {number|null} totalTime - Total processing time in ms
395
+ */
396
+ utils.recordPerformanceMetrics = function(node, msg, timings = {}, totalTime = null) {
397
+ if (!msg || !node) {
398
+ return;
399
+ }
400
+
401
+ const key = utils.getPerformanceKey(node);
402
+ const path = `performance.rpimage.${key}`;
403
+
404
+ const convertMs = typeof timings.conversion === 'number'
405
+ ? timings.conversion
406
+ : (typeof timings.convertMs === 'number' ? timings.convertMs : 0) +
407
+ (typeof timings.encodeMs === 'number' ? timings.encodeMs : 0);
408
+ const taskMs = typeof timings.task === 'number'
409
+ ? timings.task
410
+ : (typeof timings.taskMs === 'number' ? timings.taskMs : null);
411
+ const totalMs = typeof totalTime === 'number'
412
+ ? totalTime
413
+ : (typeof timings.total === 'number' ? timings.total : null);
414
+
415
+ const payload = {
416
+ conversion: Number.isFinite(convertMs) ? convertMs : null,
417
+ task: Number.isFinite(taskMs) ? taskMs : null,
418
+ total: Number.isFinite(totalMs) ? totalMs : null
419
+ };
420
+
421
+ try {
422
+ RED.util.setMessageProperty(msg, path, payload, true);
423
+ } catch (err) {
424
+ node.warn(`Failed to record performance metrics: ${err.message}`);
425
+ }
426
+ }
427
+
428
+ return utils;
429
+ }
@@ -0,0 +1,110 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('rp-array-in', {
3
+ category: 'RP Utils',
4
+ color: '#87CEEB',
5
+ defaults: {
6
+ name: { value: "" },
7
+ inputPath: { value: "payload" },
8
+ inputPathType: { value: "msg" },
9
+ arrayPositionType: { value: "num" },
10
+ arrayPositionValue: { value: 0 }
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: "font-awesome/fa-sign-in",
15
+ label: function() {
16
+ if (this.name) return this.name;
17
+
18
+ const posDisplay = this.arrayPositionType === 'num'
19
+ ? this.arrayPositionValue
20
+ : `${this.arrayPositionType}.${this.arrayPositionValue}`;
21
+ return `Array In [${posDisplay}]`;
22
+ },
23
+ oneditprepare: function() {
24
+ // Set up TypedInput for input path
25
+ $("#node-input-inputPath").typedInput({
26
+ default: 'msg',
27
+ types: ['msg', 'flow', 'global'],
28
+ typeField: "#node-input-inputPathType"
29
+ });
30
+
31
+ // Set up TypedInput for array position
32
+ $("#node-input-arrayPositionValue").typedInput({
33
+ default: 'num',
34
+ types: ['num', 'msg', 'flow', 'global'],
35
+ typeField: "#node-input-arrayPositionType"
36
+ });
37
+ }
38
+ });
39
+ </script>
40
+
41
+ <script type="text/x-red" data-template-name="rp-array-in">
42
+ <div class="form-row">
43
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
44
+ <input type="text" id="node-input-name" placeholder="Name">
45
+ </div>
46
+
47
+ <hr>
48
+
49
+ <div class="form-row">
50
+ <label for="node-input-inputPath"><i class="fa fa-sign-in"></i> Input from</label>
51
+ <input type="text" id="node-input-inputPath" style="width: 70%;">
52
+ <input type="hidden" id="node-input-inputPathType">
53
+ </div>
54
+
55
+ <div class="form-row">
56
+ <label for="node-input-arrayPositionValue"><i class="fa fa-sort-numeric-asc"></i> Array Position</label>
57
+ <input type="text" id="node-input-arrayPositionValue" style="width: 70%;" placeholder="0">
58
+ <input type="hidden" id="node-input-arrayPositionType">
59
+ </div>
60
+
61
+ <div class="form-tips">
62
+ <b>Tip:</b> Array position can be a fixed number (starting from 0) or dynamically resolved from msg, flow, or global context. This determines where this data will be placed in the output array.
63
+ </div>
64
+ </script>
65
+
66
+ <script type="text/x-red" data-help-name="rp-array-in">
67
+ <p>Collects data from a specified input path and tags it for ordered array assembly with a position index.</p>
68
+
69
+ <h3>Details</h3>
70
+ <p>This node is designed to work with the rp-array-out node to create ordered arrays from multiple data sources. It extracts data from the specified input path and tags it with a position index, allowing multiple rp-array-in nodes to feed into a single rp-array-out node for ordered collection.</p>
71
+
72
+ <h3>Properties</h3>
73
+ <dl class="message-properties">
74
+ <dt>Input from <span class="property-type">string</span></dt>
75
+ <dd>The location to read data from. You can select between <code>msg</code>, <code>flow</code>, or <code>global</code> context. Defaults to <code>msg.payload</code>.</dd>
76
+ <dt>Array Position <span class="property-type">number | msg | flow | global</span></dt>
77
+ <dd>The index position (0, 1, 2...) where this data should be placed in the final array. Can be a fixed number or dynamically resolved from message properties, flow context, or global context. Must resolve to a non-negative integer.</dd>
78
+ </dl>
79
+
80
+ <h3>Inputs</h3>
81
+ <dl class="message-properties">
82
+ <dt>payload <span class="property-type">any</span></dt>
83
+ <dd>Input message containing the data to be extracted. The actual data location is determined by the "Input from" property. The payload is only replaced when the input is set to <code>msg.payload</code>; otherwise it is left untouched.</dd>
84
+ </dl>
85
+
86
+ <h3>Outputs</h3>
87
+ <p>The node passes through the original message with added metadata for array assembly. When the input path is <code>msg.payload</code> the payload is mirrored; for all other inputs the existing payload is preserved and only the metadata is updated.</p>
88
+ <dl class="message-properties">
89
+ <dt>meta.arrayPosition <span class="property-type">number</span></dt>
90
+ <dd>The configured array position index.</dd>
91
+ <dt>meta.arrayData <span class="property-type">any</span></dt>
92
+ <dd>The data extracted from the specified input path.</dd>
93
+ </dl>
94
+
95
+ <h3>Examples</h3>
96
+ <ul>
97
+ <li><strong>Basic Array Assembly:</strong> Three rp-array-in nodes with positions 0, 1, 2 feeding into an rp-array-out will create: <code>[data0, data1, data2]</code></li>
98
+ <li><strong>Dynamic Positioning:</strong> Set array position from <code>msg.index</code> to dynamically determine where data should be placed based on message content</li>
99
+ <li><strong>Image Collection:</strong> Multiple image-in nodes → rp-array-in nodes (positions 0, 1, 2...) → rp-array-out → transform nodes that support arrays</li>
100
+ <li><strong>Mixed Data Sources:</strong> Combine data from different context levels (msg, flow, global) into a single ordered array</li>
101
+ <li><strong>Conditional Assembly:</strong> Use flow variables to set positions based on processing conditions or user preferences</li>
102
+ </ul>
103
+
104
+ <h3>Node Interactions</h3>
105
+ <p><strong>Works with:</strong> Must be used with rp-array-out node for array assembly. Can receive data from any node that outputs to the specified input path.</p>
106
+ <p><strong>Best Practice:</strong> Use sequential positions starting from 0, ensure each rp-array-in node connected to the same rp-array-out has a unique position.</p>
107
+
108
+ <h3>Error Handling</h3>
109
+ <p>If the input path contains no data or is invalid, the node will set <code>meta.arrayData</code> to <code>null</code> and display a warning.</p>
110
+ </script>