@yroshcha/node-red-contrib-redis-full 1.0.7 → 2.0.0
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/CHANGELOG.md +23 -0
- package/README.md +4 -1
- package/examples/redis-streams.json +163 -0
- package/package.json +6 -5
- package/redis.js +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 2.0.0 — 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- The minimum supported Node.js version is now 20 and the minimum supported Node-RED version is now 4. This aligns the package with `ioredis` 6.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated `ioredis` to the current 6.x release line.
|
|
14
|
+
- Added an importable Redis Streams example flow (`XADD → XREADGROUP → XACK`) for the Node-RED library.
|
|
15
|
+
|
|
16
|
+
## 1.0.9 — 2026-09-07
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Declared the supported Node-RED runtime range in the package metadata so the Flow Library scorecard and Palette Manager can evaluate compatibility correctly.
|
|
21
|
+
|
|
22
|
+
## 1.0.8 — 2026-08-12
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `redis xreadgroup` and `redis xautoclaim` now restore nested object and array fields that were serialised by `redis xadd`, while preserving scalar Redis fields as strings.
|
|
27
|
+
|
|
5
28
|
## 1.0.7 — 2026-08-12
|
|
6
29
|
|
|
7
30
|
### Added
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ For a local archive, use `npm install /path/to/yroshcha-node-red-contrib-redis-f
|
|
|
45
45
|
|
|
46
46
|
## Release status
|
|
47
47
|
|
|
48
|
-
**`
|
|
48
|
+
**`2.0.0` is the current stable release.** See [CHANGELOG.md](CHANGELOG.md) for release notes and compatibility information.
|
|
49
49
|
|
|
50
50
|
## Production profile
|
|
51
51
|
|
|
@@ -58,6 +58,7 @@ Safe consumer defaults and important settings:
|
|
|
58
58
|
- **Rate limit (msg/s) = 0** is unlimited. For example, `500` limits average downstream delivery to 500 events per second.
|
|
59
59
|
- **Read interval (ms) = 0** reads as fast as Redis responds. `1000` calls `XREADGROUP` no more often than once per second, independent of queue depth.
|
|
60
60
|
- **Batch wait (ms) = 0** emits one event at a time. With a positive value, the node emits an array once it has collected `COUNT` events or the wait expires. `msg.payload` and `msg.streamId` become arrays.
|
|
61
|
+
- Nested object and array fields written by `redis xadd` are automatically decoded from JSON by `redis xreadgroup` and `redis xautoclaim`; scalar Redis fields remain strings.
|
|
61
62
|
- **Batch interval (ms)** sets the minimum delay between emitted batches regardless of queue depth. For one batch of up to 50 entries per second, use `COUNT = 50` and `Batch interval = 1000`.
|
|
62
63
|
- **Max deliveries = 5.** After the limit is exceeded, an entry is atomically moved to `<stream>:dlq` and acknowledged. In Redis Cluster, source and DLQ keys must share a hash tag, for example `orders:{eu}` and `orders:{eu}:dlq`.
|
|
63
64
|
- **Start paused (wait for resume)** makes a consumer create/verify its group without calling `XREADGROUP`. It takes no work until a control-plane `resume` request arrives; use it for controlled pod startup after readiness checks.
|
|
@@ -123,6 +124,8 @@ Run `redis consumer gc` from an input message, typically a scheduled Inject node
|
|
|
123
124
|
|
|
124
125
|
## Example flow
|
|
125
126
|
|
|
127
|
+
Import [examples/redis-streams.json](examples/redis-streams.json) from **Import → Examples** after installing the palette. It contains a self-contained `XADD → XREADGROUP → XACK` flow using `example:events`.
|
|
128
|
+
|
|
126
129
|
```
|
|
127
130
|
[yroshcha-redis-config] ← shared configuration for all nodes below
|
|
128
131
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "redis-streams-example-tab",
|
|
4
|
+
"type": "tab",
|
|
5
|
+
"label": "Redis Streams example",
|
|
6
|
+
"disabled": false,
|
|
7
|
+
"info": "Publish an example event, consume it with a Redis consumer group, then ACK it after processing."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "redis-streams-example-note",
|
|
11
|
+
"type": "comment",
|
|
12
|
+
"z": "redis-streams-example-tab",
|
|
13
|
+
"name": "Redis Streams: XADD → XREADGROUP → XACK",
|
|
14
|
+
"info": "Set the Redis config node to your Redis server. Click Publish example event. The consumer group is created automatically and the event is acknowledged after the processing function completes.",
|
|
15
|
+
"x": 310,
|
|
16
|
+
"y": 60,
|
|
17
|
+
"wires": []
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "redis-streams-example-config",
|
|
21
|
+
"type": "yroshcha-redis-config",
|
|
22
|
+
"name": "Local Redis",
|
|
23
|
+
"host": "127.0.0.1",
|
|
24
|
+
"port": "6379",
|
|
25
|
+
"db": "0",
|
|
26
|
+
"username": "",
|
|
27
|
+
"tls": false,
|
|
28
|
+
"cluster": false,
|
|
29
|
+
"connectTimeout": "10000",
|
|
30
|
+
"commandTimeout": "30000",
|
|
31
|
+
"retryMaxDelay": "30000"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "redis-streams-example-inject",
|
|
35
|
+
"type": "inject",
|
|
36
|
+
"z": "redis-streams-example-tab",
|
|
37
|
+
"name": "Publish example event",
|
|
38
|
+
"props": [{ "p": "payload" }],
|
|
39
|
+
"repeat": "",
|
|
40
|
+
"crontab": "",
|
|
41
|
+
"once": false,
|
|
42
|
+
"onceDelay": 0.1,
|
|
43
|
+
"topic": "",
|
|
44
|
+
"payload": "{\"eventType\":\"example.created\",\"event\":{\"id\":\"example-1\",\"message\":\"Hello Redis Streams\"}}",
|
|
45
|
+
"payloadType": "json",
|
|
46
|
+
"x": 160,
|
|
47
|
+
"y": 140,
|
|
48
|
+
"wires": [["redis-streams-example-out"]]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "redis-streams-example-out",
|
|
52
|
+
"type": "yroshcha-redis-stream-out",
|
|
53
|
+
"z": "redis-streams-example-tab",
|
|
54
|
+
"name": "XADD example:events",
|
|
55
|
+
"server": "redis-streams-example-config",
|
|
56
|
+
"streamKey": "example:events",
|
|
57
|
+
"maxlen": "",
|
|
58
|
+
"approxTrim": true,
|
|
59
|
+
"unsafeTrim": false,
|
|
60
|
+
"x": 400,
|
|
61
|
+
"y": 140,
|
|
62
|
+
"wires": [["redis-streams-example-published"]]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "redis-streams-example-published",
|
|
66
|
+
"type": "debug",
|
|
67
|
+
"z": "redis-streams-example-tab",
|
|
68
|
+
"name": "Published stream ID",
|
|
69
|
+
"active": true,
|
|
70
|
+
"tosidebar": true,
|
|
71
|
+
"console": false,
|
|
72
|
+
"tostatus": false,
|
|
73
|
+
"complete": "streamId",
|
|
74
|
+
"targetType": "msg",
|
|
75
|
+
"statusVal": "",
|
|
76
|
+
"statusType": "auto",
|
|
77
|
+
"x": 650,
|
|
78
|
+
"y": 140,
|
|
79
|
+
"wires": []
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "redis-streams-example-in",
|
|
83
|
+
"type": "yroshcha-redis-stream-in",
|
|
84
|
+
"z": "redis-streams-example-tab",
|
|
85
|
+
"name": "XREADGROUP example-workers",
|
|
86
|
+
"server": "redis-streams-example-config",
|
|
87
|
+
"streamKey": "example:events",
|
|
88
|
+
"group": "example-workers",
|
|
89
|
+
"consumer": "",
|
|
90
|
+
"count": "100",
|
|
91
|
+
"blockMs": "5000",
|
|
92
|
+
"readIntervalMs": "0",
|
|
93
|
+
"rateLimitPerSecond": "0",
|
|
94
|
+
"batchWindowMs": "0",
|
|
95
|
+
"batchIntervalMs": "0",
|
|
96
|
+
"autoAck": false,
|
|
97
|
+
"startPaused": false,
|
|
98
|
+
"startId": "$",
|
|
99
|
+
"maxPending": "1000",
|
|
100
|
+
"capacityPollMs": "500",
|
|
101
|
+
"capacityCheckIntervalMs": "250",
|
|
102
|
+
"autoClaim": true,
|
|
103
|
+
"reclaimIdleMs": "60000",
|
|
104
|
+
"reclaimIntervalMs": "30000",
|
|
105
|
+
"reclaimCount": "100",
|
|
106
|
+
"maxDeliveries": "5",
|
|
107
|
+
"deadLetterStream": "",
|
|
108
|
+
"initialBackoffMs": "500",
|
|
109
|
+
"maxBackoffMs": "30000",
|
|
110
|
+
"backoffMultiplier": "2",
|
|
111
|
+
"pelAlertThreshold": "0",
|
|
112
|
+
"pelCheckIntervalMs": "30000",
|
|
113
|
+
"x": 190,
|
|
114
|
+
"y": 260,
|
|
115
|
+
"wires": [["redis-streams-example-process"]]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "redis-streams-example-process",
|
|
119
|
+
"type": "function",
|
|
120
|
+
"z": "redis-streams-example-tab",
|
|
121
|
+
"name": "Business logic (replace me)",
|
|
122
|
+
"func": "// Do business work here.\n// ACK only after this succeeds.\nreturn msg;",
|
|
123
|
+
"outputs": 1,
|
|
124
|
+
"timeout": 0,
|
|
125
|
+
"noerr": 0,
|
|
126
|
+
"initialize": "",
|
|
127
|
+
"finalize": "",
|
|
128
|
+
"libs": [],
|
|
129
|
+
"x": 445,
|
|
130
|
+
"y": 260,
|
|
131
|
+
"wires": [["redis-streams-example-ack"]]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "redis-streams-example-ack",
|
|
135
|
+
"type": "yroshcha-redis-stream-ack",
|
|
136
|
+
"z": "redis-streams-example-tab",
|
|
137
|
+
"name": "ACK after success",
|
|
138
|
+
"server": "redis-streams-example-config",
|
|
139
|
+
"streamKey": "",
|
|
140
|
+
"group": "",
|
|
141
|
+
"deleteAfterAck": false,
|
|
142
|
+
"x": 665,
|
|
143
|
+
"y": 260,
|
|
144
|
+
"wires": [["redis-streams-example-acked"]]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "redis-streams-example-acked",
|
|
148
|
+
"type": "debug",
|
|
149
|
+
"z": "redis-streams-example-tab",
|
|
150
|
+
"name": "ACK result",
|
|
151
|
+
"active": true,
|
|
152
|
+
"tosidebar": true,
|
|
153
|
+
"console": false,
|
|
154
|
+
"tostatus": false,
|
|
155
|
+
"complete": "acked",
|
|
156
|
+
"targetType": "msg",
|
|
157
|
+
"statusVal": "",
|
|
158
|
+
"statusType": "auto",
|
|
159
|
+
"x": 865,
|
|
160
|
+
"y": 260,
|
|
161
|
+
"wires": []
|
|
162
|
+
}
|
|
163
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yroshcha/node-red-contrib-redis-full",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Production-oriented Redis palette for Node-RED: generic commands, Pub/Sub, scans, transactions, and Redis Streams consumer groups.",
|
|
5
5
|
"keywords": ["node-red", "redis", "redis-streams", "pubsub", "xadd", "xreadgroup", "ioredis"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/yroshcha/node-red-contrib-redis-full#readme",
|
|
16
16
|
"main": "redis.js",
|
|
17
|
-
"files": ["redis.js", "redis.html", "README.md", "CHANGELOG.md", "LICENSE", "icons"],
|
|
17
|
+
"files": ["redis.js", "redis.html", "README.md", "CHANGELOG.md", "LICENSE", "icons", "examples"],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=20"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"node-red": ">=
|
|
22
|
+
"node-red": ">=4 <6"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "node --test test/*.test.js",
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
"prepublishOnly": "npm run check"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"ioredis": "^
|
|
31
|
+
"ioredis": "^6.0.0"
|
|
32
32
|
},
|
|
33
33
|
"node-red": {
|
|
34
|
+
"version": ">=4.0.0 <6.0.0",
|
|
34
35
|
"nodes": {
|
|
35
36
|
"yroshcha-redis": "redis.js"
|
|
36
37
|
}
|
package/redis.js
CHANGED
|
@@ -173,6 +173,22 @@ module.exports = function (RED) {
|
|
|
173
173
|
return obj;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function parseStreamFields(flat) {
|
|
177
|
+
const obj = parseFlatFields(flat);
|
|
178
|
+
for (const [field, value] of Object.entries(obj)) {
|
|
179
|
+
// redis xadd serialises nested objects and arrays to JSON. Restore only
|
|
180
|
+
// these compound JSON values; scalar strings such as "true", "42", or
|
|
181
|
+
// "null" remain strings and preserve Redis field semantics.
|
|
182
|
+
if (typeof value !== 'string' || !/^[\[{]/.test(value.trim())) continue;
|
|
183
|
+
try {
|
|
184
|
+
obj[field] = JSON.parse(value);
|
|
185
|
+
} catch (_) {
|
|
186
|
+
// A value that merely looks like JSON is still a valid Redis string.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return obj;
|
|
190
|
+
}
|
|
191
|
+
|
|
176
192
|
function isNoGroupError(err) {
|
|
177
193
|
return String(err && err.message ? err.message : err).includes('NOGROUP');
|
|
178
194
|
}
|
|
@@ -868,7 +884,7 @@ module.exports = function (RED) {
|
|
|
868
884
|
if (!Array.isArray(flat)) {
|
|
869
885
|
throw new Error(`Stream entry ${id} no longer has a payload; do not trim streams below the PEL horizon`);
|
|
870
886
|
}
|
|
871
|
-
const payload =
|
|
887
|
+
const payload = parseStreamFields(flat);
|
|
872
888
|
if (checkDeliveryCount && await shouldDeadLetter(id, payload)) return;
|
|
873
889
|
if (!await waitForDeliverySlot() || paused) return false;
|
|
874
890
|
|
|
@@ -1173,7 +1189,7 @@ module.exports = function (RED) {
|
|
|
1173
1189
|
const [nextCursor, entries] = res;
|
|
1174
1190
|
cursor = nextCursor;
|
|
1175
1191
|
for (const [id, flat] of entries || []) {
|
|
1176
|
-
claimed.push({ streamId: id, payload:
|
|
1192
|
+
claimed.push({ streamId: id, payload: parseStreamFields(flat) });
|
|
1177
1193
|
}
|
|
1178
1194
|
} while (cursor !== '0' && claimed.length < node.maxMessages);
|
|
1179
1195
|
|