node-red-contrib-redis-variable 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-redis-variable",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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",
@@ -303,33 +303,76 @@ module.exports = function (RED) {
303
303
 
304
304
  const options = this.getConnectionOptions(msg, executingNode);
305
305
 
306
+ // Add connection limits to prevent infinite retry loops
307
+ const connectionOptions = {
308
+ ...options,
309
+ maxRetriesPerRequest: 1, // Limit retries per request
310
+ retryDelayOnFailover: 100,
311
+ enableReadyCheck: false,
312
+ lazyConnect: true, // Don't connect immediately
313
+ connectTimeout: 5000, // 5 second timeout
314
+ commandTimeout: 3000, // 3 second command timeout
315
+ // Disable automatic reconnection to prevent error loops
316
+ retryDelayOnClusterDown: 0,
317
+ retryDelayOnFailover: 0,
318
+ maxRetriesPerRequest: 0
319
+ };
320
+
306
321
  // Create Redis client
307
322
  let client;
308
323
  if (this.cluster) {
309
324
  // For cluster mode, options should be an array of nodes
310
- const clusterNodes = Array.isArray(options) ? options : [options];
325
+ const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
311
326
  client = new Redis.Cluster(clusterNodes);
312
327
  } else {
313
- client = new Redis(options);
328
+ client = new Redis(connectionOptions);
314
329
  }
315
330
 
331
+ // Track error state to prevent spam
332
+ let errorReported = false;
333
+ let lastErrorTime = 0;
334
+ const ERROR_REPORT_INTERVAL = 30000; // Report errors only once per 30 seconds
335
+
316
336
  // Handle connection errors
317
337
  client.on("error", (e) => {
318
- let errorMsg = `Redis connection error: ${e.message}`;
338
+ const now = Date.now();
319
339
 
320
- // Add specific diagnostics for common SSL issues
321
- if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
322
- errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
323
- errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
324
- errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
325
- errorMsg += "\n3. Check if your Redis server is configured for SSL";
326
- errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
340
+ // Only report errors once per interval to prevent spam
341
+ if (!errorReported || (now - lastErrorTime) > ERROR_REPORT_INTERVAL) {
342
+ let errorMsg = `Redis connection error: ${e.message}`;
343
+
344
+ // Add specific diagnostics for common SSL issues
345
+ if (e.message.includes("Protocol error") || e.message.includes("\\u0015")) {
346
+ errorMsg += "\nThis usually indicates an SSL/TLS configuration issue. Try:";
347
+ errorMsg += "\n1. Disable SSL/TLS if your Redis server doesn't support it";
348
+ errorMsg += "\n2. Use port 6380 for Redis SSL instead of 6379";
349
+ errorMsg += "\n3. Check if your Redis server is configured for SSL";
350
+ errorMsg += "\n4. Enable 'Debug: Force No SSL' option for testing";
351
+ }
352
+
353
+ if (executingNode) {
354
+ executingNode.error(errorMsg, {});
355
+ } else {
356
+ this.error(errorMsg, {});
357
+ }
358
+
359
+ errorReported = true;
360
+ lastErrorTime = now;
327
361
  }
328
-
362
+ });
363
+
364
+ // Handle successful connection
365
+ client.on("connect", () => {
366
+ errorReported = false;
329
367
  if (executingNode) {
330
- executingNode.error(errorMsg, {});
331
- } else {
332
- this.error(errorMsg, {});
368
+ executingNode.status({ fill: "green", shape: "dot", text: "connected" });
369
+ }
370
+ });
371
+
372
+ // Handle disconnection
373
+ client.on("disconnect", () => {
374
+ if (executingNode) {
375
+ executingNode.status({ fill: "red", shape: "ring", text: "disconnected" });
333
376
  }
334
377
  });
335
378
 
@@ -364,6 +407,20 @@ module.exports = function (RED) {
364
407
  }
365
408
  };
366
409
 
410
+ // Force disconnect method for error recovery
411
+ this.forceDisconnect = function(nodeId) {
412
+ const id = nodeId || this.id;
413
+ if (connections[id]) {
414
+ try {
415
+ connections[id].disconnect();
416
+ } catch (e) {
417
+ // Ignore disconnect errors
418
+ }
419
+ delete connections[id];
420
+ delete usedConn[id];
421
+ }
422
+ };
423
+
367
424
  // Clean up on node close
