@keyv/redis 5.1.0 → 5.1.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/README.md CHANGED
@@ -38,6 +38,7 @@ Redis storage adapter for [Keyv](https://github.com/jaredwray/keyv).
38
38
  * [Clustering](#clustering)
39
39
  * [Sentinel](#sentinel)
40
40
  * [TLS Support](#tls-support)
41
+ * [Keyv Redis Options](#keyv-redis-options)
41
42
  * [API](#api)
42
43
  * [Using Custom Redis Client Events](#using-custom-redis-client-events)
43
44
  * [Migrating from v3 to v4](#migrating-from-v3-to-v4)
@@ -79,21 +80,20 @@ Here you can pass in the Redis options directly:
79
80
  import Keyv from 'keyv';
80
81
  import KeyvRedis from '@keyv/redis';
81
82
 
82
- const redisOptions = {
83
- url: 'redis://localhost:6379', // The Redis server URL (use 'rediss' for TLS)
84
- password: 'your_password', // Optional password if Redis has authentication enabled
83
+ const uri = "redis://localhost:6379";
85
84
 
86
- socket: {
87
- host: 'localhost', // Hostname of the Redis server
88
- port: 6379, // Port number
89
- reconnectStrategy: (retries) => Math.min(retries * 50, 2000), // Custom reconnect logic
90
-
91
- tls: false, // Enable TLS if you need to connect over SSL
92
- keepAlive: 30000, // Keep-alive timeout (in milliseconds)
93
- }
85
+ // NOTE: please use the settings that you need to configure. Check out Keyv Redis Options section
86
+ const options = {
87
+ namespace: "test",
88
+ keyPrefixSeparator: "->",
89
+ clearBatchSize: 100,
90
+ useUnlink: true,
91
+ noNamespaceAffectsAll: true,
94
92
  };
95
93
 
96
- const keyv = new Keyv(new KeyvRedis(redisOptions));
94
+ const keyvRedis = new KeyvRedis(uri, options);
95
+
96
+ const keyv = new Keyv(keyvRedis);
97
97
  ```
98
98
 
99
99
  Or you can create a new Redis instance and pass it in with `KeyvOptions` such as setting the `store`:
@@ -465,6 +465,60 @@ const tlsOptions = {
465
465
  const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
466
466
  ```
467
467
 
468
+ # Keyv Redis Options
469
+
470
+ Here are all the options that you can set on the constructor
471
+
472
+ ```ts
473
+ export type KeyvRedisOptions = {
474
+ /**
475
+ * Namespace for the current instance.
476
+ */
477
+ namespace?: string;
478
+ /**
479
+ * Separator to use between namespace and key.
480
+ */
481
+ keyPrefixSeparator?: string;
482
+ /**
483
+ * Number of keys to delete in a single batch.
484
+ */
485
+ clearBatchSize?: number;
486
+ /**
487
+ * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
488
+ */
489
+ useUnlink?: boolean;
490
+
491
+ /**
492
+ * Whether to allow clearing all keys when no namespace is set.
493
+ * If set to true and no namespace is set, iterate() will return all keys.
494
+ * Defaults to `false`.
495
+ */
496
+ noNamespaceAffectsAll?: boolean;
497
+
498
+ /**
499
+ * This is used to throw an error if the client is not connected when trying to connect. By default, this is
500
+ * set to true so that it throws an error when trying to connect to the Redis server fails.
501
+ */
502
+ throwOnConnectError?: boolean;
503
+
504
+ /**
505
+ * This is used to throw an error if at any point there is a failure. Use this if you want to
506
+ * ensure that all operations are successful and you want to handle errors. By default, this is
507
+ * set to false so that it does not throw an error on every operation and instead emits an error event
508
+ * and returns no-op responses.
509
+ * @default false
510
+ */
511
+ throwOnErrors?: boolean;
512
+
513
+ /**
514
+ * Timeout in milliseconds for the connection. Default is undefined, which uses the default timeout of the Redis client.
515
+ * If set, it will throw an error if the connection does not succeed within the specified time.
516
+ * @default undefined
517
+ */
518
+ connectionTimeout?: number;
519
+ };
520
+ ```
521
+
468
522
  # API
469
523
  * **constructor([connection], [options])**
470
524
  * **namespace** - The namespace to use for the keys.
@@ -530,14 +584,10 @@ const keyv = new Keyv({ store: redis, namespace: 'my-namespace', useKeyPrefix: f
530
584
 
531
585
  This will make it so the storage adapter `@keyv/redis` will handle the namespace and not the `keyv` instance. If you leave it on it will just look duplicated like `my-namespace:my-namespace:key`.
532
586
 
533
-
534
-
535
587
  # About Redis Sets and its Support in v4
536
588
 
537
589
  We no longer support redis sets. This is due to the fact that it caused significant performance issues and was not a good fit for the library.
538
590
 
539
-
540
-
541
591
  # Using with NestJS
542
592
 
543
593
  > You can integrate `@keyv/redis` with NestJS by creating a custom `CacheModule`. This allows you to use Keyv as a cache store in your application.
package/dist/index.cjs CHANGED
@@ -42,9 +42,9 @@ __export(index_exports, {
42
42
  });
43
43
  module.exports = __toCommonJS(index_exports);
44
44
  var import_client = require("@redis/client");
45
+ var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
45
46
  var import_hookified = require("hookified");
46
47
  var import_keyv = require("keyv");
47
- var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
48
48
  var import_client2 = require("@redis/client");
49
49
  var import_keyv2 = require("keyv");
50
50
  var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
@@ -79,7 +79,10 @@ var KeyvRedis = class extends import_hookified.Hookified {
79
79
  };
80
80
  if (connect) {
81
81
  if (typeof connect === "string") {
82
- this._client = (0, import_client.createClient)({ url: connect, socket });
82
+ this._client = (0, import_client.createClient)({
83
+ url: connect,
84
+ socket
85
+ });
83
86
  } else if (connect.connect !== void 0) {
84
87
  if (this.isClientSentinel(connect)) {
85
88
  this._client = connect;
@@ -90,9 +93,13 @@ var KeyvRedis = class extends import_hookified.Hookified {
90
93
  }
91
94
  } else if (connect instanceof Object) {
92
95
  if (connect.sentinelRootNodes !== void 0) {
93
- this._client = (0, import_client.createSentinel)(connect);
96
+ this._client = (0, import_client.createSentinel)(
97
+ connect
98
+ );
94
99
  } else if (connect.rootNodes === void 0) {
95
- this._client = (0, import_client.createClient)(connect);
100
+ this._client = (0, import_client.createClient)(
101
+ connect
102
+ );
96
103
  } else {
97
104
  this._client = (0, import_client.createCluster)(connect);
98
105
  }
@@ -321,20 +328,50 @@ var KeyvRedis = class extends import_hookified.Hookified {
321
328
  * @param {KeyvEntry[]} entries - the key value pairs to set with optional ttl
322
329
  */
323
330
  async setMany(entries) {
324
- const client = await this.getClient();
325
331
  try {
326
- const multi = client.multi();
327
- for (const { key, value, ttl } of entries) {
328
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
329
- if (ttl) {
330
- multi.set(prefixedKey, value, { PX: ttl });
331
- } else {
332
- multi.set(prefixedKey, value);
332
+ if (this.isCluster()) {
333
+ await this.getClient();
334
+ const slotMap = /* @__PURE__ */ new Map();
335
+ for (const entry of entries) {
336
+ const prefixedKey = this.createKeyPrefix(entry.key, this._namespace);
337
+ const slot = (0, import_cluster_key_slot.default)(prefixedKey);
338
+ const slotEntries = slotMap.get(slot) ?? [];
339
+ slotEntries.push(entry);
340
+ slotMap.set(slot, slotEntries);
333
341
  }
342
+ await Promise.all(
343
+ Array.from(slotMap.entries(), async ([slot, slotEntries]) => {
344
+ const client = await this.getSlotMaster(slot);
345
+ const multi = client.multi();
346
+ for (const { key, value, ttl } of slotEntries) {
347
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
348
+ if (ttl) {
349
+ multi.set(prefixedKey, value, { PX: ttl });
350
+ } else {
351
+ multi.set(prefixedKey, value);
352
+ }
353
+ }
354
+ await multi.exec();
355
+ })
356
+ );
357
+ } else {
358
+ const client = await this.getClient();
359
+ const multi = client.multi();
360
+ for (const { key, value, ttl } of entries) {
361
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
362
+ if (ttl) {
363
+ multi.set(prefixedKey, value, { PX: ttl });
364
+ } else {
365
+ multi.set(prefixedKey, value);
366
+ }
367
+ }
368
+ await multi.exec();
334
369
  }
335
- await multi.exec();
336
370
  } catch (error) {
337
371
  this.emit("error", error);
372
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
373
+ throw error;
374
+ }
338
375
  if (this._throwOnErrors) {
339
376
  throw error;
340
377
  }
@@ -365,17 +402,46 @@ var KeyvRedis = class extends import_hookified.Hookified {
365
402
  * @returns {Promise<Array<boolean>>} - array of booleans for each key if it exists
366
403
  */
367
404
  async hasMany(keys) {
368
- const client = await this.getClient();
369
405
  try {
370
- const multi = client.multi();
371
- for (const key of keys) {
372
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
373
- multi.exists(prefixedKey);
406
+ const prefixedKeys = keys.map(
407
+ (key) => this.createKeyPrefix(key, this._namespace)
408
+ );
409
+ if (this.isCluster()) {
410
+ const slotMap = this.getSlotMap(prefixedKeys);
411
+ const resultMap = /* @__PURE__ */ new Map();
412
+ await Promise.all(
413
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
414
+ const client = await this.getSlotMaster(slot);
415
+ const multi = client.multi();
416
+ for (const key of slotKeys) {
417
+ multi.exists(key);
418
+ }
419
+ const results = await multi.exec();
420
+ for (const [index, result] of results.entries()) {
421
+ resultMap.set(
422
+ slotKeys[index],
423
+ typeof result === "number" && result === 1
424
+ );
425
+ }
426
+ })
427
+ );
428
+ return prefixedKeys.map((key) => resultMap.get(key) ?? false);
429
+ } else {
430
+ const client = await this.getClient();
431
+ const multi = client.multi();
432
+ for (const key of prefixedKeys) {
433
+ multi.exists(key);
434
+ }
435
+ const results = await multi.exec();
436
+ return results.map(
437
+ (result) => typeof result === "number" && result === 1
438
+ );
374
439
  }
375
- const results = await multi.exec();
376
- return results.map((result) => typeof result === "number" && result === 1);
377
440
  } catch (error) {
378
441
  this.emit("error", error);
442
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
443
+ throw error;
444
+ }
379
445
  if (this._throwOnErrors) {
380
446
  throw error;
381
447
  }
@@ -452,25 +518,53 @@ var KeyvRedis = class extends import_hookified.Hookified {
452
518
  */
453
519
  async deleteMany(keys) {
454
520
  let result = false;
455
- const client = await this.getClient();
456
521
  try {
457
- const multi = client.multi();
458
- for (const key of keys) {
459
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
460
- if (this._useUnlink) {
461
- multi.unlink(prefixedKey);
462
- } else {
463
- multi.del(prefixedKey);
522
+ const prefixedKeys = keys.map(
523
+ (key) => this.createKeyPrefix(key, this._namespace)
524
+ );
525
+ if (this.isCluster()) {
526
+ const slotMap = this.getSlotMap(prefixedKeys);
527
+ await Promise.all(
528
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
529
+ const client = await this.getSlotMaster(slot);
530
+ const multi = client.multi();
531
+ for (const key of slotKeys) {
532
+ if (this._useUnlink) {
533
+ multi.unlink(key);
534
+ } else {
535
+ multi.del(key);
536
+ }
537
+ }
538
+ const results = await multi.exec();
539
+ for (const deleted of results) {
540
+ if (typeof deleted === "number" && deleted > 0) {
541
+ result = true;
542
+ }
543
+ }
544
+ })
545
+ );
546
+ } else {
547
+ const client = await this.getClient();
548
+ const multi = client.multi();
549
+ for (const key of prefixedKeys) {
550
+ if (this._useUnlink) {
551
+ multi.unlink(key);
552
+ } else {
553
+ multi.del(key);
554
+ }
464
555
  }
465
- }
466
- const results = await multi.exec();
467
- for (const deleted of results) {
468
- if (typeof deleted === "number" && deleted > 0) {
469
- result = true;
556
+ const results = await multi.exec();
557
+ for (const deleted of results) {
558
+ if (typeof deleted === "number" && deleted > 0) {
559
+ result = true;
560
+ }
470
561
  }
471
562
  }
472
563
  } catch (error) {
473
564
  this.emit("error", error);
565
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
566
+ throw error;
567
+ }
474
568
  if (this._throwOnErrors) {
475
569
  throw error;
476
570
  }
@@ -534,7 +628,9 @@ var KeyvRedis = class extends import_hookified.Hookified {
534
628
  async getMasterNodes() {
535
629
  if (this.isCluster()) {
536
630
  const cluster = await this.getClient();
537
- const nodes = cluster.masters.map(async (main) => cluster.nodeClient(main));
631
+ const nodes = cluster.masters.map(
632
+ async (main) => cluster.nodeClient(main)
633
+ );
538
634
  return Promise.all(nodes);
539
635
  }
540
636
  return [await this.getClient()];
@@ -550,7 +646,10 @@ var KeyvRedis = class extends import_hookified.Hookified {
550
646
  const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
551
647
  let cursor = "0";
552
648
  do {
553
- const result = await client.scan(cursor, { MATCH: match, TYPE: "string" });
649
+ const result = await client.scan(cursor, {
650
+ MATCH: match,
651
+ TYPE: "string"
652
+ });
554
653
  cursor = result.cursor.toString();
555
654
  let { keys } = result;
556
655
  if (!namespace && !this._noNamespaceAffectsAll) {
@@ -577,29 +676,37 @@ var KeyvRedis = class extends import_hookified.Hookified {
577
676
  async clear() {
578
677
  try {
579
678
  const clients = await this.getMasterNodes();
580
- await Promise.all(clients.map(async (client) => {
581
- if (!this._namespace && this._noNamespaceAffectsAll) {
582
- await client.flushDb();
583
- return;
584
- }
585
- let cursor = "0";
586
- const batchSize = this._clearBatchSize;
587
- const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
588
- const deletePromises = [];
589
- do {
590
- const result = await client.scan(cursor, { MATCH: match, COUNT: batchSize, TYPE: "string" });
591
- cursor = result.cursor.toString();
592
- let { keys } = result;
593
- if (keys.length === 0) {
594
- continue;
595
- }
596
- if (!this._namespace) {
597
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
679
+ await Promise.all(
680
+ clients.map(async (client) => {
681
+ if (!this._namespace && this._noNamespaceAffectsAll) {
682
+ await client.flushDb();
683
+ return;
598
684
  }
599
- deletePromises.push(this.clearWithClusterSupport(keys));
600
- } while (cursor !== "0");
601
- await Promise.all(deletePromises);
602
- }));
685
+ let cursor = "0";
686
+ const batchSize = this._clearBatchSize;
687
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
688
+ const deletePromises = [];
689
+ do {
690
+ const result = await client.scan(cursor, {
691
+ MATCH: match,
692
+ COUNT: batchSize,
693
+ TYPE: "string"
694
+ });
695
+ cursor = result.cursor.toString();
696
+ let { keys } = result;
697
+ if (keys.length === 0) {
698
+ continue;
699
+ }
700
+ if (!this._namespace) {
701
+ keys = keys.filter(
702
+ (key) => !key.includes(this._keyPrefixSeparator)
703
+ );
704
+ }
705
+ deletePromises.push(this.clearWithClusterSupport(keys));
706
+ } while (cursor !== "0");
707
+ await Promise.all(deletePromises);
708
+ })
709
+ );
603
710
  } catch (error) {
604
711
  this.emit("error", error);
605
712
  }
@@ -609,15 +716,25 @@ var KeyvRedis = class extends import_hookified.Hookified {
609
716
  * by separating the keys by slot to solve the CROSS-SLOT restriction.
610
717
  */
611
718
  async mget(keys) {
612
- const slotMap = this.getSlotMap(keys);
613
719
  const valueMap = /* @__PURE__ */ new Map();
614
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
615
- const client = await this.getSlotMaster(slot);
616
- const values = await client.mGet(keys2);
720
+ if (this.isCluster()) {
721
+ const slotMap = this.getSlotMap(keys);
722
+ await Promise.all(
723
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
724
+ const client = await this.getSlotMaster(slot);
725
+ const values = await client.mGet(slotKeys);
726
+ for (const [index, value] of values.entries()) {
727
+ valueMap.set(slotKeys[index], value ?? void 0);
728
+ }
729
+ })
730
+ );
731
+ } else {
732
+ const client = await this.getClient();
733
+ const values = await client.mGet(keys);
617
734
  for (const [index, value] of values.entries()) {
618
- valueMap.set(keys2[index], value ?? void 0);
735
+ valueMap.set(keys[index], value ?? void 0);
619
736
  }
620
- }));
737
+ }
621
738
  return keys.map((key) => valueMap.get(key));
622
739
  }
623
740
  /**
@@ -627,10 +744,12 @@ var KeyvRedis = class extends import_hookified.Hookified {
627
744
  async clearWithClusterSupport(keys) {
628
745
  if (keys.length > 0) {
629
746
  const slotMap = this.getSlotMap(keys);
630
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
631
- const client = await this.getSlotMaster(slot);
632
- return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
633
- }));
747
+ await Promise.all(
748
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
749
+ const client = await this.getSlotMaster(slot);
750
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
751
+ })
752
+ );
634
753
  }
635
754
  }
636
755
  /**
@@ -712,15 +831,11 @@ var KeyvRedis = class extends import_hookified.Hookified {
712
831
  });
713
832
  }
714
833
  async createTimeoutPromise(timeoutMs) {
715
- return new Promise((_, reject) => (
716
- // eslint-disable-next-line no-promise-executor-return
717
- setTimeout(
718
- () => {
719
- reject(new Error(`Redis timed out after ${timeoutMs}ms`));
720
- },
721
- timeoutMs
722
- )
723
- ));
834
+ return new Promise(
835
+ (_, reject) => setTimeout(() => {
836
+ reject(new Error(`Redis timed out after ${timeoutMs}ms`));
837
+ }, timeoutMs)
838
+ );
724
839
  }
725
840
  };
726
841
  function createKeyv(connect, options) {
@@ -728,7 +843,10 @@ function createKeyv(connect, options) {
728
843
  const adapter = new KeyvRedis(connect, options);
729
844
  if (options?.namespace) {
730
845
  adapter.namespace = options.namespace;
731
- const keyv2 = new import_keyv.Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
846
+ const keyv2 = new import_keyv.Keyv(adapter, {
847
+ namespace: options?.namespace,
848
+ useKeyPrefix: false
849
+ });
732
850
  if (options?.throwOnConnectError) {
733
851
  keyv2.throwOnErrors = true;
734
852
  }
package/dist/index.d.cts CHANGED
@@ -51,7 +51,7 @@ type KeyvRedisPropertyOptions = KeyvRedisOptions & {
51
51
  /**
52
52
  * Dialect used by the adapter. This is legacy so Keyv knows what is iteratable.
53
53
  */
54
- dialect: 'redis';
54
+ dialect: "redis";
55
55
  /**
56
56
  * URL used to connect to the Redis server. This is legacy so Keyv knows what is iteratable.
57
57
  */
package/dist/index.d.ts CHANGED
@@ -51,7 +51,7 @@ type KeyvRedisPropertyOptions = KeyvRedisOptions & {
51
51
  /**
52
52
  * Dialect used by the adapter. This is legacy so Keyv knows what is iteratable.
53
53
  */
54
- dialect: 'redis';
54
+ dialect: "redis";
55
55
  /**
56
56
  * URL used to connect to the Redis server. This is legacy so Keyv knows what is iteratable.
57
57
  */
package/dist/index.js CHANGED
@@ -4,17 +4,15 @@ import {
4
4
  createCluster,
5
5
  createSentinel
6
6
  } from "@redis/client";
7
+ import calculateSlot from "cluster-key-slot";
7
8
  import { Hookified } from "hookified";
8
9
  import { Keyv } from "keyv";
9
- import calculateSlot from "cluster-key-slot";
10
10
  import {
11
11
  createClient as createClient2,
12
12
  createCluster as createCluster2,
13
13
  createSentinel as createSentinel2
14
14
  } from "@redis/client";
15
- import {
16
- Keyv as Keyv2
17
- } from "keyv";
15
+ import { Keyv as Keyv2 } from "keyv";
18
16
  var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
19
17
  RedisErrorMessages2["RedisClientNotConnectedThrown"] = "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true.";
20
18
  return RedisErrorMessages2;
@@ -47,7 +45,10 @@ var KeyvRedis = class extends Hookified {
47
45
  };
48
46
  if (connect) {
49
47
  if (typeof connect === "string") {
50
- this._client = createClient({ url: connect, socket });
48
+ this._client = createClient({
49
+ url: connect,
50
+ socket
51
+ });
51
52
  } else if (connect.connect !== void 0) {
52
53
  if (this.isClientSentinel(connect)) {
53
54
  this._client = connect;
@@ -58,9 +59,13 @@ var KeyvRedis = class extends Hookified {
58
59
  }
59
60
  } else if (connect instanceof Object) {
60
61
  if (connect.sentinelRootNodes !== void 0) {
61
- this._client = createSentinel(connect);
62
+ this._client = createSentinel(
63
+ connect
64
+ );
62
65
  } else if (connect.rootNodes === void 0) {
63
- this._client = createClient(connect);
66
+ this._client = createClient(
67
+ connect
68
+ );
64
69
  } else {
65
70
  this._client = createCluster(connect);
66
71
  }
@@ -289,20 +294,50 @@ var KeyvRedis = class extends Hookified {
289
294
  * @param {KeyvEntry[]} entries - the key value pairs to set with optional ttl
290
295
  */
291
296
  async setMany(entries) {
292
- const client = await this.getClient();
293
297
  try {
294
- const multi = client.multi();
295
- for (const { key, value, ttl } of entries) {
296
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
297
- if (ttl) {
298
- multi.set(prefixedKey, value, { PX: ttl });
299
- } else {
300
- multi.set(prefixedKey, value);
298
+ if (this.isCluster()) {
299
+ await this.getClient();
300
+ const slotMap = /* @__PURE__ */ new Map();
301
+ for (const entry of entries) {
302
+ const prefixedKey = this.createKeyPrefix(entry.key, this._namespace);
303
+ const slot = calculateSlot(prefixedKey);
304
+ const slotEntries = slotMap.get(slot) ?? [];
305
+ slotEntries.push(entry);
306
+ slotMap.set(slot, slotEntries);
301
307
  }
308
+ await Promise.all(
309
+ Array.from(slotMap.entries(), async ([slot, slotEntries]) => {
310
+ const client = await this.getSlotMaster(slot);
311
+ const multi = client.multi();
312
+ for (const { key, value, ttl } of slotEntries) {
313
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
314
+ if (ttl) {
315
+ multi.set(prefixedKey, value, { PX: ttl });
316
+ } else {
317
+ multi.set(prefixedKey, value);
318
+ }
319
+ }
320
+ await multi.exec();
321
+ })
322
+ );
323
+ } else {
324
+ const client = await this.getClient();
325
+ const multi = client.multi();
326
+ for (const { key, value, ttl } of entries) {
327
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
328
+ if (ttl) {
329
+ multi.set(prefixedKey, value, { PX: ttl });
330
+ } else {
331
+ multi.set(prefixedKey, value);
332
+ }
333
+ }
334
+ await multi.exec();
302
335
  }
303
- await multi.exec();
304
336
  } catch (error) {
305
337
  this.emit("error", error);
338
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
339
+ throw error;
340
+ }
306
341
  if (this._throwOnErrors) {
307
342
  throw error;
308
343
  }
@@ -333,17 +368,46 @@ var KeyvRedis = class extends Hookified {
333
368
  * @returns {Promise<Array<boolean>>} - array of booleans for each key if it exists
334
369
  */
335
370
  async hasMany(keys) {
336
- const client = await this.getClient();
337
371
  try {
338
- const multi = client.multi();
339
- for (const key of keys) {
340
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
341
- multi.exists(prefixedKey);
372
+ const prefixedKeys = keys.map(
373
+ (key) => this.createKeyPrefix(key, this._namespace)
374
+ );
375
+ if (this.isCluster()) {
376
+ const slotMap = this.getSlotMap(prefixedKeys);
377
+ const resultMap = /* @__PURE__ */ new Map();
378
+ await Promise.all(
379
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
380
+ const client = await this.getSlotMaster(slot);
381
+ const multi = client.multi();
382
+ for (const key of slotKeys) {
383
+ multi.exists(key);
384
+ }
385
+ const results = await multi.exec();
386
+ for (const [index, result] of results.entries()) {
387
+ resultMap.set(
388
+ slotKeys[index],
389
+ typeof result === "number" && result === 1
390
+ );
391
+ }
392
+ })
393
+ );
394
+ return prefixedKeys.map((key) => resultMap.get(key) ?? false);
395
+ } else {
396
+ const client = await this.getClient();
397
+ const multi = client.multi();
398
+ for (const key of prefixedKeys) {
399
+ multi.exists(key);
400
+ }
401
+ const results = await multi.exec();
402
+ return results.map(
403
+ (result) => typeof result === "number" && result === 1
404
+ );
342
405
  }
343
- const results = await multi.exec();
344
- return results.map((result) => typeof result === "number" && result === 1);
345
406
  } catch (error) {
346
407
  this.emit("error", error);
408
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
409
+ throw error;
410
+ }
347
411
  if (this._throwOnErrors) {
348
412
  throw error;
349
413
  }
@@ -420,25 +484,53 @@ var KeyvRedis = class extends Hookified {
420
484
  */
421
485
  async deleteMany(keys) {
422
486
  let result = false;
423
- const client = await this.getClient();
424
487
  try {
425
- const multi = client.multi();
426
- for (const key of keys) {
427
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
428
- if (this._useUnlink) {
429
- multi.unlink(prefixedKey);
430
- } else {
431
- multi.del(prefixedKey);
488
+ const prefixedKeys = keys.map(
489
+ (key) => this.createKeyPrefix(key, this._namespace)
490
+ );
491
+ if (this.isCluster()) {
492
+ const slotMap = this.getSlotMap(prefixedKeys);
493
+ await Promise.all(
494
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
495
+ const client = await this.getSlotMaster(slot);
496
+ const multi = client.multi();
497
+ for (const key of slotKeys) {
498
+ if (this._useUnlink) {
499
+ multi.unlink(key);
500
+ } else {
501
+ multi.del(key);
502
+ }
503
+ }
504
+ const results = await multi.exec();
505
+ for (const deleted of results) {
506
+ if (typeof deleted === "number" && deleted > 0) {
507
+ result = true;
508
+ }
509
+ }
510
+ })
511
+ );
512
+ } else {
513
+ const client = await this.getClient();
514
+ const multi = client.multi();
515
+ for (const key of prefixedKeys) {
516
+ if (this._useUnlink) {
517
+ multi.unlink(key);
518
+ } else {
519
+ multi.del(key);
520
+ }
432
521
  }
433
- }
434
- const results = await multi.exec();
435
- for (const deleted of results) {
436
- if (typeof deleted === "number" && deleted > 0) {
437
- result = true;
522
+ const results = await multi.exec();
523
+ for (const deleted of results) {
524
+ if (typeof deleted === "number" && deleted > 0) {
525
+ result = true;
526
+ }
438
527
  }
439
528
  }
440
529
  } catch (error) {
441
530
  this.emit("error", error);
531
+ if (this._throwOnConnectError && error.message === "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true." /* RedisClientNotConnectedThrown */) {
532
+ throw error;
533
+ }
442
534
  if (this._throwOnErrors) {
443
535
  throw error;
444
536
  }
@@ -502,7 +594,9 @@ var KeyvRedis = class extends Hookified {
502
594
  async getMasterNodes() {
503
595
  if (this.isCluster()) {
504
596
  const cluster = await this.getClient();
505
- const nodes = cluster.masters.map(async (main) => cluster.nodeClient(main));
597
+ const nodes = cluster.masters.map(
598
+ async (main) => cluster.nodeClient(main)
599
+ );
506
600
  return Promise.all(nodes);
507
601
  }
508
602
  return [await this.getClient()];
@@ -518,7 +612,10 @@ var KeyvRedis = class extends Hookified {
518
612
  const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
519
613
  let cursor = "0";
520
614
  do {
521
- const result = await client.scan(cursor, { MATCH: match, TYPE: "string" });
615
+ const result = await client.scan(cursor, {
616
+ MATCH: match,
617
+ TYPE: "string"
618
+ });
522
619
  cursor = result.cursor.toString();
523
620
  let { keys } = result;
524
621
  if (!namespace && !this._noNamespaceAffectsAll) {
@@ -545,29 +642,37 @@ var KeyvRedis = class extends Hookified {
545
642
  async clear() {
546
643
  try {
547
644
  const clients = await this.getMasterNodes();
548
- await Promise.all(clients.map(async (client) => {
549
- if (!this._namespace && this._noNamespaceAffectsAll) {
550
- await client.flushDb();
551
- return;
552
- }
553
- let cursor = "0";
554
- const batchSize = this._clearBatchSize;
555
- const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
556
- const deletePromises = [];
557
- do {
558
- const result = await client.scan(cursor, { MATCH: match, COUNT: batchSize, TYPE: "string" });
559
- cursor = result.cursor.toString();
560
- let { keys } = result;
561
- if (keys.length === 0) {
562
- continue;
563
- }
564
- if (!this._namespace) {
565
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
645
+ await Promise.all(
646
+ clients.map(async (client) => {
647
+ if (!this._namespace && this._noNamespaceAffectsAll) {
648
+ await client.flushDb();
649
+ return;
566
650
  }
567
- deletePromises.push(this.clearWithClusterSupport(keys));
568
- } while (cursor !== "0");
569
- await Promise.all(deletePromises);
570
- }));
651
+ let cursor = "0";
652
+ const batchSize = this._clearBatchSize;
653
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
654
+ const deletePromises = [];
655
+ do {
656
+ const result = await client.scan(cursor, {
657
+ MATCH: match,
658
+ COUNT: batchSize,
659
+ TYPE: "string"
660
+ });
661
+ cursor = result.cursor.toString();
662
+ let { keys } = result;
663
+ if (keys.length === 0) {
664
+ continue;
665
+ }
666
+ if (!this._namespace) {
667
+ keys = keys.filter(
668
+ (key) => !key.includes(this._keyPrefixSeparator)
669
+ );
670
+ }
671
+ deletePromises.push(this.clearWithClusterSupport(keys));
672
+ } while (cursor !== "0");
673
+ await Promise.all(deletePromises);
674
+ })
675
+ );
571
676
  } catch (error) {
572
677
  this.emit("error", error);
573
678
  }
@@ -577,15 +682,25 @@ var KeyvRedis = class extends Hookified {
577
682
  * by separating the keys by slot to solve the CROSS-SLOT restriction.
578
683
  */
579
684
  async mget(keys) {
580
- const slotMap = this.getSlotMap(keys);
581
685
  const valueMap = /* @__PURE__ */ new Map();
582
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
583
- const client = await this.getSlotMaster(slot);
584
- const values = await client.mGet(keys2);
686
+ if (this.isCluster()) {
687
+ const slotMap = this.getSlotMap(keys);
688
+ await Promise.all(
689
+ Array.from(slotMap.entries(), async ([slot, slotKeys]) => {
690
+ const client = await this.getSlotMaster(slot);
691
+ const values = await client.mGet(slotKeys);
692
+ for (const [index, value] of values.entries()) {
693
+ valueMap.set(slotKeys[index], value ?? void 0);
694
+ }
695
+ })
696
+ );
697
+ } else {
698
+ const client = await this.getClient();
699
+ const values = await client.mGet(keys);
585
700
  for (const [index, value] of values.entries()) {
586
- valueMap.set(keys2[index], value ?? void 0);
701
+ valueMap.set(keys[index], value ?? void 0);
587
702
  }
588
- }));
703
+ }
589
704
  return keys.map((key) => valueMap.get(key));
590
705
  }
591
706
  /**
@@ -595,10 +710,12 @@ var KeyvRedis = class extends Hookified {
595
710
  async clearWithClusterSupport(keys) {
596
711
  if (keys.length > 0) {
597
712
  const slotMap = this.getSlotMap(keys);
598
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
599
- const client = await this.getSlotMaster(slot);
600
- return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
601
- }));
713
+ await Promise.all(
714
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
715
+ const client = await this.getSlotMaster(slot);
716
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
717
+ })
718
+ );
602
719
  }
603
720
  }
604
721
  /**
@@ -680,15 +797,11 @@ var KeyvRedis = class extends Hookified {
680
797
  });
681
798
  }
682
799
  async createTimeoutPromise(timeoutMs) {
683
- return new Promise((_, reject) => (
684
- // eslint-disable-next-line no-promise-executor-return
685
- setTimeout(
686
- () => {
687
- reject(new Error(`Redis timed out after ${timeoutMs}ms`));
688
- },
689
- timeoutMs
690
- )
691
- ));
800
+ return new Promise(
801
+ (_, reject) => setTimeout(() => {
802
+ reject(new Error(`Redis timed out after ${timeoutMs}ms`));
803
+ }, timeoutMs)
804
+ );
692
805
  }
693
806
  };
694
807
  function createKeyv(connect, options) {
@@ -696,7 +809,10 @@ function createKeyv(connect, options) {
696
809
  const adapter = new KeyvRedis(connect, options);
697
810
  if (options?.namespace) {
698
811
  adapter.namespace = options.namespace;
699
- const keyv2 = new Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
812
+ const keyv2 = new Keyv(adapter, {
813
+ namespace: options?.namespace,
814
+ useKeyPrefix: false
815
+ });
700
816
  if (options?.throwOnConnectError) {
701
817
  keyv2.throwOnErrors = true;
702
818
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/redis",
3
- "version": "5.1.0",
3
+ "version": "5.1.2",
4
4
  "description": "Redis storage adapter for Keyv",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -34,22 +34,22 @@
34
34
  },
35
35
  "homepage": "https://github.com/jaredwray/keyv",
36
36
  "dependencies": {
37
- "@redis/client": "^5.7.0",
37
+ "@redis/client": "^5.8.2",
38
38
  "cluster-key-slot": "^1.1.2",
39
- "hookified": "^1.11.0"
39
+ "hookified": "^1.12.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "keyv": "^5.5.0"
42
+ "keyv": "^5.5.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@faker-js/faker": "^9.9.0",
45
+ "@biomejs/biome": "^2.2.3",
46
+ "@faker-js/faker": "^10.0.0",
46
47
  "@vitest/coverage-v8": "^3.2.4",
47
48
  "rimraf": "^6.0.1",
48
49
  "timekeeper": "^2.3.1",
49
- "tsd": "^0.32.0",
50
+ "tsd": "^0.33.0",
50
51
  "vitest": "^3.2.4",
51
- "xo": "^1.2.1",
52
- "@keyv/test-suite": "^2.1.0"
52
+ "@keyv/test-suite": "^2.1.1"
53
53
  },
54
54
  "tsd": {
55
55
  "directory": "test"
@@ -63,8 +63,8 @@
63
63
  ],
64
64
  "scripts": {
65
65
  "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
66
- "test": "xo --fix && vitest run --coverage",
67
- "test:ci": "xo && vitest --run --sequence.setupFiles=list --coverage",
66
+ "test": "biome check --write && vitest run --coverage",
67
+ "test:ci": "biome check && vitest --run --sequence.setupFiles=list --coverage",
68
68
  "clean": "rimraf ./node_modules ./coverage ./dist"
69
69
  }
70
70
  }