node-red-contrib-redis-variable 1.4.1 → 1.5.2
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/package.json +1 -1
- package/redis-variable-config.js +29 -61
- package/redis-variable.js +117 -52
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.2",
|
|
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",
|
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) {
|
|
@@ -382,58 +382,26 @@ module.exports = function (RED) {
|
|
|
382
382
|
maxRetriesPerRequest: 0
|
|
383
383
|
};
|
|
384
384
|
|
|
385
|
-
// Create Redis client
|
|
386
385
|
// Create Redis client
|
|
387
386
|
let client;
|
|
388
387
|
if (this.cluster) {
|
|
389
|
-
//
|
|
390
|
-
const
|
|
391
|
-
const port = parseInt(options.port);
|
|
392
|
-
|
|
393
|
-
// AWS ElastiCache cluster connection (TLS-safe)
|
|
394
|
-
const clusterNodes = [{ host, port }];
|
|
395
|
-
|
|
388
|
+
// ioredis Cluster: first arg = startup nodes [{ host, port }], second = options with redisOptions
|
|
389
|
+
const clusterNodes = [{ host: options.host, port: parseInt(options.port, 10) }];
|
|
396
390
|
const clusterOptions = {
|
|
397
|
-
scaleReads: "slave",
|
|
398
391
|
enableReadyCheck: false,
|
|
399
|
-
|
|
392
|
+
lazyConnect: true,
|
|
393
|
+
maxRetriesPerRequest: 3,
|
|
400
394
|
redisOptions: {
|
|
401
395
|
username: options.username || undefined,
|
|
402
396
|
password: options.password || undefined,
|
|
403
|
-
db:
|
|
404
|
-
connectTimeout: 5000,
|
|
405
|
-
|
|
406
|
-
retryStrategy: (times) => Math.min(times * 200, 2000),
|
|
407
|
-
tls: this.enableTLS
|
|
408
|
-
? {
|
|
409
|
-
servername: host,
|
|
410
|
-
rejectUnauthorized: this.tlsRejectUnauthorized,
|
|
411
|
-
ca: this.credentials?.tlsCa || undefined,
|
|
412
|
-
cert: this.credentials?.tlsCert || undefined,
|
|
413
|
-
key: this.credentials?.tlsKey || undefined,
|
|
414
|
-
}
|
|
415
|
-
: undefined,
|
|
397
|
+
db: 0,
|
|
398
|
+
connectTimeout: connectionOptions.connectTimeout || 5000,
|
|
399
|
+
tls: connectionOptions.tls || undefined,
|
|
416
400
|
},
|
|
417
401
|
};
|
|
418
|
-
|
|
419
402
|
client = new Redis.Cluster(clusterNodes, clusterOptions);
|
|
420
403
|
} else {
|
|
421
|
-
|
|
422
|
-
client = new Redis({
|
|
423
|
-
...options,
|
|
424
|
-
enableReadyCheck: false,
|
|
425
|
-
connectTimeout: 5000,
|
|
426
|
-
maxRetriesPerRequest: 3,
|
|
427
|
-
tls: this.enableTLS
|
|
428
|
-
? {
|
|
429
|
-
servername: options.host,
|
|
430
|
-
rejectUnauthorized: this.tlsRejectUnauthorized,
|
|
431
|
-
ca: this.credentials?.tlsCa || undefined,
|
|
432
|
-
cert: this.credentials?.tlsCert || undefined,
|
|
433
|
-
key: this.credentials?.tlsKey || undefined,
|
|
434
|
-
}
|
|
435
|
-
: undefined,
|
|
436
|
-
});
|
|
404
|
+
client = new Redis(connectionOptions);
|
|
437
405
|
}
|
|
438
406
|
|
|
439
407
|
// Track error state to prevent spam
|
|
@@ -444,11 +412,11 @@ module.exports = function (RED) {
|
|
|
444
412
|
// Handle connection errors
|
|
445
413
|
client.on("error", (e) => {
|
|
446
414
|
const now = Date.now();
|
|
447
|
-
|
|
415
|
+
|
|
448
416
|
// Only report errors once per interval to prevent spam
|
|
449
417
|
if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
|
|
450
418
|
let errorMsg = `Redis connection error: ${e.message}`;
|
|
451
|
-
|
|
419
|
+
|
|
452
420
|
// Add specific diagnostics for common SSL issues
|
|
453
421
|
if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
|
|
454
422
|
errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
|
|
@@ -457,13 +425,13 @@ module.exports = function (RED) {
|
|
|
457
425
|
errorMsg += "\n3. Check if your Redis server is configured for SSL";
|
|
458
426
|
errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
|
|
459
427
|
}
|
|
460
|
-
|
|
428
|
+
|
|
461
429
|
if (executingNode) {
|
|
462
430
|
executingNode.error(errorMsg, {});
|
|
463
431
|
} else {
|
|
464
432
|
this.error(errorMsg, {});
|
|
465
433
|
}
|
|
466
|
-
|
|
434
|
+
|
|
467
435
|
errorReported = true;
|
|
468
436
|
lastErrorTime = now;
|
|
469
437
|
}
|
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;
|
|
@@ -52,6 +52,19 @@ module.exports = function (RED) {
|
|
|
52
52
|
return str;
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
68
|
// Handle different operations
|
|
56
69
|
switch (this.operation) {
|
|
57
70
|
case "subscribe":
|
|
@@ -296,6 +309,21 @@ module.exports = function (RED) {
|
|
|
296
309
|
send = send || function() { node.send.apply(node, arguments) };
|
|
297
310
|
done = done || function(err) { if(err) node.error(err, msg); };
|
|
298
311
|
|
|
312
|
+
// Ensure we only send and call done once per message
|
|
313
|
+
let completed = false;
|
|
314
|
+
const finish = (err) => {
|
|
315
|
+
if (completed) return;
|
|
316
|
+
completed = true;
|
|
317
|
+
if (err) node.error(err.message || err, msg);
|
|
318
|
+
done(err);
|
|
319
|
+
};
|
|
320
|
+
const sendAndDone = (m) => {
|
|
321
|
+
if (completed) return;
|
|
322
|
+
completed = true;
|
|
323
|
+
send(m);
|
|
324
|
+
done();
|
|
325
|
+
};
|
|
326
|
+
|
|
299
327
|
if (!running) {
|
|
300
328
|
running = true;
|
|
301
329
|
}
|
|
@@ -304,8 +332,26 @@ module.exports = function (RED) {
|
|
|
304
332
|
if (!redisConfig) {
|
|
305
333
|
node.error("Redis configuration not found", msg);
|
|
306
334
|
msg.payload = { error: "Redis configuration not found" };
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
sendAndDone(msg);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Recreate client when connection options change (e.g., context-based config)
|
|
340
|
+
try {
|
|
341
|
+
const options = redisConfig.getConnectionOptions(msg, node);
|
|
342
|
+
const configKey = stableStringify({
|
|
343
|
+
cluster: redisConfig.cluster === true,
|
|
344
|
+
options
|
|
345
|
+
});
|
|
346
|
+
if (node._lastConnKey && node._lastConnKey !== configKey) {
|
|
347
|
+
redisConfig.forceDisconnect(node.id);
|
|
348
|
+
client = null;
|
|
349
|
+
}
|
|
350
|
+
node._lastConnKey = configKey;
|
|
351
|
+
} catch (configError) {
|
|
352
|
+
node.error(configError.message, msg);
|
|
353
|
+
msg.payload = { error: configError.message };
|
|
354
|
+
sendAndDone(msg);
|
|
309
355
|
return;
|
|
310
356
|
}
|
|
311
357
|
|
|
@@ -317,41 +363,50 @@ module.exports = function (RED) {
|
|
|
317
363
|
node.warn("Redis configuration not available. Operation skipped.");
|
|
318
364
|
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
319
365
|
msg.payload = { error: "Redis configuration not available" };
|
|
320
|
-
|
|
321
|
-
done();
|
|
366
|
+
sendAndDone(msg);
|
|
322
367
|
return;
|
|
323
368
|
}
|
|
324
369
|
}
|
|
325
|
-
|
|
326
|
-
//
|
|
370
|
+
|
|
371
|
+
// Ensure client is connected before running commands
|
|
327
372
|
if (client.status !== 'ready' && client.status !== 'connect') {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
373
|
+
const CONNECT_WAIT_MS = 15000;
|
|
374
|
+
const waitForReady = () => new Promise((resolve, reject) => {
|
|
375
|
+
const timeout = setTimeout(() => reject(new Error('Connection timeout')), CONNECT_WAIT_MS);
|
|
376
|
+
const onReady = () => { clearTimeout(timeout); cleanup(); resolve(); };
|
|
377
|
+
const onError = (e) => { clearTimeout(timeout); cleanup(); reject(e); };
|
|
378
|
+
const cleanup = () => {
|
|
379
|
+
client.removeListener('ready', onReady);
|
|
380
|
+
client.removeListener('connect', onReady);
|
|
381
|
+
client.removeListener('error', onError);
|
|
382
|
+
};
|
|
383
|
+
client.once('ready', onReady);
|
|
384
|
+
client.once('connect', onReady);
|
|
385
|
+
client.once('error', onError);
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
if (client.status === 'connecting') {
|
|
389
|
+
// Already connecting: wait for ready, do not call connect() again
|
|
390
|
+
await waitForReady();
|
|
391
|
+
} else if (typeof client.connect === 'function') {
|
|
392
|
+
await client.connect();
|
|
393
|
+
}
|
|
394
|
+
} catch (connectErr) {
|
|
395
|
+
const msg = connectErr.message || String(connectErr);
|
|
396
|
+
if (msg.includes('already connecting') || msg.includes('already connected')) {
|
|
397
|
+
// Race: another message triggered connect; wait for ready
|
|
398
|
+
await waitForReady();
|
|
399
|
+
} else {
|
|
334
400
|
redisConfig.forceDisconnect(node.id);
|
|
335
401
|
client = null;
|
|
336
|
-
|
|
337
|
-
if (!client) {
|
|
338
|
-
node.warn("Redis configuration not available during reconnection. Operation skipped.");
|
|
339
|
-
node.status({ fill: "yellow", shape: "ring", text: "no config" });
|
|
340
|
-
msg.payload = { error: "Redis configuration not available" };
|
|
341
|
-
send(msg);
|
|
342
|
-
done();
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
} catch (reconnectError) {
|
|
346
|
-
throw new Error(`Failed to reconnect: ${reconnectError.message}`);
|
|
402
|
+
throw new Error(`Failed to connect: ${msg}`);
|
|
347
403
|
}
|
|
348
404
|
}
|
|
349
405
|
}
|
|
350
406
|
} catch (err) {
|
|
351
407
|
node.error(err.message, msg);
|
|
352
408
|
msg.payload = { error: err.message };
|
|
353
|
-
|
|
354
|
-
done();
|
|
409
|
+
sendAndDone(msg);
|
|
355
410
|
return;
|
|
356
411
|
}
|
|
357
412
|
|
|
@@ -380,12 +435,25 @@ module.exports = function (RED) {
|
|
|
380
435
|
if (!payload.key) {
|
|
381
436
|
throw new Error("Missing key for SET operation. Use payload.key");
|
|
382
437
|
}
|
|
383
|
-
|
|
438
|
+
const payloadIsObject = payload && typeof payload === 'object' && !Array.isArray(payload);
|
|
439
|
+
const payloadHasKey = payloadIsObject && Object.prototype.hasOwnProperty.call(payload, 'key');
|
|
440
|
+
const payloadHasValue = payloadIsObject && (
|
|
441
|
+
Object.prototype.hasOwnProperty.call(payload, 'value') ||
|
|
442
|
+
Object.prototype.hasOwnProperty.call(payload, 'data')
|
|
443
|
+
);
|
|
444
|
+
let setValue;
|
|
445
|
+
if (payloadHasValue) {
|
|
446
|
+
setValue = payload.value !== undefined ? payload.value : payload.data;
|
|
447
|
+
} else if (payloadHasKey) {
|
|
448
|
+
setValue = undefined;
|
|
449
|
+
} else {
|
|
450
|
+
setValue = payload;
|
|
451
|
+
}
|
|
384
452
|
if (setValue === undefined) {
|
|
385
|
-
throw new Error("Missing value for SET operation. Use payload.value or payload
|
|
453
|
+
throw new Error("Missing value for SET operation. Use payload.value, payload.data, or payload as value");
|
|
386
454
|
}
|
|
387
455
|
setValue = smartSerialize(setValue);
|
|
388
|
-
|
|
456
|
+
|
|
389
457
|
// Support TTL
|
|
390
458
|
if (payload.ttl && payload.ttl > 0) {
|
|
391
459
|
response = await client.setex(payload.key, payload.ttl, setValue);
|
|
@@ -420,14 +488,14 @@ module.exports = function (RED) {
|
|
|
420
488
|
if (!pattern || typeof pattern !== 'string') {
|
|
421
489
|
throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
|
|
422
490
|
}
|
|
423
|
-
|
|
491
|
+
|
|
424
492
|
let count = payload.count || 100; // Default scan count
|
|
425
493
|
let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
|
|
426
494
|
let allKeys = [];
|
|
427
495
|
let cursor = startCursor;
|
|
428
496
|
let maxIterations = 1000; // Prevent infinite loops
|
|
429
497
|
let iterations = 0;
|
|
430
|
-
|
|
498
|
+
|
|
431
499
|
// If skip is specified, we need to scan through keys without collecting them
|
|
432
500
|
if (payload.skip && payload.skip > 0) {
|
|
433
501
|
let skipped = 0;
|
|
@@ -438,7 +506,7 @@ module.exports = function (RED) {
|
|
|
438
506
|
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
439
507
|
cursor = scanResult[0];
|
|
440
508
|
skipped += scanResult[1].length;
|
|
441
|
-
|
|
509
|
+
|
|
442
510
|
if (skipped >= payload.skip) {
|
|
443
511
|
// We've skipped enough, start collecting from this point
|
|
444
512
|
const excessSkipped = skipped - payload.skip;
|
|
@@ -450,7 +518,7 @@ module.exports = function (RED) {
|
|
|
450
518
|
}
|
|
451
519
|
} while (cursor !== 0 && cursor !== '0');
|
|
452
520
|
}
|
|
453
|
-
|
|
521
|
+
|
|
454
522
|
// Continue scanning to collect keys up to the limit
|
|
455
523
|
do {
|
|
456
524
|
if (iterations++ > maxIterations) {
|
|
@@ -459,15 +527,15 @@ module.exports = function (RED) {
|
|
|
459
527
|
const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
|
|
460
528
|
cursor = scanResult[0];
|
|
461
529
|
allKeys = allKeys.concat(scanResult[1]);
|
|
462
|
-
|
|
530
|
+
|
|
463
531
|
// Break early if we've found enough keys
|
|
464
532
|
if (allKeys.length >= count) {
|
|
465
533
|
allKeys = allKeys.slice(0, count); // Trim to exact count
|
|
466
534
|
break;
|
|
467
535
|
}
|
|
468
536
|
} while (cursor !== 0 && cursor !== '0');
|
|
469
|
-
|
|
470
|
-
msg.payload = {
|
|
537
|
+
|
|
538
|
+
msg.payload = {
|
|
471
539
|
pattern: pattern,
|
|
472
540
|
keys: allKeys,
|
|
473
541
|
count: allKeys.length,
|
|
@@ -487,7 +555,7 @@ module.exports = function (RED) {
|
|
|
487
555
|
throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
|
|
488
556
|
}
|
|
489
557
|
response = await client.ttl(ttlKey);
|
|
490
|
-
msg.payload = {
|
|
558
|
+
msg.payload = {
|
|
491
559
|
key: ttlKey,
|
|
492
560
|
ttl: response,
|
|
493
561
|
status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
|
|
@@ -500,7 +568,7 @@ module.exports = function (RED) {
|
|
|
500
568
|
}
|
|
501
569
|
let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
|
|
502
570
|
response = await client.expire(payload.key, expireSeconds);
|
|
503
|
-
msg.payload = {
|
|
571
|
+
msg.payload = {
|
|
504
572
|
success: response === 1,
|
|
505
573
|
key: payload.key,
|
|
506
574
|
ttl: expireSeconds,
|
|
@@ -514,7 +582,7 @@ module.exports = function (RED) {
|
|
|
514
582
|
throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
|
|
515
583
|
}
|
|
516
584
|
response = await client.persist(persistKey);
|
|
517
|
-
msg.payload = {
|
|
585
|
+
msg.payload = {
|
|
518
586
|
success: response === 1,
|
|
519
587
|
key: persistKey,
|
|
520
588
|
message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
|
|
@@ -695,7 +763,7 @@ module.exports = function (RED) {
|
|
|
695
763
|
}
|
|
696
764
|
} catch (redisError) {
|
|
697
765
|
// Handle Redis-specific errors (connection, command errors, etc.)
|
|
698
|
-
if (redisError.message.includes('ECONNREFUSED') ||
|
|
766
|
+
if (redisError.message.includes('ECONNREFUSED') ||
|
|
699
767
|
redisError.message.includes('ENOTFOUND') ||
|
|
700
768
|
redisError.message.includes('ETIMEDOUT')) {
|
|
701
769
|
// Connection-related errors - force disconnect to prevent dead connections
|
|
@@ -707,8 +775,8 @@ module.exports = function (RED) {
|
|
|
707
775
|
// Ignore disconnect errors
|
|
708
776
|
}
|
|
709
777
|
}
|
|
710
|
-
|
|
711
|
-
msg.payload = {
|
|
778
|
+
|
|
779
|
+
msg.payload = {
|
|
712
780
|
error: `Redis connection failed: ${redisError.message}`,
|
|
713
781
|
operation: node.operation,
|
|
714
782
|
retryable: true
|
|
@@ -716,28 +784,25 @@ module.exports = function (RED) {
|
|
|
716
784
|
node.status({ fill: "red", shape: "ring", text: "connection failed" });
|
|
717
785
|
} else {
|
|
718
786
|
// Other Redis errors (command errors, etc.)
|
|
719
|
-
msg.payload = {
|
|
787
|
+
msg.payload = {
|
|
720
788
|
error: `Redis operation failed: ${redisError.message}`,
|
|
721
789
|
operation: node.operation,
|
|
722
790
|
retryable: false
|
|
723
791
|
};
|
|
724
792
|
}
|
|
725
|
-
|
|
726
|
-
done();
|
|
793
|
+
sendAndDone(msg);
|
|
727
794
|
return;
|
|
728
795
|
}
|
|
729
796
|
|
|
730
797
|
// Update node status on success
|
|
731
798
|
node.status({ fill: "green", shape: "dot", text: node.operation });
|
|
732
|
-
|
|
733
|
-
done();
|
|
799
|
+
sendAndDone(msg);
|
|
734
800
|
|
|
735
801
|
} catch (error) {
|
|
736
802
|
// Handle general errors (validation, etc.)
|
|
737
803
|
node.error(error.message, msg);
|
|
738
804
|
msg.payload = { error: error.message };
|
|
739
|
-
|
|
740
|
-
done();
|
|
805
|
+
sendAndDone(msg);
|
|
741
806
|
}
|
|
742
807
|
});
|
|
743
808
|
}
|
|
@@ -763,7 +828,7 @@ module.exports = function (RED) {
|
|
|
763
828
|
node.on("close", async (undeploy, done) => {
|
|
764
829
|
node.status({});
|
|
765
830
|
running = false;
|
|
766
|
-
|
|
831
|
+
|
|
767
832
|
if (node.operation === "instance" && node.location && node.topic) {
|
|
768
833
|
try {
|
|
769
834
|
node.context()[node.location].set(node.topic, null);
|
|
@@ -771,7 +836,7 @@ module.exports = function (RED) {
|
|
|
771
836
|
// Ignore errors when cleaning up context
|
|
772
837
|
}
|
|
773
838
|
}
|
|
774
|
-
|
|
839
|
+
|
|
775
840
|
if (client) {
|
|
776
841
|
try {
|
|
777
842
|
if (node.operation === 'subscribe' && node.topic) {
|
|
@@ -788,7 +853,7 @@ module.exports = function (RED) {
|
|
|
788
853
|
const nodeId = node.block ? node.id : redisConfig.id;
|
|
789
854
|
redisConfig.disconnect(nodeId);
|
|
790
855
|
}
|
|
791
|
-
|
|
856
|
+
|
|
792
857
|
client = null;
|
|
793
858
|
done();
|
|
794
859
|
});
|