@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.
- package/docs/nodes/io/queue.md +12 -5
- package/nodes/io/queue.html +21 -3
- package/nodes/io/queue.js +24 -8
- package/nodes/util/clean-debug.js +34 -27
- package/package.json +1 -1
package/docs/nodes/io/queue.md
CHANGED
|
@@ -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
|
-
- **
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
-
|
|
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.
|
package/nodes/io/queue.html
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
timeout: { value: 0 }
|
|
11
11
|
},
|
|
12
12
|
inputs: 1,
|
|
13
|
-
outputs:
|
|
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
|
|
113
|
-
<li>Timeout mode keeps buffering but evicts stale entries
|
|
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
|
-
|
|
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 (
|
|
84
|
-
node.warn(`Queue node dropped ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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