@rosepetal/node-red-contrib-utils 1.1.1 → 1.1.3

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.
@@ -16,9 +16,14 @@ The `queue` node buffers incoming messages, enforces a minimum interval between
16
16
  - **Incoming Message**: Any Node-RED message to be enqueued.
17
17
 
18
18
  ### Outputs
19
- - **Queued Message**: Messages are forwarded in FIFO order while respecting the configured interval.
20
-
21
- When timeout mode is selected, messages that exceed the configured timeout are silently discarded with a warning in the node's log.
19
+ - **Output 1**: Messages forwarded in FIFO order while respecting the configured interval.
20
+ - **Output 2**: Messages dropped due to timeout or overflow, with metadata:
21
+ - `msg.meta.queueDropped` - Always `true`
22
+ - `msg.meta.queueDropReason` - Either `"timeout"` or `"overflow"`
23
+ - `msg.meta.queuedDuration` - Time spent in queue before timeout (timeout drops only)
24
+ - `msg.meta.queueTimeout` - Configured timeout value in ms (timeout drops only)
25
+ - `msg.meta.queueSize` - Queue size when overflow occurred (overflow drops only)
26
+ - `msg.meta.queueMaxSize` - Configured max queue size (overflow drops only)
22
27
 
23
28
  ## Configuration Options
24
29
 
@@ -47,11 +52,13 @@ When timeout mode is selected, messages that exceed the configured timeout are s
47
52
  - Messages are processed in first-in-first-out order.
48
53
  - The node emits messages immediately when the interval requirement has already been satisfied.
49
54
  - If necessary, the node waits the remaining interval before releasing the next message.
50
- - When messages expire due to timeout (and timeout mode is active) they are removed from the queue and a warning is logged.
55
+ - When messages expire due to timeout (and timeout mode is active) they are sent to output 2 with metadata and a warning is logged.
56
+ - When the queue overflows (queue-size mode) excess messages are sent to output 2 with metadata.
51
57
  - Node status indicates whether the queue is idle, holding messages, or actively sending.
52
58
 
53
59
  ## Best Practices
54
60
 
55
61
  - Pair with Array nodes to control the rate of batch processing pipelines.
56
62
  - Use timeouts to keep sensor readings or camera frames current.
57
- - Combine with Catch or Status nodes to monitor when the queue reaches capacity or drops messages.
63
+ - Connect output 2 to logging, alerting, or fallback processing to handle dropped messages.
64
+ - Use `msg.meta.queueDropReason` to differentiate between timeout and overflow scenarios in downstream logic.
@@ -10,7 +10,8 @@
10
10
  timeout: { value: 0 }
11
11
  },
12
12
  inputs: 1,
13
- outputs: 1,
13
+ outputs: 2,
14
+ outputLabels: ["output", "dropped"],
14
15
  icon: "font-awesome/fa-clock-o",
15
16
  label: function() {
16
17
  if (this.name) return this.name;
@@ -106,11 +107,28 @@
106
107
  <dd>Minimum time to wait between forwarded messages. Zero means send immediately when data is available.</dd>
107
108
  </dl>
108
109
 
110
+ <h3>Outputs</h3>
111
+ <dl class="message-properties">
112
+ <dt>Output 1 <span class="property-type">message</span></dt>
113
+ <dd>Successfully queued messages released according to the interval setting.</dd>
114
+ <dt>Output 2 <span class="property-type">message</span></dt>
115
+ <dd>Messages dropped due to timeout expiration or queue overflow. Includes metadata:
116
+ <ul>
117
+ <li><code>msg.meta.queueDropped</code> - Always <code>true</code></li>
118
+ <li><code>msg.meta.queueDropReason</code> - Either <code>"timeout"</code> or <code>"overflow"</code></li>
119
+ <li><code>msg.meta.queuedDuration</code> - Time in queue before timeout (timeout only)</li>
120
+ <li><code>msg.meta.queueTimeout</code> - Configured timeout value (timeout only)</li>
121
+ <li><code>msg.meta.queueSize</code> - Current queue size (overflow only)</li>
122
+ <li><code>msg.meta.queueMaxSize</code> - Configured max size (overflow only)</li>
123
+ </ul>
124
+ </dd>
125
+ </dl>
126
+
109
127
  <h3>Behavior</h3>
110
128
  <ul>
111
129
  <li>Messages are processed in FIFO order while respecting the selected mode.</li>
112
- <li>Queue-size mode enforces a hard cap, rejecting excess messages.</li>
113
- <li>Timeout mode keeps buffering but evicts stale entries before sending.</li>
130
+ <li>Queue-size mode enforces a hard cap; excess messages go to output 2 with <code>overflow</code> reason.</li>
131
+ <li>Timeout mode keeps buffering but evicts stale entries to output 2 with <code>timeout</code> reason.</li>
114
132
  <li>The interval ensures a steady pace by adding delays between sends when configured.</li>
115
133
  </ul>
116
134
 
package/nodes/io/queue.js CHANGED
@@ -68,23 +68,32 @@ module.exports = function (RED) {
68
68
  }
69
69
 
70
70
  const now = Date.now();
71
- let dropped = 0;
71
+ const expiredEntries = [];
72
72
 
73
73
  while (state.queue.length > 0) {
74
74
  const oldest = state.queue[0];
75
75
  if (now - oldest.enqueuedAt >= node.timeoutMs) {
76
- state.queue.shift();
77
- dropped += 1;
76
+ expiredEntries.push(state.queue.shift());
78
77
  } else {
79
78
  break;
80
79
  }
81
80
  }
82
81
 
83
- if (dropped > 0) {
84
- node.warn(`Queue node dropped ${dropped} message(s) due to timeout.`);
82
+ if (expiredEntries.length > 0) {
83
+ node.warn(`Queue node dropped ${expiredEntries.length} message(s) due to timeout.`);
84
+ // Send expired messages to output 2 with metadata
85
+ for (const entry of expiredEntries) {
86
+ const droppedMsg = entry.msg;
87
+ droppedMsg.meta = droppedMsg.meta || {};
88
+ droppedMsg.meta.queueDropped = true;
89
+ droppedMsg.meta.queueDropReason = 'timeout';
90
+ droppedMsg.meta.queuedDuration = now - entry.enqueuedAt;
91
+ droppedMsg.meta.queueTimeout = node.timeoutMs;
92
+ node.send([null, droppedMsg]);
93
+ }
85
94
  }
86
95
 
87
- return dropped;
96
+ return expiredEntries.length;
88
97
  }
89
98
 
90
99
  function scheduleNextSend() {
@@ -144,7 +153,7 @@ module.exports = function (RED) {
144
153
  text: `Sending (remaining ${state.queue.length})`
145
154
  });
146
155
 
147
- node.send(entry.msg);
156
+ node.send([entry.msg, null]);
148
157
  scheduleNextSend();
149
158
  }
