node-red-contrib-redis-variable 1.3.1 → 1.5.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.
- package/README.md +0 -0
- package/examples/example.json +0 -0
- package/package.json +1 -1
- package/redis-variable-config.html +0 -0
- package/redis-variable-config.js +21 -21
- package/redis-variable.html +8 -1
- package/redis-variable.js +162 -68
package/README.md
CHANGED
|
File without changes
|
package/examples/example.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-redis-variable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "A comprehensive Node-RED node for Redis operations with universal payload-based configuration, automatic JSON handling, SSL/TLS support, and advanced pattern matching with pagination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
File without changes
|
package/redis-variable-config.js
CHANGED
|
@@ -43,11 +43,11 @@ module.exports = function (RED) {
|
|
|
43
43
|
// Helper function to get nested values like "redis_config.host"
|
|
44
44
|
function getNestedValue(context, path) {
|
|
45
45
|
if (!context) return undefined;
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
if (path.includes('.')) {
|
|
48
48
|
const parts = path.split('.');
|
|
49
49
|
let result = context.get(parts[0]);
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
// If the first part returns an object, traverse it
|
|
52
52
|
if (result && typeof result === 'object') {
|
|
53
53
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -71,7 +71,7 @@ module.exports = function (RED) {
|
|
|
71
71
|
RED.nodes.createNode(this, config);
|
|
72
72
|
this.name = config.name || "Redis Config";
|
|
73
73
|
this.cluster = config.cluster || false;
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
// Connection configuration
|
|
76
76
|
this.hostType = config.hostType || 'str';
|
|
77
77
|
this.host = config.host || 'localhost';
|
|
@@ -79,7 +79,7 @@ module.exports = function (RED) {
|
|
|
79
79
|
this.port = config.port || 6379;
|
|
80
80
|
this.portType = config.portType || 'str';
|
|
81
81
|
this.portContext = config.portContext;
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
// Authentication
|
|
84
84
|
this.passwordType = config.passwordType || 'str';
|
|
85
85
|
this.password = config.password;
|
|
@@ -87,7 +87,7 @@ module.exports = function (RED) {
|
|
|
87
87
|
this.username = config.username;
|
|
88
88
|
this.usernameType = config.usernameType || 'str';
|
|
89
89
|
this.usernameContext = config.usernameContext;
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
// SSL/TLS Configuration
|
|
92
92
|
this.enableTLS = config.enableTLS || false;
|
|
93
93
|
this.tlsRejectUnauthorized = config.tlsRejectUnauthorized !== false; // Default to true
|
|
@@ -97,12 +97,12 @@ module.exports = function (RED) {
|
|
|
97
97
|
this.tlsKeyContext = config.tlsKeyContext;
|
|
98
98
|
this.tlsCaType = config.tlsCaType || 'str';
|
|
99
99
|
this.tlsCaContext = config.tlsCaContext;
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
// Database and other options
|
|
102
102
|
this.database = config.database || 0;
|
|
103
103
|
this.databaseType = config.databaseType || 'str';
|
|
104
104
|
this.databaseContext = config.databaseContext;
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
// Advanced options
|
|
107
107
|
this.optionsType = config.optionsType || 'json';
|
|
108
108
|
this.options = config.options || '{}';
|
|
@@ -116,7 +116,7 @@ module.exports = function (RED) {
|
|
|
116
116
|
if (!value && value !== 0) {
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
try {
|
|
121
121
|
let result;
|
|
122
122
|
switch (type) {
|
|
@@ -164,11 +164,11 @@ module.exports = function (RED) {
|
|
|
164
164
|
if (this.hostType === 'str' && this.portType === 'str') {
|
|
165
165
|
return true;
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
// For context-based configuration, check if the required values exist
|
|
169
169
|
let hasHost = false;
|
|
170
170
|
let hasPort = false;
|
|
171
|
-
|
|
171
|
+
|
|
172
172
|
// Check host configuration
|
|
173
173
|
if (this.hostType === 'str') {
|
|
174
174
|
hasHost = !!(this.host);
|
|
@@ -176,7 +176,7 @@ module.exports = function (RED) {
|
|
|
176
176
|
const contextHost = this.parseCredentialValue(this.hostContext, this.hostType, msg, executingNode);
|
|
177
177
|
hasHost = !!(contextHost);
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
|
|
180
180
|
// Check port configuration
|
|
181
181
|
if (this.portType === 'str') {
|
|
182
182
|
hasPort = !!(this.port);
|
|
@@ -184,16 +184,16 @@ module.exports = function (RED) {
|
|
|
184
184
|
const contextPort = this.parseCredentialValue(this.portContext, this.portType, msg, executingNode);
|
|
185
185
|
hasPort = !!(contextPort);
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
// We need at least host and port to have a valid configuration
|
|
189
189
|
const result = hasHost && hasPort;
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
if (executingNode && process.env.NODE_RED_DEBUG) {
|
|
192
192
|
executingNode.log(`Configuration check - hasHost: ${hasHost}, hasPort: ${hasPort}, result: ${result}`);
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
return result;
|
|
196
|
-
|
|
196
|
+
|
|
197
197
|
} catch (error) {
|
|
198
198
|
if (executingNode) {
|
|
199
199
|
executingNode.error(`Error checking configuration: ${error.message}`);
|
|
@@ -339,7 +339,7 @@ module.exports = function (RED) {
|
|
|
339
339
|
this.getClient = function(msg, executingNode, nodeId) {
|
|
340
340
|
try {
|
|
341
341
|
const id = nodeId || this.id;
|
|
342
|
-
|
|
342
|
+
|
|
343
343
|
// Return existing connection if available
|
|
344
344
|
if (connections[id]) {
|
|
345
345
|
usedConn[id]++;
|
|
@@ -353,7 +353,7 @@ module.exports = function (RED) {
|
|
|
353
353
|
if (!executingNode._configWarningShown) {
|
|
354
354
|
executingNode.warn("Redis configuration not available in context. Skipping connection attempt.");
|
|
355
355
|
executingNode._configWarningShown = true;
|
|
356
|
-
|
|
356
|
+
|
|
357
357
|
// Reset warning flag after 30 seconds
|
|
358
358
|
setTimeout(() => {
|
|
359
359
|
if (executingNode) {
|
|
@@ -400,11 +400,11 @@ module.exports = function (RED) {
|
|
|
400
400
|
// Handle connection errors
|
|
401
401
|
client.on("error", (e) => {
|
|
402
402
|
const now = Date.now();
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
// Only report errors once per interval to prevent spam
|
|
405
405
|
if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
|
|
406
406
|
let errorMsg = `Redis connection error: ${e.message}`;
|
|
407
|
-
|
|
407
|
+
|
|
408
408
|
// Add specific diagnostics for common SSL issues
|
|
409
409
|
if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
|
|
410
410
|
errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
|
|
@@ -413,13 +413,13 @@ module.exports = function (RED) {
|
|
|
413
413
|
errorMsg += "\n3. Check if your Redis server is configured for SSL";
|
|
414
414
|
errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
|
|
415
415
|
}
|
|
416
|
-
|
|
416
|
+
|
|
417
417
|
if (executingNode) {
|
|
418
418
|
executingNode.error(errorMsg, {});
|
|
419
419
|
} else {
|
|
420
420
|
this.error(errorMsg, {});
|
|
421
421
|
}
|
|
422
|
-
|
|
422
|
+
|
|
423
423
|
errorReported = true;
|
|
424
424
|
lastErrorTime = now;
|
|
425
425
|
}
|
package/redis-variable.html
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
redisConfig: { value: "", type: "redis-variable-config" },
|
|
8
|
-
operation: { value: "get" }
|
|
8
|
+
operation: { value: "get" },
|
|
9
|
+
topic: { value: "" }
|
|
9
10
|
},
|
|
10
11
|
inputs: 1,
|
|
11
12
|
outputs: 1,
|
|
@@ -62,9 +63,15 @@
|
|
|
62
63
|
</optgroup>
|
|
63
64
|
<optgroup label="Pub/Sub Operations">
|
|
64
65
|
<option value="publish">PUBLISH - Publish message</option>
|
|
66
|
+
<option value="subscribe">SUBSCRIBE - Subscribe to channel</option>
|
|
67
|
+
<option value="psubscribe">PSUBSCRIBE - Pattern subscribe</option>
|
|
65
68
|
</optgroup>
|
|
66
69
|
</select>
|
|
67
70
|
</div>
|
|
71
|
+
<div class="form-row">
|
|
72
|
+
<label for="node-input-topic"><i class="fa fa-hashtag"></i> Channel/Pattern</label>
|
|
73
|
+
<input type="text" id="node-input-topic" placeholder="notifications or news.*">
|
|
74
|
+
</div>
|
|
68
75
|
</script>
|
|
69
76
|
|
|
70
77
|
<script type="text/x-red" data-help-name="redis-variable">
|
package/redis-variable.js
CHANGED
|
@@ -4,7 +4,7 @@ module.exports = function (RED) {
|
|
|
4
4
|
function RedisVariableNode(config) {
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
6
6
|
const node = this;
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
// Node configuration
|
|
9
9
|
this.operation = config.operation || "get";
|
|
10
10
|
this.timeout = config.timeout || 0;
|
|
@@ -14,6 +14,7 @@ module.exports = function (RED) {
|
|
|
14
14
|
this.stored = config.stored || false;
|
|
15
15
|
this.params = config.params;
|
|
16
16
|
this.location = config.location || 'flow';
|
|
17
|
+
this.topic = config.topic || "";
|
|
17
18
|
this.sha1 = "";
|
|
18
19
|
|
|
19
20
|
// Save redisConfig once in the constructor
|
|
@@ -51,6 +52,19 @@ module.exports = function (RED) {
|
|
|
51
52
|
return str;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function stableStringify(value) {
|
|
56
|
+
if (value === null || value === undefined) return String(value);
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'object') {
|
|
61
|
+
const keys = Object.keys(value).sort();
|
|
62
|
+
const entries = keys.map((key) => `"${key}":${stableStringify(value[key])}`);
|
|
63
|
+
return `{${entries.join(',')}}`;
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
// Handle different operations
|
|
55
69
|
switch (this.operation) {
|
|
56
70
|
case "subscribe":
|
|
@@ -75,56 +89,89 @@ module.exports = function (RED) {
|
|
|
75
89
|
// Subscription operations (subscribe, psubscribe)
|
|
76
90
|
function handleSubscription() {
|
|
77
91
|
try {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
if (!redisConfig) {
|
|
93
|
+
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
94
|
+
node.warn("Redis configuration not found for subscription.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!node.topic) {
|
|
99
|
+
node.status({ fill: "red", shape: "dot", text: "missing channel" });
|
|
100
|
+
node.error("Missing channel/pattern for subscription (topic)");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
client = redisConfig.getClient(null, node, node.id);
|
|
105
|
+
if (!client) {
|
|
106
|
+
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
107
|
+
node.warn("Redis configuration not available. Subscription skipped.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const setup = async () => {
|
|
112
|
+
try {
|
|
113
|
+
if (client.status !== 'ready' && client.status !== 'connect') {
|
|
114
|
+
await client.connect();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const topics = (node.topic || "").split(/[\s,]+/).filter(Boolean);
|
|
118
|
+
|
|
119
|
+
if (node.operation === "psubscribe") {
|
|
120
|
+
client.on("pmessage", function (pattern, channel, message) {
|
|
121
|
+
var payload = smartParse(message);
|
|
122
|
+
node.send({
|
|
123
|
+
pattern: pattern,
|
|
124
|
+
topic: channel,
|
|
125
|
+
payload: payload,
|
|
126
|
+
});
|
|
94
127
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
client.psubscribe(...topics, (err) => {
|
|
129
|
+
if (err) {
|
|
130
|
+
// Fallback: if server doesn't support PSUBSCRIBE and no wildcard is used, try SUBSCRIBE
|
|
131
|
+
if (err.message && err.message.toLowerCase().includes('unknown command')) {
|
|
132
|
+
const hasWildcard = topics.some(t => t.includes('*'));
|
|
133
|
+
if (!hasWildcard) {
|
|
134
|
+
client.subscribe(...topics, (subErr) => {
|
|
135
|
+
if (subErr) {
|
|
136
|
+
node.error(subErr.message);
|
|
137
|
+
node.status({ fill: "red", shape: "dot", text: "error" });
|
|
138
|
+
} else {
|
|
139
|
+
node.status({ fill: "green", shape: "dot", text: "connected" });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
node.error(err.message);
|
|
146
|
+
node.status({ fill: "red", shape: "dot", text: "error" });
|
|
147
|
+
} else {
|
|
148
|
+
node.status({ fill: "green", shape: "dot", text: "connected" });
|
|
149
|
+
}
|
|
100
150
|
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
payload: payload,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
client[node.operation](node.topic, (err, count) => {
|
|
112
|
-
if (err) {
|
|
113
|
-
node.error(err.message);
|
|
114
|
-
node.status({
|
|
115
|
-
fill: "red",
|
|
116
|
-
shape: "dot",
|
|
117
|
-
text: "error",
|
|
151
|
+
} else if (node.operation === "subscribe") {
|
|
152
|
+
client.on("message", function (channel, message) {
|
|
153
|
+
var payload = smartParse(message);
|
|
154
|
+
node.send({
|
|
155
|
+
topic: channel,
|
|
156
|
+
payload: payload,
|
|
157
|
+
});
|
|
118
158
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
159
|
+
client.subscribe(...topics, (err) => {
|
|
160
|
+
if (err) {
|
|
161
|
+
node.error(err.message);
|
|
162
|
+
node.status({ fill: "red", shape: "dot", text: "error" });
|
|
163
|
+
} else {
|
|
164
|
+
node.status({ fill: "green", shape: "dot", text: "connected" });
|
|
165
|
+
}
|
|
124
166
|
});
|
|
125
167
|
}
|
|
126
|
-
})
|
|
127
|
-
|
|
168
|
+
} catch (e) {
|
|
169
|
+
node.error(`Subscription setup failed: ${e.message}`);
|
|
170
|
+
node.status({ fill: "red", shape: "dot", text: "error" });
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
setup();
|
|
128
175
|
} catch (error) {
|
|
129
176
|
node.error(`Subscription error: ${error.message}`);
|
|
130
177
|
node.status({
|
|
@@ -275,6 +322,26 @@ module.exports = function (RED) {
|
|
|
275
322
|
return;
|
|
276
323
|
}
|
|
277
324
|
|
|
325
|
+
// Recreate client when connection options change (e.g., context-based config)
|
|
326
|
+
try {
|
|
327
|
+
const options = redisConfig.getConnectionOptions(msg, node);
|
|
328
|
+
const configKey = stableStringify({
|
|
329
|
+
cluster: redisConfig.cluster === true,
|
|
330
|
+
options
|
|
331
|
+
});
|
|
332
|
+
if (node._lastConnKey && node._lastConnKey !== configKey) {
|
|
333
|
+
redisConfig.forceDisconnect(node.id);
|
|
334
|
+
client = null;
|
|
335
|
+
}
|
|
336
|
+
node._lastConnKey = configKey;
|
|
337
|
+
} catch (configError) {
|
|
338
|
+
node.error(configError.message, msg);
|
|
339
|
+
msg.payload = { error: configError.message };
|
|
340
|
+
send(msg);
|
|
341
|
+
done();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
278
345
|
// Only create client if not already created
|
|
279
346
|
try {
|
|
280
347
|
if (!client) {
|
|
@@ -288,11 +355,13 @@ module.exports = function (RED) {
|
|
|
288
355
|
return;
|
|
289
356
|
}
|
|
290
357
|
}
|
|
291
|
-
|
|
358
|
+
|
|
292
359
|
// Check if client is connected before proceeding
|
|
293
360
|
if (client.status !== 'ready' && client.status !== 'connect') {
|
|
294
361
|
// Try to connect if not connected
|
|
295
|
-
if (client.status === '
|
|
362
|
+
if (client.status === 'wait') {
|
|
363
|
+
await client.connect();
|
|
364
|
+
} else if (client.status === 'disconnect' || client.status === 'end') {
|
|
296
365
|
await client.connect();
|
|
297
366
|
} else {
|
|
298
367
|
// Force disconnect and recreate client for other error states
|
|
@@ -346,12 +415,25 @@ module.exports = function (RED) {
|
|
|
346
415
|
if (!payload.key) {
|
|
347
416
|
throw new Error("Missing key for SET operation. Use payload.key");
|
|
348
417
|
}
|
|
349
|
-
|
|
418
|
+
const payloadIsObject = payload && typeof payload === 'object' && !Array.isArray(payload);
|
|
419
|
+
const payloadHasKey = payloadIsObject && Object.prototype.hasOwnProperty.call(payload, 'key');
|
|
420
|
+
const payloadHasValue = payloadIsObject && (
|
|
421
|
+
Object.prototype.hasOwnProperty.call(payload, 'value') ||
|
|
422
|
+
Object.prototype.hasOwnProperty.call(payload, 'data')
|
|
423
|
+
);
|
|
424
|
+
let setValue;
|
|
425
|
+
if (payloadHasValue) {
|
|
426
|
+
setValue = payload.value !== undefined ? payload.value : payload.data;
|
|
427
|
+
} else if (payloadHasKey) {
|
|
428
|
+
setValue = undefined;
|
|
429
|
+
} else {
|
|
430
|
+
setValue = payload;
|
|
431
|
+
}
|
|
350
432
|
if (setValue === undefined) {
|
|
351
|
-
throw new Error("Missing value for SET operation. Use payload.value or payload
|
|
433
|
+
throw new Error("Missing value for SET operation. Use payload.value, payload.data, or payload as value");
|
|
352
434
|
}
|
|
353
435
|
setValue = smartSerialize(setValue);
|
|
354
|
-
|
|
436
|
+
|
|
355
437
|
// Support TTL
|
|
356
438
|
if (payload.ttl && payload.ttl > 0) {
|
|
357
439
|
response = await client.setex(payload.key, payload.ttl, setValue);
|
|
@@ -386,14 +468,14 @@ module.exports = function (RED) {
|
|
|
386
468
|
if (!pattern || typeof pattern !== 'string') {
|
|
387
469
|
throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
|
|
388
470
|
}
|
|
389
|
-
|
|
471
|
+
|
|
390
472
|
let count = payload.count || 100; // Default scan count
|
|
391
473
|
let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
|
|
392
474
|
let allKeys = [];
|
|
393
475
|
let cursor = startCursor;
|
|
394
476
|
let maxIterations = 1000; // Prevent infinite loops
|
|
395
477
|
let iterations = 0;
|
|
396
|
-
|
|
478
|
+
|
|
397
479
|
// If skip is specified, we need to scan through keys without collecting them
|
|
398
480
|
if (payload.skip && payload.skip > 0) {
|
|
399
481
|
let skipped = 0;
|
|
@@ -404,7 +486,7 @@ module.exports = function (RED) {
|
|
|
404
486
|
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
405
487
|
cursor = scanResult[0];
|
|
406
488
|
skipped += scanResult[1].length;
|
|
407
|
-
|
|
489
|
+
|
|
408
490
|
if (skipped >= payload.skip) {
|
|
409
491
|
// We've skipped enough, start collecting from this point
|
|
410
492
|
const excessSkipped = skipped - payload.skip;
|
|
@@ -416,7 +498,7 @@ module.exports = function (RED) {
|
|
|
416
498
|
}
|
|
417
499
|
} while (cursor !== 0 && cursor !== '0');
|
|
418
500
|
}
|
|
419
|
-
|
|
501
|
+
|
|
420
502
|
// Continue scanning to collect keys up to the limit
|
|
421
503
|
do {
|
|
422
504
|
if (iterations++ > maxIterations) {
|
|
@@ -425,15 +507,15 @@ module.exports = function (RED) {
|
|
|
425
507
|
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
426
508
|
cursor = scanResult[0];
|
|
427
509
|
allKeys = allKeys.concat(scanResult[1]);
|
|
428
|
-
|
|
510
|
+
|
|
429
511
|
// Break early if we've found enough keys
|
|
430
512
|
if (allKeys.length >= count) {
|
|
431
513
|
allKeys = allKeys.slice(0, count); // Trim to exact count
|
|
432
514
|
break;
|
|
433
515
|
}
|
|
434
516
|
} while (cursor !== 0 && cursor !== '0');
|
|
435
|
-
|
|
436
|
-
msg.payload = {
|
|
517
|
+
|
|
518
|
+
msg.payload = {
|
|
437
519
|
pattern: pattern,
|
|
438
520
|
keys: allKeys,
|
|
439
521
|
count: allKeys.length,
|
|
@@ -453,7 +535,7 @@ module.exports = function (RED) {
|
|
|
453
535
|
throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
|
|
454
536
|
}
|
|
455
537
|
response = await client.ttl(ttlKey);
|
|
456
|
-
msg.payload = {
|
|
538
|
+
msg.payload = {
|
|
457
539
|
key: ttlKey,
|
|
458
540
|
ttl: response,
|
|
459
541
|
status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
|
|
@@ -466,7 +548,7 @@ module.exports = function (RED) {
|
|
|
466
548
|
}
|
|
467
549
|
let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
|
|
468
550
|
response = await client.expire(payload.key, expireSeconds);
|
|
469
|
-
msg.payload = {
|
|
551
|
+
msg.payload = {
|
|
470
552
|
success: response === 1,
|
|
471
553
|
key: payload.key,
|
|
472
554
|
ttl: expireSeconds,
|
|
@@ -480,7 +562,7 @@ module.exports = function (RED) {
|
|
|
480
562
|
throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
|
|
481
563
|
}
|
|
482
564
|
response = await client.persist(persistKey);
|
|
483
|
-
msg.payload = {
|
|
565
|
+
msg.payload = {
|
|
484
566
|
success: response === 1,
|
|
485
567
|
key: persistKey,
|
|
486
568
|
message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
|
|
@@ -661,7 +743,7 @@ module.exports = function (RED) {
|
|
|
661
743
|
}
|
|
662
744
|
} catch (redisError) {
|
|
663
745
|
// Handle Redis-specific errors (connection, command errors, etc.)
|
|
664
|
-
if (redisError.message.includes('ECONNREFUSED') ||
|
|
746
|
+
if (redisError.message.includes('ECONNREFUSED') ||
|
|
665
747
|
redisError.message.includes('ENOTFOUND') ||
|
|
666
748
|
redisError.message.includes('ETIMEDOUT')) {
|
|
667
749
|
// Connection-related errors - force disconnect to prevent dead connections
|
|
@@ -673,8 +755,8 @@ module.exports = function (RED) {
|
|
|
673
755
|
// Ignore disconnect errors
|
|
674
756
|
}
|
|
675
757
|
}
|
|
676
|
-
|
|
677
|
-
msg.payload = {
|
|
758
|
+
|
|
759
|
+
msg.payload = {
|
|
678
760
|
error: `Redis connection failed: ${redisError.message}`,
|
|
679
761
|
operation: node.operation,
|
|
680
762
|
retryable: true
|
|
@@ -682,7 +764,7 @@ module.exports = function (RED) {
|
|
|
682
764
|
node.status({ fill: "red", shape: "ring", text: "connection failed" });
|
|
683
765
|
} else {
|
|
684
766
|
// Other Redis errors (command errors, etc.)
|
|
685
|
-
msg.payload = {
|
|
767
|
+
msg.payload = {
|
|
686
768
|
error: `Redis operation failed: ${redisError.message}`,
|
|
687
769
|
operation: node.operation,
|
|
688
770
|
retryable: false
|
|
@@ -729,7 +811,7 @@ module.exports = function (RED) {
|
|
|
729
811
|
node.on("close", async (undeploy, done) => {
|
|
730
812
|
node.status({});
|
|
731
813
|
running = false;
|
|
732
|
-
|
|
814
|
+
|
|
733
815
|
if (node.operation === "instance" && node.location && node.topic) {
|
|
734
816
|
try {
|
|
735
817
|
node.context()[node.location].set(node.topic, null);
|
|
@@ -737,12 +819,24 @@ module.exports = function (RED) {
|
|
|
737
819
|
// Ignore errors when cleaning up context
|
|
738
820
|
}
|
|
739
821
|
}
|
|
740
|
-
|
|
822
|
+
|
|
823
|
+
if (client) {
|
|
824
|
+
try {
|
|
825
|
+
if (node.operation === 'subscribe' && node.topic) {
|
|
826
|
+
await client.unsubscribe(node.topic);
|
|
827
|
+
} else if (node.operation === 'psubscribe' && node.topic) {
|
|
828
|
+
await client.punsubscribe(node.topic);
|
|
829
|
+
}
|
|
830
|
+
} catch (e) {
|
|
831
|
+
// ignore
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
741
835
|
if (redisConfig) {
|
|
742
836
|
const nodeId = node.block ? node.id : redisConfig.id;
|
|
743
837
|
redisConfig.disconnect(nodeId);
|
|
744
838
|
}
|
|
745
|
-
|
|
839
|
+
|
|
746
840
|
client = null;
|
|
747
841
|
done();
|
|
748
842
|
});
|