@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 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
- **`1.0.7` is the current stable release.** See [CHANGELOG.md](CHANGELOG.md) for release notes and compatibility information.
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": "1.0.7",
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": ">=18"
19
+ "node": ">=20"
20
20
  },
21
21
  "peerDependencies": {
22
- "node-red": ">=3 <6"
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": "^5.4.1"
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 = parseFlatFields(flat);
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: parseFlatFields(flat) });
1192
+ claimed.push({ streamId: id, payload: parseStreamFields(flat) });
1177
1193
  }
1178
1194
  } while (cursor !== '0' && claimed.length < node.maxMessages);
1179
1195