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 +1 -1
- package/redis-variable-config.js +18 -3
- package/redis-variable.js +83 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-redis-variable",
|
|
3
|
-
"version": "1.5.
|
|
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",
|
package/redis-variable-config.js
CHANGED
|
@@ -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
|
-
//
|
|
389
|
-
const clusterNodes =
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
done();
|
|
366
|
+
sendAndDone(msg);
|
|
355
367
|
return;
|
|
356
368
|
}
|
|
357
369
|
}
|
|
358
370
|
|
|
359
|
-
//
|
|
371
|
+
// Ensure client is connected before running commands
|
|
360
372
|
if (client.status !== 'ready' && client.status !== 'connect') {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
407
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
788
|
-
done();
|
|
835
|
+
sendAndDone(msg);
|
|
789
836
|
}
|
|
790
837
|
});
|
|
791
838
|
}
|