368
425
  this.on('close', function() {
369
426
  this.disconnect();
package/redis-variable.js CHANGED
@@ -283,6 +283,26 @@ module.exports = function (RED) {
283
283
  throw new Error("Failed to initialize Redis client");
284
284
  }
285
285
  }
286
+
287
+ // Check if client is connected before proceeding
288
+ if (client.status !== 'ready' && client.status !== 'connect') {
289
+ // Try to connect if not connected
290
+ if (client.status === 'disconnect' || client.status === 'end') {
291
+ await client.connect();
292
+ } else {
293
+ // Force disconnect and recreate client for other error states
294
+ try {
295
+ redisConfig.forceDisconnect(node.id);
296
+ client = null;
297
+ client = redisConfig.getClient(msg, node, node.id);
298
+ if (!client) {
299
+ throw new Error("Failed to recreate Redis client");
300
+ }
301
+ } catch (reconnectError) {
302
+ throw new Error(`Failed to reconnect: ${reconnectError.message}`);
303
+ }
304
+ }
305
+ }
286
306
  } catch (err) {
287
307
  node.error(err.message, msg);
288
308
  msg.payload = { error: err.message };
@@ -300,330 +320,378 @@ module.exports = function (RED) {
300
320
  throw new Error("Missing payload");
301
321
  }
302
322
 
303
- switch (node.operation) {
304
- case "get":
305
- let getKey = payload.key || payload;
306
- if (!getKey || typeof getKey !== 'string') {
307
- throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
308
- }
309
- response = await client.get(getKey);
310
- msg.payload = smartParse(response);
311
- break;
323
+ // Wrap all Redis operations in try-catch to handle connection errors gracefully
324
+ try {
325
+ switch (node.operation) {
326
+ case "get":
327
+ let getKey = payload.key || payload;
328
+ if (!getKey || typeof getKey !== 'string') {
329
+ throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
330
+ }
331
+ response = await client.get(getKey);
332
+ msg.payload = smartParse(response);
333
+ break;
312
334
 
313
- case "set":
314
- if (!payload.key) {
315
- throw new Error("Missing key for SET operation. Use payload.key");
316
- }
317
- let setValue = payload.value !== undefined ? payload.value : payload.data;
318
- if (setValue === undefined) {
319
- throw new Error("Missing value for SET operation. Use payload.value or payload.data");
320
- }
321
- setValue = smartSerialize(setValue);
322
-
323
- // Support TTL
324
- if (payload.ttl && payload.ttl > 0) {
325
- response = await client.setex(payload.key, payload.ttl, setValue);
326
- } else {
327
- response = await client.set(payload.key, setValue);
328
- }
329
- msg.payload = { success: true, result: response, ttl: payload.ttl || null };
330
- break;
335
+ case "set":
336
+ if (!payload.key) {
337
+ throw new Error("Missing key for SET operation. Use payload.key");
338
+ }
339
+ let setValue = payload.value !== undefined ? payload.value : payload.data;
340
+ if (setValue === undefined) {
341
+ throw new Error("Missing value for SET operation. Use payload.value or payload.data");
342
+ }
343
+ setValue = smartSerialize(setValue);
344
+
345
+ // Support TTL
346
+ if (payload.ttl && payload.ttl > 0) {
347
+ response = await client.setex(payload.key, payload.ttl, setValue);
348
+ } else {
349
+ response = await client.set(payload.key, setValue);
350
+ }
351
+ msg.payload = { success: true, result: response, ttl: payload.ttl || null };
352
+ break;
331
353
 
332
- case "del":
333
- let delKeys = payload.keys || payload.key || payload;
334
- if (!delKeys) {
335
- throw new Error("Missing keys for DEL operation. Use payload.keys (array) or payload.key");
336
- }
337
- let keysToDelete = Array.isArray(delKeys) ? delKeys : [delKeys];
338
- response = await client.del(...keysToDelete);
339
- msg.payload = { success: true, deleted: response, keys: keysToDelete };
340
- break;
341
-
342
- case "exists":
343
- let existsKeys = payload.keys || payload.key || payload;
344
- if (!existsKeys) {
345
- throw new Error("Missing keys for EXISTS operation. Use payload.keys (array) or payload.key");
346
- }
347
- let keysToCheck = Array.isArray(existsKeys) ? existsKeys : [existsKeys];
348
- response = await client.exists(...keysToCheck);
349
- msg.payload = { exists: response > 0, count: response, keys: keysToCheck };
350
- break;
351
-
352
- case "match":
353
- let pattern = payload.pattern || payload;
354
- if (!pattern || typeof pattern !== 'string') {
355
- throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
356
- }
357
-
358
- let count = payload.count || 100; // Default scan count
359
- let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
360
- let allKeys = [];
361
- let cursor = startCursor;
362
- let maxIterations = 1000; // Prevent infinite loops
363
- let iterations = 0;
364
-
365
- // If skip is specified, we need to scan through keys without collecting them
366
- if (payload.skip && payload.skip > 0) {
367
- let skipped = 0;
354
+ case "del":
355
+ let delKeys = payload.keys || payload.key || payload;
356
+ if (!delKeys) {
357
+ throw new Error("Missing keys for DEL operation. Use payload.keys (array) or payload.key");
358
+ }
359
+ let keysToDelete = Array.isArray(delKeys) ? delKeys : [delKeys];
360
+ response = await client.del(...keysToDelete);
361
+ msg.payload = { success: true, deleted: response, keys: keysToDelete };
362
+ break;
363
+
364
+ case "exists":
365
+ let existsKeys = payload.keys || payload.key || payload;
366
+ if (!existsKeys) {
367
+ throw new Error("Missing keys for EXISTS operation. Use payload.keys (array) or payload.key");
368
+ }
369
+ let keysToCheck = Array.isArray(existsKeys) ? existsKeys : [existsKeys];
370
+ response = await client.exists(...keysToCheck);
371
+ msg.payload = { exists: response > 0, count: response, keys: keysToCheck };
372
+ break;
373
+
374
+ case "match":
375
+ let pattern = payload.pattern || payload;
376
+ if (!pattern || typeof pattern !== 'string') {
377
+ throw new Error("Missing pattern for MATCH operation. Use payload.pattern or payload as string");
378
+ }
379
+
380
+ let count = payload.count || 100; // Default scan count
381
+ let startCursor = payload.cursor || payload.skip || 0; // Support cursor/skip for pagination
382
+ let allKeys = [];
383
+ let cursor = startCursor;
384
+ let maxIterations = 1000; // Prevent infinite loops
385
+ let iterations = 0;
386
+
387
+ // If skip is specified, we need to scan through keys without collecting them
388
+ if (payload.skip && payload.skip > 0) {
389
+ let skipped = 0;
390
+ do {
391
+ if (iterations++ > maxIterations) {
392
+ throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce skip value.");
393
+ }
394
+ const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
395
+ cursor = scanResult[0];
396
+ skipped += scanResult[1].length;
397
+
398
+ if (skipped >= payload.skip) {
399
+ // We've skipped enough, start collecting from this point
400
+ const excessSkipped = skipped - payload.skip;
401
+ if (excessSkipped > 0) {
402
+ // Add keys from current batch, excluding the excess
403
+ allKeys = scanResult[1].slice(excessSkipped);
404
+ }
405
+ break;
406
+ }
407
+ } while (cursor !== 0 && cursor !== '0');
408
+ }
409
+
410
+ // Continue scanning to collect keys up to the limit
368
411
  do {
369
412
  if (iterations++ > maxIterations) {
370
- throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce skip value.");
413
+ throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce count value.");
371
414
  }
372
415
  const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
373
416
  cursor = scanResult[0];
374
- skipped += scanResult[1].length;
417
+ allKeys = allKeys.concat(scanResult[1]);
375
418
 
376
- if (skipped >= payload.skip) {
377
- // We've skipped enough, start collecting from this point
378
- const excessSkipped = skipped - payload.skip;
379
- if (excessSkipped > 0) {
380
- // Add keys from current batch, excluding the excess
381
- allKeys = scanResult[1].slice(excessSkipped);
382
- }
419
+ // Break early if we've found enough keys
420
+ if (allKeys.length >= count) {
421
+ allKeys = allKeys.slice(0, count); // Trim to exact count
383
422
  break;
384
423
  }
385
424
  } while (cursor !== 0 && cursor !== '0');
386
- }
387
-
388
- // Continue scanning to collect keys up to the limit
389
- do {
390
- if (iterations++ > maxIterations) {
391
- throw new Error("Maximum scan iterations exceeded. Check your pattern or reduce count value.");
392
- }
393
- const scanResult = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
394
- cursor = scanResult[0];
395
- allKeys = allKeys.concat(scanResult[1]);
396
425
 
397
- // Break early if we've found enough keys
398
- if (allKeys.length >= count) {
399
- allKeys = allKeys.slice(0, count); // Trim to exact count
400
- break;
426
+ msg.payload = {
427
+ pattern: pattern,
428
+ keys: allKeys,
429
+ count: allKeys.length,
430
+ limit: count,
431
+ cursor: cursor, // Return next cursor for pagination
432
+ startCursor: startCursor,
433
+ scanned: true,
434
+ truncated: allKeys.length === count,
435
+ iterations: iterations
436
+ };
437
+ break;
438
+
439
+ // TTL Operations
440
+ case "ttl":
441
+ let ttlKey = payload.key || payload;
442
+ if (!ttlKey || typeof ttlKey !== 'string') {
443
+ throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
401
444
  }
402
- } while (cursor !== 0 && cursor !== '0');
403
-
404
- msg.payload = {
405
- pattern: pattern,
406
- keys: allKeys,
407
- count: allKeys.length,
408
- limit: count,
409
- cursor: cursor, // Return next cursor for pagination
410
- startCursor: startCursor,
411
- scanned: true,
412
- truncated: allKeys.length === count,
413
- iterations: iterations
414
- };
415
- break;
445
+ response = await client.ttl(ttlKey);
446
+ msg.payload = {
447
+ key: ttlKey,
448
+ ttl: response,
449
+ status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
450
+ };
451
+ break;
452
+
453
+ case "expire":
454
+ if (!payload.key) {
455
+ throw new Error("Missing key for EXPIRE operation. Use payload.key");
456
+ }
457
+ let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
458
+ response = await client.expire(payload.key, expireSeconds);
459
+ msg.payload = {
460
+ success: response === 1,
461
+ key: payload.key,
462
+ ttl: expireSeconds,
463
+ message: response === 1 ? "Expiration set" : "Key not found"
464
+ };
465
+ break;
466
+
467
+ case "persist":
468
+ let persistKey = payload.key || payload;
469
+ if (!persistKey || typeof persistKey !== 'string') {
470
+ throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
471
+ }
472
+ response = await client.persist(persistKey);
473
+ msg.payload = {
474
+ success: response === 1,
475
+ key: persistKey,
476
+ message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
477
+ };
478
+ break;
479
+
480
+ // Counter Operations
481
+ case "incr":
482
+ let incrKey = payload.key || payload;
483
+ if (!incrKey || typeof incrKey !== 'string') {
484
+ throw new Error("Missing key for INCR operation. Use payload.key or payload as string");
485
+ }
486
+ response = await client.incr(incrKey);
487
+ msg.payload = { key: incrKey, value: response };
488
+ break;
489
+
490
+ case "decr":
491
+ let decrKey = payload.key || payload;
492
+ if (!decrKey || typeof decrKey !== 'string') {
493
+ throw new Error("Missing key for DECR operation. Use payload.key or payload as string");
494
+ }
495
+ response = await client.decr(decrKey);
496
+ msg.payload = { key: decrKey, value: response };
497
+ break;
416
498
 
417
- // TTL Operations
418
- case "ttl":
419
- let ttlKey = payload.key || payload;
420
- if (!ttlKey || typeof ttlKey !== 'string') {
421
- throw new Error("Missing key for TTL operation. Use payload.key or payload as string");
422
- }
423
- response = await client.ttl(ttlKey);
424
- msg.payload = {
425
- key: ttlKey,
426
- ttl: response,
427
- status: response === -1 ? "no expiration" : response === -2 ? "key not found" : "expires in " + response + " seconds"
428
- };
429
- break;
499
+ case "incrby":
500
+ if (!payload.key) {
501
+ throw new Error("Missing key for INCRBY operation. Use payload.key");
502
+ }
503
+ let incrAmount = payload.amount || payload.value || payload.increment || 1;
504
+ response = await client.incrby(payload.key, incrAmount);
505
+ msg.payload = { key: payload.key, value: response, increment: incrAmount };
506
+ break;
507
+
508
+ case "decrby":
509
+ if (!payload.key) {
510
+ throw new Error("Missing key for DECRBY operation. Use payload.key");
511
+ }
512
+ let decrAmount = payload.amount || payload.value || payload.decrement || 1;
513
+ response = await client.decrby(payload.key, decrAmount);
514
+ msg.payload = { key: payload.key, value: response, decrement: decrAmount };
515
+ break;
516
+
517
+ // List Operations
518
+ case "lpush":
519
+ if (!payload.key) {
520
+ throw new Error("Missing key for LPUSH operation. Use payload.key");
521
+ }
522
+ let lpushValue = payload.value !== undefined ? payload.value : payload.data;
523
+ if (lpushValue === undefined) {
524
+ throw new Error("Missing value for LPUSH operation. Use payload.value or payload.data");
525
+ }
526
+ lpushValue = smartSerialize(lpushValue);
527
+ response = await client.lpush(payload.key, lpushValue);
528
+ msg.payload = { success: true, result: response, key: payload.key };
529
+ break;
530
+
531
+ case "rpush":
532
+ if (!payload.key) {
533
+ throw new Error("Missing key for RPUSH operation. Use payload.key");
534
+ }
535
+ let rpushValue = payload.value !== undefined ? payload.value : payload.data;
536
+ if (rpushValue === undefined) {
537
+ throw new Error("Missing value for RPUSH operation. Use payload.value or payload.data");
538
+ }
539
+ rpushValue = smartSerialize(rpushValue);
540
+ response = await client.rpush(payload.key, rpushValue);
541
+ msg.payload = { success: true, result: response, key: payload.key };
542
+ break;
543
+
544
+ case "lpop":
545
+ let lpopKey = payload.key || payload;
546
+ if (!lpopKey || typeof lpopKey !== 'string') {
547
+ throw new Error("Missing key for LPOP operation. Use payload.key or payload as string");
548
+ }
549
+ response = await client.lpop(lpopKey);
550
+ msg.payload = smartParse(response);
551
+ break;
552
+
553
+ case "rpop":
554
+ let rpopKey = payload.key || payload;
555
+ if (!rpopKey || typeof rpopKey !== 'string') {
556
+ throw new Error("Missing key for RPOP operation. Use payload.key or payload as string");
557
+ }
558
+ response = await client.rpop(rpopKey);
559
+ msg.payload = smartParse(response);
560
+ break;
561
+
562
+ case "lrange":
563
+ if (!payload.key) {
564
+ throw new Error("Missing key for LRANGE operation. Use payload.key");
565
+ }
566
+ let start = payload.start || payload.from || 0;
567
+ let stop = payload.stop || payload.to || -1;
568
+ response = await client.lrange(payload.key, start, stop);
569
+ msg.payload = response.map(item => smartParse(item));
570
+ break;
571
+
572
+ case "llen":
573
+ let llenKey = payload.key || payload;
574
+ if (!llenKey || typeof llenKey !== 'string') {
575
+ throw new Error("Missing key for LLEN operation. Use payload.key or payload as string");
576
+ }
577
+ response = await client.llen(llenKey);
578
+ msg.payload = { key: llenKey, length: response };
579
+ break;
580
+
581
+ // Hash Operations
582
+ case "hset":
583
+ if (!payload.key) {
584
+ throw new Error("Missing key for HSET operation. Use payload.key");
585
+ }
586
+ let hsetField = payload.field || payload.name;
587
+ let hsetValue = payload.value !== undefined ? payload.value : payload.data;
588
+ if (!hsetField || hsetValue === undefined) {
589
+ throw new Error("Missing field or value for HSET operation. Use payload.field and payload.value");
590
+ }
591
+ hsetValue = smartSerialize(hsetValue);
592
+ response = await client.hset(payload.key, hsetField, hsetValue);
593
+ msg.payload = { success: true, result: response, key: payload.key, field: hsetField };
594
+ break;
595
+
596
+ case "hget":
597
+ if (!payload.key) {
598
+ throw new Error("Missing key for HGET operation. Use payload.key");
599
+ }
600
+ let hgetField = payload.field || payload.name;
601
+ if (!hgetField) {
602
+ throw new Error("Missing field for HGET operation. Use payload.field");
603
+ }
604
+ response = await client.hget(payload.key, hgetField);
605
+ msg.payload = smartParse(response);
606
+ break;
607
+
608
+ case "hgetall":
609
+ let hgetallKey = payload.key || payload;
610
+ if (!hgetallKey || typeof hgetallKey !== 'string') {
611
+ throw new Error("Missing key for HGETALL operation. Use payload.key or payload as string");
612
+ }
613
+ response = await client.hgetall(hgetallKey);
614
+ // Parse all values in the hash
615
+ const parsedHash = {};
616
+ for (const [field, value] of Object.entries(response)) {
617
+ parsedHash[field] = smartParse(value);
618
+ }
619
+ msg.payload = parsedHash;
620
+ break;
621
+
622
+ case "hdel":
623
+ if (!payload.key) {
624
+ throw new Error("Missing key for HDEL operation. Use payload.key");
625
+ }
626
+ let hdelFields = payload.fields || payload.field || payload.names;
627
+ if (!hdelFields) {
628
+ throw new Error("Missing fields for HDEL operation. Use payload.fields (array) or payload.field");
629
+ }
630
+ let fieldsToDelete = Array.isArray(hdelFields) ? hdelFields : [hdelFields];
631
+ response = await client.hdel(payload.key, ...fieldsToDelete);
632
+ msg.payload = { success: true, deleted: response, key: payload.key, fields: fieldsToDelete };
633
+ break;
634
+
635
+ // Pub/Sub Operations
636
+ case "publish":
637
+ if (!payload.channel) {
638
+ throw new Error("Missing channel for PUBLISH operation. Use payload.channel");
639
+ }
640
+ let publishMessage = payload.message !== undefined ? payload.message : payload.data;
641
+ if (publishMessage === undefined) {
642
+ throw new Error("Missing message for PUBLISH operation. Use payload.message or payload.data");
643
+ }
644
+ publishMessage = smartSerialize(publishMessage);
645
+ response = await client.publish(payload.channel, publishMessage);
646
+ msg.payload = { success: true, subscribers: response, channel: payload.channel };
647
+ break;
430
648
 
431
- case "expire":
432
- if (!payload.key) {
433
- throw new Error("Missing key for EXPIRE operation. Use payload.key");
649
+ default:
650
+ throw new Error(`Unknown operation: ${node.operation}`);
651
+ }
652
+ } catch (redisError) {
653
+ // Handle Redis-specific errors (connection, command errors, etc.)
654
+ if (redisError.message.includes('ECONNREFUSED') ||
655
+ redisError.message.includes('ENOTFOUND') ||
656
+ redisError.message.includes('ETIMEDOUT')) {
657
+ // Connection-related errors - force disconnect to prevent dead connections
658
+ if (client) {
659
+ try {
660
+ redisConfig.forceDisconnect(node.id);
661
+ client = null;
662
+ } catch (e) {
663
+ // Ignore disconnect errors
664
+ }
434
665
  }
435
- let expireSeconds = payload.ttl || payload.seconds || payload.value || 3600;
436
- response = await client.expire(payload.key, expireSeconds);
666
+
437
667
  msg.payload = {
438
- success: response === 1,
439
- key: payload.key,
440
- ttl: expireSeconds,
441
- message: response === 1 ? "Expiration set" : "Key not found"
668
+ error: `Redis connection failed: ${redisError.message}`,
669
+ operation: node.operation,
670
+ retryable: true
442
671
  };
443
- break;
444
-
445
- case "persist":
446
- let persistKey = payload.key || payload;
447
- if (!persistKey || typeof persistKey !== 'string') {
448
- throw new Error("Missing key for PERSIST operation. Use payload.key or payload as string");
449
- }
450
- response = await client.persist(persistKey);
672
+ node.status({ fill: "red", shape: "ring", text: "connection failed" });
673
+ } else {
674
+ // Other Redis errors (command errors, etc.)
451
675
  msg.payload = {
452
- success: response === 1,
453
- key: persistKey,
454
- message: response === 1 ? "Expiration removed" : "Key not found or no expiration"
676
+ error: `Redis operation failed: ${redisError.message}`,
677
+ operation: node.operation,
678
+ retryable: false
455
679
  };
456
- break;
457
-
458
- // Counter Operations
459
- case "incr":
460
- let incrKey = payload.key || payload;
461
- if (!incrKey || typeof incrKey !== 'string') {
462
- throw new Error("Missing key for INCR operation. Use payload.key or payload as string");
463
- }
464
- response = await client.incr(incrKey);
465
- msg.payload = { key: incrKey, value: response };
466
- break;
467
-
468
- case "decr":
469
- let decrKey = payload.key || payload;
470
- if (!decrKey || typeof decrKey !== 'string') {
471
- throw new Error("Missing key for DECR operation. Use payload.key or payload as string");
472
- }
473
- response = await client.decr(decrKey);
474
- msg.payload = { key: decrKey, value: response };
475
- break;
476
-
477
- case "incrby":
478
- if (!payload.key) {
479
- throw new Error("Missing key for INCRBY operation. Use payload.key");
480
- }
481
- let incrAmount = payload.amount || payload.value || payload.increment || 1;
482
- response = await client.incrby(payload.key, incrAmount);
483
- msg.payload = { key: payload.key, value: response, increment: incrAmount };
484
- break;
485
-
486
- case "decrby":
487
- if (!payload.key) {
488
- throw new Error("Missing key for DECRBY operation. Use payload.key");
489
- }
490
- let decrAmount = payload.amount || payload.value || payload.decrement || 1;
491
- response = await client.decrby(payload.key, decrAmount);
492
- msg.payload = { key: payload.key, value: response, decrement: decrAmount };
493
- break;
494
-
495
- // List Operations
496
- case "lpush":
497
- case "rpush":
498
- if (!payload.key) {
499
- throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key`);
500
- }
501
- let pushValue = payload.value !== undefined ? payload.value : payload.data;
502
- if (pushValue === undefined) {
503
- throw new Error(`Missing value for ${node.operation.toUpperCase()} operation. Use payload.value or payload.data`);
504
- }
505
- pushValue = smartSerialize(pushValue);
506
- response = await client[node.operation](payload.key, pushValue);
507
- msg.payload = { success: true, key: payload.key, length: response, operation: node.operation };
508
- break;
509
-
510
- case "lpop":
511
- case "rpop":
512
- let popKey = payload.key || payload;
513
- if (!popKey || typeof popKey !== 'string') {
514
- throw new Error(`Missing key for ${node.operation.toUpperCase()} operation. Use payload.key or payload as string`);
515
- }
516
- response = await client[node.operation](popKey);
517
- msg.payload = smartParse(response);
518
- break;
519
-
520
- case "llen":
521
- let llenKey = payload.key || payload;
522
- if (!llenKey || typeof llenKey !== 'string') {
523
- throw new Error("Missing key for LLEN operation. Use payload.key or payload as string");
524
- }
525
- response = await client.llen(llenKey);
526
- msg.payload = { key: llenKey, length: response };
527
- break;
528
-
529
- case "lrange":
530
- if (!payload.key) {
531
- throw new Error("Missing key for LRANGE operation. Use payload.key");
532
- }
533
- let start = payload.start !== undefined ? payload.start : 0;
534
- let stop = payload.stop !== undefined ? payload.stop : -1;
535
- response = await client.lrange(payload.key, start, stop);
536
- // Auto-parse each item in the array
537
- response = response.map(item => smartParse(item));
538
- msg.payload = { key: payload.key, range: { start, stop }, values: response, count: response.length };
539
- break;
540
-
541
- // Hash Operations
542
- case "hset":
543
- if (!payload.key) {
544
- throw new Error("Missing key for HSET operation. Use payload.key");
545
- }
546
- if (payload.field && payload.value !== undefined) {
547
- // Single field
548
- let hashValue = smartSerialize(payload.value);
549
- response = await client.hset(payload.key, payload.field, hashValue);
550
- msg.payload = { success: true, key: payload.key, field: payload.field, created: response === 1 };
551
- } else if (payload.fields && typeof payload.fields === 'object') {
552
- // Multiple fields from payload.fields
553
- const fields = {};
554
- for (const [key, value] of Object.entries(payload.fields)) {
555
- fields[key] = smartSerialize(value);
556
- }
557
- response = await client.hset(payload.key, fields);
558
- msg.payload = { success: true, key: payload.key, fields: Object.keys(fields), created: response };
559
- } else {
560
- throw new Error("HSET requires field and value (payload.field, payload.value) or multiple fields (payload.fields)");
561
- }
562
- break;
563
-
564
- case "hget":
565
- if (!payload.key) {
566
- throw new Error("Missing key for HGET operation. Use payload.key");
567
- }
568
- let field = payload.field;
569
- if (!field) {
570
- throw new Error("Missing field for HGET operation. Use payload.field");
571
- }
572
- response = await client.hget(payload.key, field);
573
- msg.payload = smartParse(response);
574
- break;
575
-
576
- case "hgetall":
577
- let hgetallKey = payload.key || payload;
578
- if (!hgetallKey || typeof hgetallKey !== 'string') {
579
- throw new Error("Missing key for HGETALL operation. Use payload.key or payload as string");
580
- }
581
- response = await client.hgetall(hgetallKey);
582
- // Auto-parse each field value
583
- const parsed = {};
584
- for (const [key, value] of Object.entries(response)) {
585
- parsed[key] = smartParse(value);
586
- }
587
- msg.payload = parsed;
588
- break;
589
-
590
- case "hdel":
591
- if (!payload.key) {
592
- throw new Error("Missing key for HDEL operation. Use payload.key");
593
- }
594
- let fieldsToDelete = payload.fields || payload.field;
595
- if (!fieldsToDelete) {
596
- throw new Error("Missing fields for HDEL operation. Use payload.fields (array) or payload.field");
597
- }
598
- fieldsToDelete = Array.isArray(fieldsToDelete) ? fieldsToDelete : [fieldsToDelete];
599
- response = await client.hdel(payload.key, ...fieldsToDelete);
600
- msg.payload = { success: true, key: payload.key, deleted: response, fields: fieldsToDelete };
601
- break;
602
-
603
- case "publish":
604
- let channel = payload.channel || payload.key;
605
- if (!channel) {
606
- throw new Error("Missing channel for PUBLISH operation. Use payload.channel or payload.key");
607
- }
608
- let pubValue = payload.message || payload.value || payload.data;
609
- if (pubValue === undefined) {
610
- throw new Error("Missing message for PUBLISH operation. Use payload.message, payload.value, or payload.data");
611
- }
612
- pubValue = smartSerialize(pubValue);
613
- response = await client.publish(channel, pubValue);
614
- msg.payload = { success: true, channel: channel, subscribers: response, message: pubValue };
615
- break;
616
-
617
- default:
618
- throw new Error(`Unsupported operation: ${node.operation}`);
680
+ }
681
+ send(msg);
682
+ done();
683
+ return;
619
684
  }
620
685
 
686
+ // Update node status on success
687
+ node.status({ fill: "green", shape: "dot", text: node.operation });
621
688
  send(msg);
622
689
  done();
623
690
 
624
- } catch (err) {
625
- node.error(err.message, msg);
626
- msg.payload = { error: err.message };
691
+ } catch (error) {
692
+ // Handle general errors (validation, etc.)
693
+ node.error(error.message, msg);
694
+ msg.payload = { error: error.message };
627
695
  send(msg);
628
696
  done();
629
697
  }