@keyv/redis 5.0.0 → 5.1.1

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
@@ -35,11 +35,15 @@ Redis storage adapter for [Keyv](https://github.com/jaredwray/keyv).
35
35
  * [High Memory Usage on Redis Server](#high-memory-usage-on-redis-server)
36
36
  * [Gracefully Handling Errors and Timeouts](#gracefully-handling-errors-and-timeouts)
37
37
  * [Using Cacheable with Redis](#using-cacheable-with-redis)
38
- * [Clustering and TLS Support](#clustering-and-tls-support)
38
+ * [Clustering](#clustering)
39
+ * [Sentinel](#sentinel)
40
+ * [TLS Support](#tls-support)
41
+ * [Keyv Redis Options](#keyv-redis-options)
39
42
  * [API](#api)
40
43
  * [Using Custom Redis Client Events](#using-custom-redis-client-events)
41
44
  * [Migrating from v3 to v4](#migrating-from-v3-to-v4)
42
45
  * [About Redis Sets and its Support in v4](#about-redis-sets-and-its-support-in-v4)
46
+ * [Using with NestJS](#using-with-nestjs)
43
47
  * [License](#license)
44
48
 
45
49
  # Installation
@@ -76,21 +80,20 @@ Here you can pass in the Redis options directly:
76
80
  import Keyv from 'keyv';
77
81
  import KeyvRedis from '@keyv/redis';
78
82
 
79
- const redisOptions = {
80
- url: 'redis://localhost:6379', // The Redis server URL (use 'rediss' for TLS)
81
- password: 'your_password', // Optional password if Redis has authentication enabled
83
+ const uri = "redis://localhost:6379";
82
84
 
83
- socket: {
84
- host: 'localhost', // Hostname of the Redis server
85
- port: 6379, // Port number
86
- reconnectStrategy: (retries) => Math.min(retries * 50, 2000), // Custom reconnect logic
87
-
88
- tls: false, // Enable TLS if you need to connect over SSL
89
- keepAlive: 30000, // Keep-alive timeout (in milliseconds)
90
- }
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,
91
92
  };
92
93
 
93
- const keyv = new Keyv(new KeyvRedis(redisOptions));
94
+ const keyvRedis = new KeyvRedis(uri, options);
95
+
96
+ const keyv = new Keyv(keyvRedis);
94
97
  ```
95
98
 
96
99
  Or you can create a new Redis instance and pass it in with `KeyvOptions` such as setting the `store`:
@@ -379,9 +382,9 @@ const cache = new Cacheable( { secondary, nonBlocking: true } );
379
382
 
380
383
  This will make it so that the secondary does not block the primary cache and will be very fast. 🚀
381
384
 
382
- # Clustering and TLS Support
385
+ # Clustering
383
386
 
384
- If you are using a Redis Cluster or need to use TLS, you can pass in the `redisOptions` directly. Here is an example of how to do that:
387
+ If you are using a Redis Cluster, you can pass in the `redisOptions` directly. Here is an example of how to do that:
385
388
 
386
389
  ```js
387
390
  import Keyv from 'keyv';
@@ -404,9 +407,42 @@ const cluster = createCluster({
404
407
  const keyv = new Keyv({ store: new KeyvRedis(cluster) });
405
408
  ```
406
409
 
407
- You can learn more about the `createCluster` function in the [documentation](https://github.com/redis/node-redis/blob/master/docs/clustering.md) at https://github.com/redis/node-redis/tree/master/docs.
410
+ You can learn more about the `createCluster` function in the [documentation](https://github.com/redis/node-redis/blob/master/docs/clustering.md) at https://github.com/redis/node-redis/tree/master/docs.
408
411
 
409
- Here is an example of how to use TLS:
412
+ # Sentinel
413
+
414
+ If you are using Sentinel to provide high availability for your Redis instances, you can pass in the `redisOptions` directly. Here is an example of how to do that:
415
+
416
+ ```js
417
+ import Keyv from 'keyv';
418
+ import KeyvRedis, { createSentinel } from '@keyv/redis';
419
+
420
+ const sentinel = createSentinel({
421
+ name: 'sentinel-db',
422
+ sentinelRootNodes: [
423
+ {
424
+ host: '127.0.0.1',
425
+ port: 26379,
426
+ },
427
+ {
428
+ host: '127.0.0.1',
429
+ port: 26380,
430
+ },
431
+ {
432
+ host: '127.0.0.1',
433
+ port: 26381,
434
+ },
435
+ ],
436
+ });
437
+
438
+ const keyv = new Keyv({ store: new KeyvRedis(sentinel) });
439
+ ```
440
+
441
+ You can learn more about the `createSentinel` function in the [documentation](https://github.com/redis/node-redis/blob/master/docs/sentinel.md) at https://github.com/redis/node-redis/tree/master/docs.
442
+
443
+ # TLS Support
444
+
445
+ Here is an example of how to use TLS using the `redisOptions`:
410
446
 
411
447
  ```js
412
448
  import Keyv from 'keyv';
@@ -429,6 +465,60 @@ const tlsOptions = {
429
465
  const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
430
466
  ```
431
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
+
432
522
  # API
433
523
  * **constructor([connection], [options])**
434
524
  * **namespace** - The namespace to use for the keys.
@@ -494,12 +584,167 @@ const keyv = new Keyv({ store: redis, namespace: 'my-namespace', useKeyPrefix: f
494
584
 
495
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`.
496
586
 
497
-
498
-
499
587
  # About Redis Sets and its Support in v4
500
588
 
501
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.
502
590
 
591
+ # Using with NestJS
592
+
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.
594
+
595
+ ### 1. Install Dependencies
596
+
597
+ ```bash
598
+ npm install @keyv/redis keyv @nestjs/cache-manager cache-manager cacheable
599
+ ```
600
+
601
+ ### 2. Create a Cache Module
602
+
603
+ Create a file `cache.module.ts`:
604
+
605
+ ```ts
606
+ import { Module } from '@nestjs/common';
607
+ import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
608
+ import { createKeyv } from '@keyv/redis';
609
+
610
+ @Module({
611
+ imports: [
612
+ NestCacheModule.registerAsync({
613
+ useFactory: () => ({
614
+ stores: [createKeyv('redis://localhost:6379')],
615
+ }),
616
+ }),
617
+ ],
618
+ providers: [],
619
+ exports: [],
620
+ })
621
+ export class CacheModule {}
622
+ ```
623
+
624
+ ### 3. Import the Cache Module in AppModule
625
+ Update `app.module.ts`:
626
+
627
+ ```ts
628
+ import { Module } from '@nestjs/common';
629
+ import { CacheModule } from './modules/config/cache/cache.module';
630
+
631
+ @Module({
632
+ imports: [
633
+ CacheModule, // Import your custom cache module
634
+ // other modules...
635
+ ],
636
+ })
637
+ export class AppModule {}
638
+ ```
639
+
640
+ ### 4. Create the Cache Service
641
+ Create a file `cache.service.ts`:
642
+
643
+ ```ts
644
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
645
+ import { Inject, Injectable } from '@nestjs/common';
646
+ import { Cache } from 'cache-manager';
647
+
648
+ @Injectable()
649
+ export class CacheService {
650
+ constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
651
+
652
+ async get<T>(key: string): Promise<T | undefined> {
653
+ return this.cacheManager.get<T>(key);
654
+ }
655
+
656
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
657
+ await this.cacheManager.set(key, value, ttl);
658
+ }
659
+
660
+ async delete(key: string): Promise<void> {
661
+ await this.cacheManager.del(key);
662
+ }
663
+ }
664
+ ```
665
+
666
+ ### 5. Register CacheService in CacheModule
667
+ Update `cache.module.ts`:
668
+
669
+ ```ts
670
+ import { Module } from '@nestjs/common';
671
+ import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
672
+ import { createKeyv } from '@keyv/redis';
673
+ import { CacheService } from './services/cache.service';
674
+
675
+ @Module({
676
+ imports: [
677
+ NestCacheModule.registerAsync({
678
+ useFactory: () => ({
679
+ stores: [createKeyv('redis://localhost:6379')],
680
+ }),
681
+ }),
682
+ ],
683
+ providers: [CacheService],
684
+ exports: [CacheService],
685
+ })
686
+ export class CacheModule {}
687
+ ```
688
+ ### 6. Import CacheModule in the Target Module (e.g. TaskModule)
689
+ ```ts
690
+ import { Module } from '@nestjs/common';
691
+ import { TaskService } from './task.service';
692
+ import { TaskRepository } from './repositories/task.repository';
693
+ import { CacheModule } from 'src/modules/config/cache/cache.module';
694
+
695
+ @Module({
696
+ imports: [CacheModule],
697
+ providers: [TaskService, TaskRepository],
698
+ })
699
+ export class TaskModule {}
700
+ ```
701
+
702
+ ### 7. Using the Cache in a Service
703
+
704
+ ```ts
705
+ import { Injectable, NotFoundException } from '@nestjs/common';
706
+ import { TaskRepository } from '../repositories/task.repository';
707
+ import { TaskDto } from '../dto/task.dto';
708
+ import { CacheService } from 'src/modules/config/cache/services/cache.service';
709
+
710
+ @Injectable()
711
+ export class TaskService {
712
+ constructor(
713
+ private readonly taskRepository: TaskRepository,
714
+ private readonly cache: CacheService, // Inject the CacheService
715
+ ) {}
716
+
717
+ async findById(id: number): Promise<TaskDto> {
718
+ const cacheKey = `task:${id}`;
719
+
720
+ // 1. Try to get from cache
721
+ const cached = await this.cache.get<TaskDto>(cacheKey);
722
+
723
+ if (cached) {
724
+ return cached;
725
+ }
726
+
727
+ // 2. If not found in cache, fetch from database
728
+ const task = await this.taskRepository.findById(id);
729
+
730
+ if (!task) {
731
+ throw new NotFoundException('Task not found');
732
+ }
733
+
734
+ // 3. Set in cache for future requests
735
+ await this.cache.set(cacheKey, task, 300 * 1000); // 5 minutes TTL
736
+ return task;
737
+ }
738
+ }
739
+ ```
740
+
741
+
742
+ You can learn more about caching in NestJS in the [official documentation](https://docs.nestjs.com/techniques/caching#in-memory-cache).
743
+
744
+
745
+ ---
746
+
747
+
503
748
  # License
504
749
 
505
750
  [MIT © Jared Wray](LISCENCE)
package/dist/index.cjs CHANGED
@@ -36,14 +36,15 @@ __export(index_exports, {
36
36
  createCluster: () => import_client2.createCluster,
37
37
  createKeyv: () => createKeyv,
38
38
  createKeyvNonBlocking: () => createKeyvNonBlocking,
39
+ createSentinel: () => import_client2.createSentinel,
39
40
  default: () => KeyvRedis,
40
41
  defaultReconnectStrategy: () => defaultReconnectStrategy
41
42
  });
42
43
  module.exports = __toCommonJS(index_exports);
43
44
  var import_client = require("@redis/client");
45
+ var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
44
46
  var import_hookified = require("hookified");
45
47
  var import_keyv = require("keyv");
46
- var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
47
48
  var import_client2 = require("@redis/client");
48
49
  var import_keyv2 = require("keyv");
49
50
  var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
@@ -78,11 +79,30 @@ var KeyvRedis = class extends import_hookified.Hookified {
78
79
  };
79
80
  if (connect) {
80
81
  if (typeof connect === "string") {
81
- this._client = (0, import_client.createClient)({ url: connect, socket });
82
+ this._client = (0, import_client.createClient)({
83
+ url: connect,
84
+ socket
85
+ });
82
86
  } else if (connect.connect !== void 0) {
83
- this._client = this.isClientCluster(connect) ? connect : connect;
87
+ if (this.isClientSentinel(connect)) {
88
+ this._client = connect;
89
+ } else if (this.isClientCluster(connect)) {
90
+ this._client = connect;
91
+ } else {
92
+ this._client = connect;
93
+ }
84
94
  } else if (connect instanceof Object) {
85
- this._client = connect.rootNodes === void 0 ? (0, import_client.createClient)(connect) : (0, import_client.createCluster)(connect);
95
+ if (connect.sentinelRootNodes !== void 0) {
96
+ this._client = (0, import_client.createSentinel)(
97
+ connect
98
+ );
99
+ } else if (connect.rootNodes === void 0) {
100
+ this._client = (0, import_client.createClient)(
101
+ connect
102
+ );
103
+ } else {
104
+ this._client = (0, import_client.createCluster)(connect);
105
+ }
86
106
  }
87
107
  }
88
108
  this.setOptions(options);
@@ -360,7 +380,9 @@ var KeyvRedis = class extends import_hookified.Hookified {
360
380
  multi.exists(prefixedKey);
361
381
  }
362
382
  const results = await multi.exec();
363
- return results.map((result) => typeof result === "number" && result === 1);
383
+ return results.map(
384
+ (result) => typeof result === "number" && result === 1
385
+ );
364
386
  } catch (error) {
365
387
  this.emit("error", error);
366
388
  if (this._throwOnErrors) {
@@ -506,6 +528,13 @@ var KeyvRedis = class extends import_hookified.Hookified {
506
528
  isCluster() {
507
529
  return this.isClientCluster(this._client);
508
530
  }
531
+ /**
532
+ * Is the client a sentinel.
533
+ * @returns {boolean} - true if the client is a sentinel, false if not
534
+ */
535
+ isSentinel() {
536
+ return this.isClientSentinel(this._client);
537
+ }
509
538
  /**
510
539
  * Get the master nodes in the cluster. If not a cluster, it will return the single client.
511
540
  *
@@ -514,7 +543,9 @@ var KeyvRedis = class extends import_hookified.Hookified {
514
543
  async getMasterNodes() {
515
544
  if (this.isCluster()) {
516
545
  const cluster = await this.getClient();
517
- const nodes = cluster.masters.map(async (main) => cluster.nodeClient(main));
546
+ const nodes = cluster.masters.map(
547
+ async (main) => cluster.nodeClient(main)
548
+ );
518
549
  return Promise.all(nodes);
519
550
  }
520
551
  return [await this.getClient()];
@@ -530,7 +561,10 @@ var KeyvRedis = class extends import_hookified.Hookified {
530
561
  const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
531
562
  let cursor = "0";
532
563
  do {
533
- const result = await client.scan(cursor, { MATCH: match, TYPE: "string" });
564
+ const result = await client.scan(cursor, {
565
+ MATCH: match,
566
+ TYPE: "string"
567
+ });
534
568
  cursor = result.cursor.toString();
535
569
  let { keys } = result;
536
570
  if (!namespace && !this._noNamespaceAffectsAll) {
@@ -557,29 +591,37 @@ var KeyvRedis = class extends import_hookified.Hookified {
557
591
  async clear() {
558
592
  try {
559
593
  const clients = await this.getMasterNodes();
560
- await Promise.all(clients.map(async (client) => {
561
- if (!this._namespace && this._noNamespaceAffectsAll) {
562
- await client.flushDb();
563
- return;
564
- }
565
- let cursor = "0";
566
- const batchSize = this._clearBatchSize;
567
- const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
568
- const deletePromises = [];
569
- do {
570
- const result = await client.scan(cursor, { MATCH: match, COUNT: batchSize, TYPE: "string" });
571
- cursor = result.cursor.toString();
572
- let { keys } = result;
573
- if (keys.length === 0) {
574
- continue;
575
- }
576
- if (!this._namespace) {
577
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
594
+ await Promise.all(
595
+ clients.map(async (client) => {
596
+ if (!this._namespace && this._noNamespaceAffectsAll) {
597
+ await client.flushDb();
598
+ return;
578
599
  }
579
- deletePromises.push(this.clearWithClusterSupport(keys));
580
- } while (cursor !== "0");
581
- await Promise.all(deletePromises);
582
- }));
600
+ let cursor = "0";
601
+ const batchSize = this._clearBatchSize;
602
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
603
+ const deletePromises = [];
604
+ do {
605
+ const result = await client.scan(cursor, {
606
+ MATCH: match,
607
+ COUNT: batchSize,
608
+ TYPE: "string"
609
+ });
610
+ cursor = result.cursor.toString();
611
+ let { keys } = result;
612
+ if (keys.length === 0) {
613
+ continue;
614
+ }
615
+ if (!this._namespace) {
616
+ keys = keys.filter(
617
+ (key) => !key.includes(this._keyPrefixSeparator)
618
+ );
619
+ }
620
+ deletePromises.push(this.clearWithClusterSupport(keys));
621
+ } while (cursor !== "0");
622
+ await Promise.all(deletePromises);
623
+ })
624
+ );
583
625
  } catch (error) {
584
626
  this.emit("error", error);
585
627
  }
@@ -591,13 +633,15 @@ var KeyvRedis = class extends import_hookified.Hookified {
591
633
  async mget(keys) {
592
634
  const slotMap = this.getSlotMap(keys);
593
635
  const valueMap = /* @__PURE__ */ new Map();
594
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
595
- const client = await this.getSlotMaster(slot);
596
- const values = await client.mGet(keys2);
597
- for (const [index, value] of values.entries()) {
598
- valueMap.set(keys2[index], value ?? void 0);
599
- }
600
- }));
636
+ await Promise.all(
637
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
638
+ const client = await this.getSlotMaster(slot);
639
+ const values = await client.mGet(keys2);
640
+ for (const [index, value] of values.entries()) {
641
+ valueMap.set(keys2[index], value ?? void 0);
642
+ }
643
+ })
644
+ );
601
645
  return keys.map((key) => valueMap.get(key));
602
646
  }
603
647
  /**
@@ -607,10 +651,12 @@ var KeyvRedis = class extends import_hookified.Hookified {
607
651
  async clearWithClusterSupport(keys) {
608
652
  if (keys.length > 0) {
609
653
  const slotMap = this.getSlotMap(keys);
610
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
611
- const client = await this.getSlotMaster(slot);
612
- return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
613
- }));
654
+ await Promise.all(
655
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
656
+ const client = await this.getSlotMaster(slot);
657
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
658
+ })
659
+ );
614
660
  }
615
661
  }
616
662
  /**
@@ -648,6 +694,9 @@ var KeyvRedis = class extends import_hookified.Hookified {
648
694
  isClientCluster(client) {
649
695
  return client.slots !== void 0;
650
696
  }
697
+ isClientSentinel(client) {
698
+ return client.getSentinelNode !== void 0;
699
+ }
651
700
  setOptions(options) {
652
701
  if (!options) {
653
702
  return;
@@ -689,15 +738,11 @@ var KeyvRedis = class extends import_hookified.Hookified {
689
738
  });
690
739
  }
691
740
  async createTimeoutPromise(timeoutMs) {
692
- return new Promise((_, reject) => (
693
- // eslint-disable-next-line no-promise-executor-return
694
- setTimeout(
695
- () => {
696
- reject(new Error(`Redis timed out after ${timeoutMs}ms`));
697
- },
698
- timeoutMs
699
- )
700
- ));
741
+ return new Promise(
742
+ (_, reject) => setTimeout(() => {
743
+ reject(new Error(`Redis timed out after ${timeoutMs}ms`));
744
+ }, timeoutMs)
745
+ );
701
746
  }
702
747
  };
703
748
  function createKeyv(connect, options) {
@@ -705,7 +750,10 @@ function createKeyv(connect, options) {
705
750
  const adapter = new KeyvRedis(connect, options);
706
751
  if (options?.namespace) {
707
752
  adapter.namespace = options.namespace;
708
- const keyv2 = new import_keyv.Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
753
+ const keyv2 = new import_keyv.Keyv(adapter, {
754
+ namespace: options?.namespace,
755
+ useKeyPrefix: false
756
+ });
709
757
  if (options?.throwOnConnectError) {
710
758
  keyv2.throwOnErrors = true;
711
759
  }
@@ -747,5 +795,6 @@ function createKeyvNonBlocking(connect, options) {
747
795
  createCluster,
748
796
  createKeyv,
749
797
  createKeyvNonBlocking,
798
+ createSentinel,
750
799
  defaultReconnectStrategy
751
800
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { RedisClientOptions, RedisClusterOptions, RedisClientType, RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisClusterType } from '@redis/client';
2
- export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, createClient, createCluster } from '@redis/client';
1
+ import { RedisClientOptions, RedisClusterOptions, RedisSentinelOptions, RedisClientType, RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisClusterType, RedisSentinelType } from '@redis/client';
2
+ export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, RedisSentinelType, createClient, createCluster, createSentinel } from '@redis/client';
3
3
  import { Hookified } from 'hookified';
4
4
  import { KeyvStoreAdapter, KeyvEntry, Keyv } from 'keyv';
5
5
  export { Keyv } from 'keyv';
@@ -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
  */
@@ -80,7 +80,8 @@ declare enum RedisErrorMessages {
80
80
  declare const defaultReconnectStrategy: (attempts: number) => number | Error;
81
81
  type RedisConnectionClientType = RedisClientType | RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
82
82
  type RedisConnectionClusterType = RedisClusterType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisClusterType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
83
- type RedisClientConnectionType = RedisConnectionClientType | RedisConnectionClusterType;
83
+ type RedisConnectionSentinelType = RedisSentinelType | RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
84
+ type RedisClientConnectionType = RedisConnectionClientType | RedisConnectionClusterType | RedisConnectionSentinelType;
84
85
  declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
85
86
  private _client;
86
87
  private _namespace;
@@ -96,7 +97,7 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
96
97
  * @param {string | RedisClientOptions | RedisClientType} [connect] How to connect to the Redis server. If string pass in the url, if object pass in the options, if RedisClient pass in the client.
97
98
  * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
98
99
  */
99
- constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
100
+ constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisSentinelOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
100
101
  /**
101
102
  * Get the Redis client.
102
103
  */
@@ -278,6 +279,11 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
278
279
  * @returns {boolean} - true if the client is a cluster, false if not
279
280
  */
280
281
  isCluster(): boolean;
282
+ /**
283
+ * Is the client a sentinel.
284
+ * @returns {boolean} - true if the client is a sentinel, false if not
285
+ */
286
+ isSentinel(): boolean;
281
287
  /**
282
288
  * Get the master nodes in the cluster. If not a cluster, it will return the single client.
283
289
  *
@@ -320,6 +326,7 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
320
326
  */
321
327
  private getSlotMap;
322
328
  private isClientCluster;
329
+ private isClientSentinel;
323
330
  private setOptions;
324
331
  private initClient;
325
332
  private createTimeoutPromise;
@@ -333,4 +340,4 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
333
340
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
334
341
  declare function createKeyvNonBlocking(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
335
342
 
336
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, type RedisConnectionClientType, type RedisConnectionClusterType, RedisErrorMessages, createKeyv, createKeyvNonBlocking, KeyvRedis as default, defaultReconnectStrategy };
343
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, type RedisConnectionClientType, type RedisConnectionClusterType, type RedisConnectionSentinelType, RedisErrorMessages, createKeyv, createKeyvNonBlocking, KeyvRedis as default, defaultReconnectStrategy };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { RedisClientOptions, RedisClusterOptions, RedisClientType, RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisClusterType } from '@redis/client';
2
- export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, createClient, createCluster } from '@redis/client';
1
+ import { RedisClientOptions, RedisClusterOptions, RedisSentinelOptions, RedisClientType, RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisClusterType, RedisSentinelType } from '@redis/client';
2
+ export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, RedisSentinelType, createClient, createCluster, createSentinel } from '@redis/client';
3
3
  import { Hookified } from 'hookified';
4
4
  import { KeyvStoreAdapter, KeyvEntry, Keyv } from 'keyv';
5
5
  export { Keyv } from 'keyv';
@@ -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
  */
@@ -80,7 +80,8 @@ declare enum RedisErrorMessages {
80
80
  declare const defaultReconnectStrategy: (attempts: number) => number | Error;
81
81
  type RedisConnectionClientType = RedisClientType | RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
82
82
  type RedisConnectionClusterType = RedisClusterType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisClusterType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
83
- type RedisClientConnectionType = RedisConnectionClientType | RedisConnectionClusterType;
83
+ type RedisConnectionSentinelType = RedisSentinelType | RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions> | RedisSentinelType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>;
84
+ type RedisClientConnectionType = RedisConnectionClientType | RedisConnectionClusterType | RedisConnectionSentinelType;
84
85
  declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
85
86
  private _client;
86
87
  private _namespace;
@@ -96,7 +97,7 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
96
97
  * @param {string | RedisClientOptions | RedisClientType} [connect] How to connect to the Redis server. If string pass in the url, if object pass in the options, if RedisClient pass in the client.
97
98
  * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
98
99
  */
99
- constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
100
+ constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisSentinelOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
100
101
  /**
101
102
  * Get the Redis client.
102
103
  */
@@ -278,6 +279,11 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
278
279
  * @returns {boolean} - true if the client is a cluster, false if not
279
280
  */
280
281
  isCluster(): boolean;
282
+ /**
283
+ * Is the client a sentinel.
284
+ * @returns {boolean} - true if the client is a sentinel, false if not
285
+ */
286
+ isSentinel(): boolean;
281
287
  /**
282
288
  * Get the master nodes in the cluster. If not a cluster, it will return the single client.
283
289
  *
@@ -320,6 +326,7 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
320
326
  */
321
327
  private getSlotMap;
322
328
  private isClientCluster;
329
+ private isClientSentinel;
323
330
  private setOptions;
324
331
  private initClient;
325
332
  private createTimeoutPromise;
@@ -333,4 +340,4 @@ declare class KeyvRedis<T> extends Hookified implements KeyvStoreAdapter {
333
340
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
334
341
  declare function createKeyvNonBlocking(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
335
342
 
336
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, type RedisConnectionClientType, type RedisConnectionClusterType, RedisErrorMessages, createKeyv, createKeyvNonBlocking, KeyvRedis as default, defaultReconnectStrategy };
343
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, type RedisConnectionClientType, type RedisConnectionClusterType, type RedisConnectionSentinelType, RedisErrorMessages, createKeyv, createKeyvNonBlocking, KeyvRedis as default, defaultReconnectStrategy };
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  // src/index.ts
2
2
  import {
3
3
  createClient,
4
- createCluster
4
+ createCluster,
5
+ createSentinel
5
6
  } from "@redis/client";
7
+ import calculateSlot from "cluster-key-slot";
6
8
  import { Hookified } from "hookified";
7
9
  import { Keyv } from "keyv";
8
- import calculateSlot from "cluster-key-slot";
9
10
  import {
10
11
  createClient as createClient2,
11
- createCluster as createCluster2
12
+ createCluster as createCluster2,
13
+ createSentinel as createSentinel2
12
14
  } from "@redis/client";
13
- import {
14
- Keyv as Keyv2
15
- } from "keyv";
15
+ import { Keyv as Keyv2 } from "keyv";
16
16
  var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
17
17
  RedisErrorMessages2["RedisClientNotConnectedThrown"] = "Redis client is not connected or has failed to connect. This is thrown because throwOnConnectError is set to true.";
18
18
  return RedisErrorMessages2;
@@ -45,11 +45,30 @@ var KeyvRedis = class extends Hookified {
45
45
  };
46
46
  if (connect) {
47
47
  if (typeof connect === "string") {
48
- this._client = createClient({ url: connect, socket });
48
+ this._client = createClient({
49
+ url: connect,
50
+ socket
51
+ });
49
52
  } else if (connect.connect !== void 0) {
50
- this._client = this.isClientCluster(connect) ? connect : connect;
53
+ if (this.isClientSentinel(connect)) {
54
+ this._client = connect;
55
+ } else if (this.isClientCluster(connect)) {
56
+ this._client = connect;
57
+ } else {
58
+ this._client = connect;
59
+ }
51
60
  } else if (connect instanceof Object) {
52
- this._client = connect.rootNodes === void 0 ? createClient(connect) : createCluster(connect);
61
+ if (connect.sentinelRootNodes !== void 0) {
62
+ this._client = createSentinel(
63
+ connect
64
+ );
65
+ } else if (connect.rootNodes === void 0) {
66
+ this._client = createClient(
67
+ connect
68
+ );
69
+ } else {
70
+ this._client = createCluster(connect);
71
+ }
53
72
  }
54
73
  }
55
74
  this.setOptions(options);
@@ -327,7 +346,9 @@ var KeyvRedis = class extends Hookified {
327
346
  multi.exists(prefixedKey);
328
347
  }
329
348
  const results = await multi.exec();
330
- return results.map((result) => typeof result === "number" && result === 1);
349
+ return results.map(
350
+ (result) => typeof result === "number" && result === 1
351
+ );
331
352
  } catch (error) {
332
353
  this.emit("error", error);
333
354
  if (this._throwOnErrors) {
@@ -473,6 +494,13 @@ var KeyvRedis = class extends Hookified {
473
494
  isCluster() {
474
495
  return this.isClientCluster(this._client);
475
496
  }
497
+ /**
498
+ * Is the client a sentinel.
499
+ * @returns {boolean} - true if the client is a sentinel, false if not
500
+ */
501
+ isSentinel() {
502
+ return this.isClientSentinel(this._client);
503
+ }
476
504
  /**
477
505
  * Get the master nodes in the cluster. If not a cluster, it will return the single client.
478
506
  *
@@ -481,7 +509,9 @@ var KeyvRedis = class extends Hookified {
481
509
  async getMasterNodes() {
482
510
  if (this.isCluster()) {
483
511
  const cluster = await this.getClient();
484
- const nodes = cluster.masters.map(async (main) => cluster.nodeClient(main));
512
+ const nodes = cluster.masters.map(
513
+ async (main) => cluster.nodeClient(main)
514
+ );
485
515
  return Promise.all(nodes);
486
516
  }
487
517
  return [await this.getClient()];
@@ -497,7 +527,10 @@ var KeyvRedis = class extends Hookified {
497
527
  const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
498
528
  let cursor = "0";
499
529
  do {
500
- const result = await client.scan(cursor, { MATCH: match, TYPE: "string" });
530
+ const result = await client.scan(cursor, {
531
+ MATCH: match,
532
+ TYPE: "string"
533
+ });
501
534
  cursor = result.cursor.toString();
502
535
  let { keys } = result;
503
536
  if (!namespace && !this._noNamespaceAffectsAll) {
@@ -524,29 +557,37 @@ var KeyvRedis = class extends Hookified {
524
557
  async clear() {
525
558
  try {
526
559
  const clients = await this.getMasterNodes();
527
- await Promise.all(clients.map(async (client) => {
528
- if (!this._namespace && this._noNamespaceAffectsAll) {
529
- await client.flushDb();
530
- return;
531
- }
532
- let cursor = "0";
533
- const batchSize = this._clearBatchSize;
534
- const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
535
- const deletePromises = [];
536
- do {
537
- const result = await client.scan(cursor, { MATCH: match, COUNT: batchSize, TYPE: "string" });
538
- cursor = result.cursor.toString();
539
- let { keys } = result;
540
- if (keys.length === 0) {
541
- continue;
542
- }
543
- if (!this._namespace) {
544
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
560
+ await Promise.all(
561
+ clients.map(async (client) => {
562
+ if (!this._namespace && this._noNamespaceAffectsAll) {
563
+ await client.flushDb();
564
+ return;
545
565
  }
546
- deletePromises.push(this.clearWithClusterSupport(keys));
547
- } while (cursor !== "0");
548
- await Promise.all(deletePromises);
549
- }));
566
+ let cursor = "0";
567
+ const batchSize = this._clearBatchSize;
568
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
569
+ const deletePromises = [];
570
+ do {
571
+ const result = await client.scan(cursor, {
572
+ MATCH: match,
573
+ COUNT: batchSize,
574
+ TYPE: "string"
575
+ });
576
+ cursor = result.cursor.toString();
577
+ let { keys } = result;
578
+ if (keys.length === 0) {
579
+ continue;
580
+ }
581
+ if (!this._namespace) {
582
+ keys = keys.filter(
583
+ (key) => !key.includes(this._keyPrefixSeparator)
584
+ );
585
+ }
586
+ deletePromises.push(this.clearWithClusterSupport(keys));
587
+ } while (cursor !== "0");
588
+ await Promise.all(deletePromises);
589
+ })
590
+ );
550
591
  } catch (error) {
551
592
  this.emit("error", error);
552
593
  }
@@ -558,13 +599,15 @@ var KeyvRedis = class extends Hookified {
558
599
  async mget(keys) {
559
600
  const slotMap = this.getSlotMap(keys);
560
601
  const valueMap = /* @__PURE__ */ new Map();
561
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
562
- const client = await this.getSlotMaster(slot);
563
- const values = await client.mGet(keys2);
564
- for (const [index, value] of values.entries()) {
565
- valueMap.set(keys2[index], value ?? void 0);
566
- }
567
- }));
602
+ await Promise.all(
603
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
604
+ const client = await this.getSlotMaster(slot);
605
+ const values = await client.mGet(keys2);
606
+ for (const [index, value] of values.entries()) {
607
+ valueMap.set(keys2[index], value ?? void 0);
608
+ }
609
+ })
610
+ );
568
611
  return keys.map((key) => valueMap.get(key));
569
612
  }
570
613
  /**
@@ -574,10 +617,12 @@ var KeyvRedis = class extends Hookified {
574
617
  async clearWithClusterSupport(keys) {
575
618
  if (keys.length > 0) {
576
619
  const slotMap = this.getSlotMap(keys);
577
- await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
578
- const client = await this.getSlotMaster(slot);
579
- return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
580
- }));
620
+ await Promise.all(
621
+ Array.from(slotMap.entries(), async ([slot, keys2]) => {
622
+ const client = await this.getSlotMaster(slot);
623
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
624
+ })
625
+ );
581
626
  }
582
627
  }
583
628
  /**
@@ -615,6 +660,9 @@ var KeyvRedis = class extends Hookified {
615
660
  isClientCluster(client) {
616
661
  return client.slots !== void 0;
617
662
  }
663
+ isClientSentinel(client) {
664
+ return client.getSentinelNode !== void 0;
665
+ }
618
666
  setOptions(options) {
619
667
  if (!options) {
620
668
  return;
@@ -656,15 +704,11 @@ var KeyvRedis = class extends Hookified {
656
704
  });
657
705
  }
658
706
  async createTimeoutPromise(timeoutMs) {
659
- return new Promise((_, reject) => (
660
- // eslint-disable-next-line no-promise-executor-return
661
- setTimeout(
662
- () => {
663
- reject(new Error(`Redis timed out after ${timeoutMs}ms`));
664
- },
665
- timeoutMs
666
- )
667
- ));
707
+ return new Promise(
708
+ (_, reject) => setTimeout(() => {
709
+ reject(new Error(`Redis timed out after ${timeoutMs}ms`));
710
+ }, timeoutMs)
711
+ );
668
712
  }
669
713
  };
670
714
  function createKeyv(connect, options) {
@@ -672,7 +716,10 @@ function createKeyv(connect, options) {
672
716
  const adapter = new KeyvRedis(connect, options);
673
717
  if (options?.namespace) {
674
718
  adapter.namespace = options.namespace;
675
- const keyv2 = new Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
719
+ const keyv2 = new Keyv(adapter, {
720
+ namespace: options?.namespace,
721
+ useKeyPrefix: false
722
+ });
676
723
  if (options?.throwOnConnectError) {
677
724
  keyv2.throwOnErrors = true;
678
725
  }
@@ -713,6 +760,7 @@ export {
713
760
  createCluster2 as createCluster,
714
761
  createKeyv,
715
762
  createKeyvNonBlocking,
763
+ createSentinel2 as createSentinel,
716
764
  KeyvRedis as default,
717
765
  defaultReconnectStrategy
718
766
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/redis",
3
- "version": "5.0.0",
3
+ "version": "5.1.1",
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.6.0",
37
+ "@redis/client": "^5.7.0",
38
38
  "cluster-key-slot": "^1.1.2",
39
- "hookified": "^1.10.0"
39
+ "hookified": "^1.11.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "keyv": "^5.4.0"
42
+ "keyv": "^5.5.0"
43
43
  },
44
44
  "devDependencies": {
45
+ "@biomejs/biome": "^2.2.0",
45
46
  "@faker-js/faker": "^9.9.0",
46
47
  "@vitest/coverage-v8": "^3.2.4",
47
48
  "rimraf": "^6.0.1",
48
49
  "timekeeper": "^2.3.1",
49
50
  "tsd": "^0.32.0",
50
51
  "vitest": "^3.2.4",
51
- "xo": "^1.1.1",
52
- "@keyv/test-suite": "^2.0.9"
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
  }