150
159
 
@@ -161,8 +170,15 @@ module.exports = function (RED) {
161
170
  state.queue.length >= node.maxQueueSize
162
171
  ) {
163
172
  node.warn(
164
- `Queue node at capacity (${node.maxQueueSize}). Incoming message ignored.`
173
+ `Queue node at capacity (${node.maxQueueSize}). Incoming message dropped.`
165
174
  );
175
+ // Send overflow message to output 2 with metadata
176
+ msg.meta = msg.meta || {};
177
+ msg.meta.queueDropped = true;
178
+ msg.meta.queueDropReason = 'overflow';
179
+ msg.meta.queueSize = state.queue.length;
180
+ msg.meta.queueMaxSize = node.maxQueueSize;
181
+ node.send([null, msg]);
166
182
  setStatusQueued();
167
183
  return done?.();
168
184
  }
@@ -102,8 +102,7 @@ module.exports = function registerCleanDebugNode(RED) {
102
102
  return;
103
103
  }
104
104
 
105
- const format = detectValueType(value);
106
- const debugMessage = {
105
+ var debugMessage = {
107
106
  id: node.id,
108
107
  z: node.z,
109
108
  _alias: node._alias,
@@ -112,13 +111,45 @@ module.exports = function registerCleanDebugNode(RED) {
112
111
  property: node.targetType === "full" ? "msg" : node.complete,
113
112
  propertyType: node.targetType,
114
113
  clean: node.clean,
115
- format,
116
114
  msg: value,
117
115
  _msgid: msg && msg._msgid
118
116
  };
119
117
 
118
+ encodeDebugValue(debugMessage);
119
+
120
120
  RED.comms.publish("debug", debugMessage);
121
121
  }
122
+
123
+ /**
124
+ * Encode debugMsg.msg and set debugMsg.format in-place so the value
125
+ * matches the format expected by Node-RED's debug-utils.js in the editor.
126
+ * RED.util.encodeObject expects the whole debug message object — it reads
127
+ * from debugMsg.msg and sets debugMsg.msg + debugMsg.format in-place.
128
+ * @param {object} debugMsg
129
+ */
130
+ function encodeDebugValue(debugMsg) {
131
+ var value = debugMsg.msg;
132
+ if (value === null || typeof value === "undefined") {
133
+ debugMsg.format = (value === null) ? "null" : "undefined";
134
+ debugMsg.msg = (value === null) ? "null" : "(undefined)";
135
+ } else if (typeof value === "object") {
136
+ try {
137
+ RED.util.encodeObject(debugMsg, { maxLength: DEFAULT_DEBUG_MAX_LENGTH });
138
+ } catch (_e) {
139
+ debugMsg.format = "error";
140
+ debugMsg.msg = "Failed to encode debug value";
141
+ }
142
+ } else if (typeof value === "boolean") {
143
+ debugMsg.format = "boolean";
144
+ debugMsg.msg = value.toString();
145
+ } else if (typeof value === "number") {
146
+ debugMsg.format = "number";
147
+ debugMsg.msg = value.toString();
148
+ } else {
149
+ debugMsg.format = "string[" + ("" + value).length + "]";
150
+ debugMsg.msg = "" + value;
151
+ }
152
+ }
122
153
  }
123
154
 
124
155
  /**
@@ -330,30 +361,6 @@ module.exports = function registerCleanDebugNode(RED) {
330
361
  return `[${type} withheld: ${detail}]`;
331
362
  }
332
363
 
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
364
  /**
358
365
  * Format for console/log output.
359
366
  * @param {*} value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rosepetal/node-red-contrib-utils",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Utility and I/O nodes for Node-RED, including array helpers and file saving.",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"