node-red-contrib-redis-variable 1.5.1 → 1.5.3

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.5.1",
3
+ "version": "1.5.3",
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",
@@ -265,6 +265,7 @@ module.exports = function (RED) {
265
265
  retryDelayOnFailover: 100,
266
266
  enableReadyCheck: false,
267
267
  maxRetriesPerRequest: null,
268
+ enableOfflineQueue: false,
268
269
  ...additionalOptions
269
270
  };
270
271
 
@@ -385,9 +386,23 @@ module.exports = function (RED) {
385
386
  // Create Redis client
386
387
  let client;
387
388
  if (this.cluster) {
388
- // For cluster mode, options should be an array of nodes
389
- const clusterNodes = Array.isArray(connectionOptions) ? connectionOptions : [connectionOptions];
390
- client = new Redis.Cluster(clusterNodes);
389
+ // ioredis Cluster: first arg = startup nodes [{ host, port }], second = options with redisOptions
390
+ const clusterNodes = [{ host: options.host, port: parseInt(options.port, 10) }];
391
+ const clusterOptions = {
392
+ enableReadyCheck: false,
393
+ lazyConnect: true,
394
+ maxRetriesPerRequest: 3,
395
+ enableOfflineQueue: false,
396
+ redisOptions: {
397
+ username: options.username || undefined,
398
+ password: options.password || undefined,
399
+ db: 0,
400
+ connectTimeout: connectionOptions.connectTimeout || 5000,
401
+ commandTimeout: connectionOptions.commandTimeout || 3000,
402
+ tls: connectionOptions.tls || undefined,
403
+ },
404
+ };
405
+ client = new Redis.Cluster(clusterNodes, clusterOptions);
391
406
  } else {
392
407
  client = new Redis(connectionOptions);
393
408
  }
package/redis-variable.js CHANGED
@@ -309,6 +309,21 @@ module.exports = function (RED) {
309
309
  send = send || function() { node.send.apply(node, arguments) };
310
310
  done = done || function(err) { if(err) node.error(err, msg); };
311
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
+
312
327
  if (!running) {
313
328
  running = true;
314
329
  }
@@ -317,8 +332,7 @@ module.exports = function (RED) {
317
332
  if (!redisConfig) {
318
333
  node.error("Redis configuration not found", msg);
319
334
  msg.payload = { error: "Redis configuration not found" };
320
- send(msg);
321
- done();
335
+ sendAndDone(msg);
322
336
  return;
323
337
  }
324
338
 
@@ -337,8 +351,7 @@ module.exports = function (RED) {
337
351
  } catch (configError) {
338
352
  node.error(configError.message, msg);
339
353
  msg.payload = { error: configError.message };
340
- send(msg);
341
- done();
354
+ sendAndDone(msg);
342
355
  return;
343
356
  }
344
357
 
@@ -350,43 +363,55 @@ module.exports = function (RED) {
350
363
  node.warn("Redis configuration not available. Operation skipped.");
351
364
  node.status({ fill: "yellow", shape: "ring", text: "no config" });
352
365
  msg.payload = { error: "Redis configuration not available" };
353
- send(msg);
354
- done();
366
+ sendAndDone(msg);
355
367
  return;
356
368
  }
357
369
  }
358
370
 
359
- // Check if client is connected before proceeding
371
+ // Ensure client is connected before running commands
360
372
  if (client.status !== 'ready' && client.status !== 'connect') {
361
- // Try to connect if not connected
362
- if (client.status === 'wait') {
363
- await client.connect();
364
- } else if (client.status === 'disconnect' || client.status === 'end') {
365
- await client.connect();
366
- } else {
367
- // Force disconnect and recreate client for other error states
368
- try {
373
+ const CONNECT_WAIT_MS = 15000;
374
+ const waitForReady = () => new Promise((resolve, reject) => {
375
+ // If connection already established, resolve immediately
376
+ if (client.status === 'ready' || client.status === 'connect') {
377
+ resolve();
378
+ return;
379
+ }
380
+ const timeout = setTimeout(() => reject(new Error('Connection timeout')), CONNECT_WAIT_MS);
381
+ const onReady = () => { clearTimeout(timeout); cleanup(); resolve(); };
382
+ const onError = (e) => { clearTimeout(timeout); cleanup(); reject(e); };
383
+ const cleanup = () => {
384
+ client.removeListener('ready', onReady);
385
+ client.removeListener('connect', onReady);
386
+ client.removeListener('error', onError);
387
+ };
388
+ client.once('ready', onReady);
389
+ client.once('connect', onReady);
390
+ client.once('error', onError);
391
+ });
392
+ try {
393
+ if (client.status === 'connecting') {
394
+ // Already connecting: wait for ready, do not call connect() again
395
+ await waitForReady();
396
+ } else if (typeof client.connect === 'function') {
397
+ await client.connect();
398
+ }
399
+ } catch (connectErr) {
400
+ const msg = connectErr.message || String(connectErr);
401
+ if (msg.includes('already connecting') || msg.includes('already connected')) {
402
+ // Race: another message triggered connect; wait for ready
403
+ await waitForReady();
404
+ } else {
369
405
  redisConfig.forceDisconnect(node.id);
370
406
  client = null;
371
- client = redisConfig.getClient(msg, node, node.id);
372
- if (!client) {
373
- node.warn("Redis configuration not available during reconnection. Operation skipped.");
374
- node.status({ fill: "yellow", shape: "ring", text: "no config" });
375
- msg.payload = { error: "Redis configuration not available" };
376
- send(msg);
377
- done();
378
- return;
379
- }
380
- } catch (reconnectError) {
381
- throw new Error(`Failed to reconnect: ${reconnectError.message}`);
407
+ throw new Error(`Failed to connect: ${msg}`);
382
408
  }
383
409
  }
384
410
  }
385
411
  } catch (err) {
386
412
  node.error(err.message, msg);
387
413
  msg.payload = { error: err.message };
388
- send(msg);
389
- done();
414
+ sendAndDone(msg);
390
415
  return;
391
416
  }
392
417
 
@@ -403,8 +428,33 @@ module.exports = function (RED) {
403
428
  try {
404
429
  switch (node.operation) {
405
430
  case "get":
406
- let getKey = payload.key || payload;
407
- if (!getKey || typeof getKey !== 'string') {
431
+ let getKey;
432
+ if (typeof payload === 'string' && isJsonString(payload)) {
433
+ try {
434
+ const parsed = JSON.parse(payload);
435
+ getKey = parsed.key !== undefined ? parsed.key : parsed.Key;
436
+ } catch (e) {
437
+ getKey = payload;
438
+ }
439
+ } else if (Buffer.isBuffer(payload)) {
440
+ getKey = payload.toString();
441
+ } else if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
442
+ if (payload.key !== undefined || payload.Key !== undefined) {
443
+ getKey = payload.key !== undefined ? payload.key : payload.Key;
444
+ } else if (payload.payload && typeof payload.payload === 'object') {
445
+ const nested = payload.payload;
446
+ if (nested.key !== undefined || nested.Key !== undefined) {
447
+ getKey = nested.key !== undefined ? nested.key : nested.Key;
448
+ }
449
+ }
450
+ } else {
451
+ getKey = payload;
452
+ }
453
+ if (getKey === undefined || getKey === null) {
454
+ throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
455
+ }
456
+ getKey = String(getKey).trim();
457
+ if (!getKey) {
408
458
  throw new Error("Missing or invalid key for GET operation. Use payload.key or payload as string");
409
459
  }
410
460
  response = await client.get(getKey);
@@ -770,22 +820,19 @@ module.exports = function (RED) {
770
820
  retryable: false
771
821
  };
772
822
  }
773
- send(msg);
774
- done();
823
+ sendAndDone(msg);
775
824
  return;
776
825
  }
777
826
 
778
827
  // Update node status on success
779
828
  node.status({ fill: "green", shape: "dot", text: node.operation });
780
- send(msg);
781
- done();
829
+ sendAndDone(msg);
782
830
 
783
831
  } catch (error) {
784
832
  // Handle general errors (validation, etc.)
785
833
  node.error(error.message, msg);
786
834
  msg.payload = { error: error.message };
787
- send(msg);
788
- done();
835
+ sendAndDone(msg);
789
836
  }
790
837
  });
791
838
  }