@keyv/redis 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,9 +26,10 @@ Redis storage adapter for [Keyv](https://github.com/jaredwray/keyv).
26
26
  # Table of Contents
27
27
  * [Usage](#usage)
28
28
  * [Namespaces](#namespaces)
29
- * [Typescript](#typescript)
29
+ * [Using Generic Types](#using-generic-types)
30
30
  * [Performance Considerations](#performance-considerations)
31
31
  * [High Memory Usage on Redis Server](#high-memory-usage-on-redis-server)
32
+ * [Gracefully Handling Connection Errors, Retries, and Timeouts](#gracefully-handling-connection-errors-retries-and-timeouts)
32
33
  * [Using Cacheable with Redis](#using-cacheable-with-redis)
33
34
  * [Clustering and TLS Support](#clustering-and-tls-support)
34
35
  * [API](#api)
@@ -99,6 +100,71 @@ const keyvRedis = new KeyvRedis(redis);
99
100
  const keyv = new Keyv({ store: keyvRedis});
100
101
  ```
101
102
 
103
+ # Keyv Redis Options
104
+
105
+ You can pass in options to the `KeyvRedis` constructor. Here are the available options:
106
+
107
+ ```typescript
108
+ export type KeyvRedisOptions = {
109
+ /**
110
+ * Namespace for the current instance.
111
+ */
112
+ namespace?: string;
113
+ /**
114
+ * Separator to use between namespace and key.
115
+ */
116
+ keyPrefixSeparator?: string;
117
+ /**
118
+ * Number of keys to delete in a single batch.
119
+ */
120
+ clearBatchSize?: number;
121
+ /**
122
+ * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
123
+ */
124
+ useUnlink?: boolean;
125
+
126
+ /**
127
+ * Whether to allow clearing all keys when no namespace is set.
128
+ * If set to true and no namespace is set, iterate() will return all keys.
129
+ * Defaults to `false`.
130
+ */
131
+ noNamespaceAffectsAll?: boolean;
132
+
133
+ /**
134
+ * Timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
135
+ * Defaults to `200`.
136
+ */
137
+ connectTimeout?: number;
138
+ };
139
+ ```
140
+ You can pass these options when creating a new `KeyvRedis` instance:
141
+
142
+ ```js
143
+ import Keyv from 'keyv';
144
+ import KeyvRedis from '@keyv/redis';
145
+
146
+ const keyvRedis = new KeyvRedis({
147
+ namespace: 'my-namespace',
148
+ keyPrefixSeparator: ':',
149
+ clearBatchSize: 1000,
150
+ useUnlink: true,
151
+ noNamespaceAffectsAll: false,
152
+ connectTimeout: 200
153
+ });
154
+
155
+ const keyv = new Keyv({ store: keyvRedis });
156
+ ```
157
+
158
+ You can also set these options after the fact by using the `KeyvRedis` instance properties:
159
+
160
+ ```js
161
+ import {createKeyv} from '@keyv/redis';
162
+
163
+ const keyv = createKeyv('redis://user:pass@localhost:6379');
164
+ keyv.store.namespace = 'my-namespace';
165
+ ```
166
+
167
+
102
168
  # Namespaces
103
169
 
104
170
  You can set a namespace for your keys. This is useful if you want to manage your keys in a more organized way. Here is an example of how to set a `namespace` with the `store` option:
@@ -120,7 +186,7 @@ keyv.namespace = 'my-namespace';
120
186
 
121
187
  NOTE: If you plan to do many clears or deletes, it is recommended to read the [Performance Considerations](#performance-considerations) section.
122
188
 
123
- ## Typescript
189
+ ## Using Generic Types
124
190
 
125
191
  When initializing `KeyvRedis`, you can specify the type of the values you are storing and you can also specify types when calling methods:
126
192
 
@@ -129,7 +195,7 @@ import Keyv from 'keyv';
129
195
  import KeyvRedis, { createClient } from '@keyv/redis';
130
196
 
131
197
 
132
- interface User {
198
+ type User {
133
199
  id: number
134
200
  name: string
135
201
  }
@@ -175,6 +241,38 @@ const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379', { useUnl
175
241
  keyv.useUnlink = false;
176
242
  ```
177
243
 
244
+ # Gracefully Handling Connection Errors, Retries, and Timeouts
245
+
246
+ When using `@keyv/redis`, it is important to handle connection errors gracefully. You can do this by listening to the `error` event on the `KeyvRedis` instance. Here is an example of how to do that:
247
+
248
+ ```js
249
+ import Keyv from 'keyv';
250
+ import KeyvRedis from '@keyv/redis';
251
+ const keyv = new Keyv(new KeyvRedis('redis://user:pass@localhost:6379'));
252
+ keyv.on('error', (error) => {
253
+ console.error('error', error);
254
+ });
255
+ ```
256
+
257
+ We also attempt to connect to Redis and have a `connectTimeout` option that defaults to `200ms`. If the connection is not established within this time, it will emit an error. You can catch this error and handle it accordingly.
258
+
259
+ On `get`, `getMany`, `set`, `setMany`, `delete`, `deleteMany`, and `clear` methods, if the connection is lost, it will emit an error and return a no-op value. You can catch this error and handle it accordingly. This is important to ensure that your application does not crash due to a lost connection to Redis.
260
+
261
+ If you pass in just a `uri` connection string we will automatically create a Redis client for you with the following reconnect strategy:
262
+
263
+ ```typescript
264
+ export const defaultReconnectStrategy = (attempts: number): number | Error => {
265
+ // Exponential backoff base: double each time, capped at 2s.
266
+ // Parentheses make it clear we do (2 ** attempts) first, then * 100
267
+ const backoff = Math.min((2 ** attempts) * 100, 2000);
268
+
269
+ // Add random jitter of up to ±50ms to avoid thundering herds:
270
+ const jitter = (Math.random() - 0.5) * 100;
271
+
272
+ return backoff + jitter;
273
+ };
274
+ ```
275
+
178
276
  # Using Cacheable with Redis
179
277
 
180
278
  If you are wanting to see even better performance with Redis, you can use [Cacheable](https://npmjs.org/package/cacheable) which is a multi-layered cache library that has in-memory primary caching and non-blocking secondary caching. Here is an example of how to use it with Redis:
package/dist/index.cjs CHANGED
@@ -31,10 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Keyv: () => import_keyv2.Keyv,
34
+ RedisErrorMessages: () => RedisErrorMessages,
34
35
  createClient: () => import_client2.createClient,
35
36
  createCluster: () => import_client2.createCluster,
36
37
  createKeyv: () => createKeyv,
37
- default: () => KeyvRedis
38
+ default: () => KeyvRedis,
39
+ defaultReconnectStrategy: () => defaultReconnectStrategy
38
40
  });
39
41
  module.exports = __toCommonJS(index_exports);
40
42
  var import_node_events = __toESM(require("events"), 1);
@@ -43,6 +45,15 @@ var import_keyv = require("keyv");
43
45
  var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
44
46
  var import_client2 = require("@redis/client");
45
47
  var import_keyv2 = require("keyv");
48
+ var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
49
+ RedisErrorMessages2["RedisClientNotConnected"] = "Redis client is not connected or has failed to connect";
50
+ return RedisErrorMessages2;
51
+ })(RedisErrorMessages || {});
52
+ var defaultReconnectStrategy = (attempts) => {
53
+ const backoff = Math.min(2 ** attempts * 100, 2e3);
54
+ const jitter = (Math.random() - 0.5) * 100;
55
+ return backoff + jitter;
56
+ };
46
57
  var KeyvRedis = class extends import_node_events.default {
47
58
  _client = (0, import_client.createClient)();
48
59
  _namespace;
@@ -50,6 +61,10 @@ var KeyvRedis = class extends import_node_events.default {
50
61
  _clearBatchSize = 1e3;
51
62
  _useUnlink = true;
52
63
  _noNamespaceAffectsAll = false;
64
+ _connectTimeout = 200;
65
+ // Timeout for connecting to Redis in milliseconds
66
+ _reconnectClient = false;
67
+ // Whether to reconnect the client
53
68
  /**
54
69
  * KeyvRedis constructor.
55
70
  * @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.
@@ -57,9 +72,12 @@ var KeyvRedis = class extends import_node_events.default {
57
72
  */
58
73
  constructor(connect, options) {
59
74
  super();
75
+ const socket = {
76
+ reconnectStrategy: defaultReconnectStrategy
77
+ };
60
78
  if (connect) {
61
79
  if (typeof connect === "string") {
62
- this._client = (0, import_client.createClient)({ url: connect });
80
+ this._client = (0, import_client.createClient)({ url: connect, socket });
63
81
  } else if (connect.connect !== void 0) {
64
82
  this._client = this.isClientCluster(connect) ? connect : connect;
65
83
  } else if (connect instanceof Object) {
@@ -180,18 +198,50 @@ var KeyvRedis = class extends import_node_events.default {
180
198
  set noNamespaceAffectsAll(value) {
181
199
  this._noNamespaceAffectsAll = value;
182
200
  }
201
+ /**
202
+ * Get the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
203
+ * @default 200
204
+ */
205
+ get connectTimeout() {
206
+ return this._connectTimeout;
207
+ }
208
+ /**
209
+ * Set the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
210
+ * @default 200
211
+ */
212
+ set connectTimeout(value) {
213
+ if (value > 0) {
214
+ this._connectTimeout = value;
215
+ this._reconnectClient = true;
216
+ } else {
217
+ this.emit("error", "connectTimeout must be greater than 0");
218
+ }
219
+ }
183
220
  /**
184
221
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
185
222
  */
186
223
  async getClient() {
224
+ if (this._client.isOpen && !this._reconnectClient) {
225
+ return this._client;
226
+ }
227
+ if (this._reconnectClient && this._client.isOpen) {
228
+ await this._client.disconnect();
229
+ }
187
230
  try {
188
- if (!this._client.isOpen) {
189
- await this._client.connect();
190
- }
231
+ const timeoutPromise = new Promise((resolves, reject) => setTimeout(() => {
232
+ reject(new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
233
+ }, this._connectTimeout));
234
+ await Promise.race([
235
+ this._client.connect(),
236
+ timeoutPromise
237
+ ]);
238
+ this._reconnectClient = false;
239
+ this.initClient();
240
+ return this._client;
191
241
  } catch (error) {
192
242
  this.emit("error", error);
243
+ return void 0;
193
244
  }
194
- return this._client;
195
245
  }
196
246
  /**
197
247
  * Set a key value pair in the store. TTL is in milliseconds.
@@ -201,6 +251,10 @@ var KeyvRedis = class extends import_node_events.default {
201
251
  */
202
252
  async set(key, value, ttl) {
203
253
  const client = await this.getClient();
254
+ if (!client) {
255
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
256
+ return;
257
+ }
204
258
  key = this.createKeyPrefix(key, this._namespace);
205
259
  if (ttl) {
206
260
  await client.set(key, value, { PX: ttl });
@@ -214,6 +268,10 @@ var KeyvRedis = class extends import_node_events.default {
214
268
  */
215
269
  async setMany(entries) {
216
270
  const client = await this.getClient();
271
+ if (!client) {
272
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
273
+ return;
274
+ }
217
275
  const multi = client.multi();
218
276
  for (const { key, value, ttl } of entries) {
219
277
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -232,6 +290,10 @@ var KeyvRedis = class extends import_node_events.default {
232
290
  */
233
291
  async has(key) {
234
292
  const client = await this.getClient();
293
+ if (!client) {
294
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
295
+ return false;
296
+ }
235
297
  key = this.createKeyPrefix(key, this._namespace);
236
298
  const exists = await client.exists(key);
237
299
  return exists === 1;
@@ -243,6 +305,10 @@ var KeyvRedis = class extends import_node_events.default {
243
305
  */
244
306
  async hasMany(keys) {
245
307
  const client = await this.getClient();
308
+ if (!client) {
309
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
310
+ return Array.from({ length: keys.length }).fill(false);
311
+ }
246
312
  const multi = client.multi();
247
313
  for (const key of keys) {
248
314
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -258,6 +324,10 @@ var KeyvRedis = class extends import_node_events.default {
258
324
  */
259
325
  async get(key) {
260
326
  const client = await this.getClient();
327
+ if (!client) {
328
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
329
+ return void 0;
330
+ }
261
331
  key = this.createKeyPrefix(key, this._namespace);
262
332
  const value = await client.get(key);
263
333
  if (value === null) {
@@ -275,8 +345,13 @@ var KeyvRedis = class extends import_node_events.default {
275
345
  return [];
276
346
  }
277
347
  keys = keys.map((key) => this.createKeyPrefix(key, this._namespace));
278
- const values = await this.mget(keys);
279
- return values;
348
+ try {
349
+ const values = await this.mget(keys);
350
+ return values;
351
+ } catch (error) {
352
+ this.emit("error", error);
353
+ return Array.from({ length: keys.length }).fill(void 0);
354
+ }
280
355
  }
281
356
  /**
282
357
  * Delete a key from the store.
@@ -285,6 +360,10 @@ var KeyvRedis = class extends import_node_events.default {
285
360
  */
286
361
  async delete(key) {
287
362
  const client = await this.getClient();
363
+ if (!client) {
364
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
365
+ return false;
366
+ }
288
367
  key = this.createKeyPrefix(key, this._namespace);
289
368
  let deleted = 0;
290
369
  deleted = await (this._useUnlink ? client.unlink(key) : client.del(key));
@@ -298,6 +377,10 @@ var KeyvRedis = class extends import_node_events.default {
298
377
  async deleteMany(keys) {
299
378
  let result = false;
300
379
  const client = await this.getClient();
380
+ if (!client) {
381
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
382
+ return false;
383
+ }
301
384
  const multi = client.multi();
302
385
  for (const key of keys) {
303
386
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -468,6 +551,9 @@ var KeyvRedis = class extends import_node_events.default {
468
551
  */
469
552
  async getSlotMaster(slot) {
470
553
  const connection = await this.getClient();
554
+ if (!connection) {
555
+ throw new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */);
556
+ }
471
557
  if (this.isCluster()) {
472
558
  const cluster = connection;
473
559
  const mainNode = cluster.slots[slot].master;
@@ -520,23 +606,37 @@ var KeyvRedis = class extends import_node_events.default {
520
606
  if (options.noNamespaceAffectsAll !== void 0) {
521
607
  this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
522
608
  }
609
+ if (options.connectTimeout !== void 0 && options.connectTimeout > 0) {
610
+ this._connectTimeout = options.connectTimeout;
611
+ }
523
612
  }
524
613
  initClient() {
525
614
  this._client.on("error", (error) => {
526
615
  this.emit("error", error);
527
616
  });
617
+ this._client.on("connect", () => {
618
+ this.emit("connect", this._client);
619
+ });
620
+ this._client.on("disconnect", () => {
621
+ this.emit("disconnect", this._client);
622
+ });
623
+ this._client.on("reconnecting", (reconnectInfo) => {
624
+ this.emit("reconnecting", reconnectInfo);
625
+ });
528
626
  }
529
627
  };
530
628
  function createKeyv(connect, options) {
531
629
  connect ??= "redis://localhost:6379";
532
630
  const adapter = new KeyvRedis(connect, options);
533
- const keyv = new import_keyv.Keyv({ store: adapter, namespace: options?.namespace, useKeyPrefix: false });
631
+ const keyv = new import_keyv.Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
534
632
  return keyv;
535
633
  }
536
634
  // Annotate the CommonJS export names for ESM import in node:
537
635
  0 && (module.exports = {
538
636
  Keyv,
637
+ RedisErrorMessages,
539
638
  createClient,
540
639
  createCluster,
541
- createKeyv
640
+ createKeyv,
641
+ defaultReconnectStrategy
542
642
  });
package/dist/index.d.cts CHANGED
@@ -27,6 +27,11 @@ type KeyvRedisOptions = {
27
27
  * Defaults to `false`.
28
28
  */
29
29
  noNamespaceAffectsAll?: boolean;
30
+ /**
31
+ * Timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
32
+ * Defaults to `200`.
33
+ */
34
+ connectTimeout?: number;
30
35
  };
31
36
  type KeyvRedisPropertyOptions = KeyvRedisOptions & {
32
37
  /**
@@ -52,6 +57,13 @@ type KeyvRedisEntry<T> = {
52
57
  */
53
58
  ttl?: number;
54
59
  };
60
+ declare enum RedisErrorMessages {
61
+ /**
62
+ * Error message when the Redis client is not connected.
63
+ */
64
+ RedisClientNotConnected = "Redis client is not connected or has failed to connect"
65
+ }
66
+ declare const defaultReconnectStrategy: (attempts: number) => number | Error;
55
67
  type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
56
68
  declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
57
69
  private _client;
@@ -60,6 +72,8 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
60
72
  private _clearBatchSize;
61
73
  private _useUnlink;
62
74
  private _noNamespaceAffectsAll;
75
+ private _connectTimeout;
76
+ private _reconnectClient;
63
77
  /**
64
78
  * KeyvRedis constructor.
65
79
  * @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.
@@ -129,10 +143,20 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
129
143
  * Set if not namespace affects all keys.
130
144
  */
131
145
  set noNamespaceAffectsAll(value: boolean);
146
+ /**
147
+ * Get the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
148
+ * @default 200
149
+ */
150
+ get connectTimeout(): number;
151
+ /**
152
+ * Set the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
153
+ * @default 200
154
+ */
155
+ set connectTimeout(value: number);
132
156
  /**
133
157
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
134
158
  */
135
- getClient(): Promise<RedisClientConnectionType>;
159
+ getClient(): Promise<RedisClientConnectionType | undefined>;
136
160
  /**
137
161
  * Set a key value pair in the store. TTL is in milliseconds.
138
162
  * @param {string} key - the key to set
@@ -260,4 +284,4 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
260
284
  */
261
285
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
262
286
 
263
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, createKeyv, KeyvRedis as default };
287
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, RedisErrorMessages, createKeyv, KeyvRedis as default, defaultReconnectStrategy };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,11 @@ type KeyvRedisOptions = {
27
27
  * Defaults to `false`.
28
28
  */
29
29
  noNamespaceAffectsAll?: boolean;
30
+ /**
31
+ * Timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
32
+ * Defaults to `200`.
33
+ */
34
+ connectTimeout?: number;
30
35
  };
31
36
  type KeyvRedisPropertyOptions = KeyvRedisOptions & {
32
37
  /**
@@ -52,6 +57,13 @@ type KeyvRedisEntry<T> = {
52
57
  */
53
58
  ttl?: number;
54
59
  };
60
+ declare enum RedisErrorMessages {
61
+ /**
62
+ * Error message when the Redis client is not connected.
63
+ */
64
+ RedisClientNotConnected = "Redis client is not connected or has failed to connect"
65
+ }
66
+ declare const defaultReconnectStrategy: (attempts: number) => number | Error;
55
67
  type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
56
68
  declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
57
69
  private _client;
@@ -60,6 +72,8 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
60
72
  private _clearBatchSize;
61
73
  private _useUnlink;
62
74
  private _noNamespaceAffectsAll;
75
+ private _connectTimeout;
76
+ private _reconnectClient;
63
77
  /**
64
78
  * KeyvRedis constructor.
65
79
  * @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.
@@ -129,10 +143,20 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
129
143
  * Set if not namespace affects all keys.
130
144
  */
131
145
  set noNamespaceAffectsAll(value: boolean);
146
+ /**
147
+ * Get the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
148
+ * @default 200
149
+ */
150
+ get connectTimeout(): number;
151
+ /**
152
+ * Set the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
153
+ * @default 200
154
+ */
155
+ set connectTimeout(value: number);
132
156
  /**
133
157
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
134
158
  */
135
- getClient(): Promise<RedisClientConnectionType>;
159
+ getClient(): Promise<RedisClientConnectionType | undefined>;
136
160
  /**
137
161
  * Set a key value pair in the store. TTL is in milliseconds.
138
162
  * @param {string} key - the key to set
@@ -260,4 +284,4 @@ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
260
284
  */
261
285
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
262
286
 
263
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, createKeyv, KeyvRedis as default };
287
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, RedisErrorMessages, createKeyv, KeyvRedis as default, defaultReconnectStrategy };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import EventEmitter from "node:events";
2
+ import EventEmitter from "events";
3
3
  import {
4
4
  createClient,
5
5
  createCluster
@@ -13,6 +13,15 @@ import {
13
13
  import {
14
14
  Keyv as Keyv2
15
15
  } from "keyv";
16
+ var RedisErrorMessages = /* @__PURE__ */ ((RedisErrorMessages2) => {
17
+ RedisErrorMessages2["RedisClientNotConnected"] = "Redis client is not connected or has failed to connect";
18
+ return RedisErrorMessages2;
19
+ })(RedisErrorMessages || {});
20
+ var defaultReconnectStrategy = (attempts) => {
21
+ const backoff = Math.min(2 ** attempts * 100, 2e3);
22
+ const jitter = (Math.random() - 0.5) * 100;
23
+ return backoff + jitter;
24
+ };
16
25
  var KeyvRedis = class extends EventEmitter {
17
26
  _client = createClient();
18
27
  _namespace;
@@ -20,6 +29,10 @@ var KeyvRedis = class extends EventEmitter {
20
29
  _clearBatchSize = 1e3;
21
30
  _useUnlink = true;
22
31
  _noNamespaceAffectsAll = false;
32
+ _connectTimeout = 200;
33
+ // Timeout for connecting to Redis in milliseconds
34
+ _reconnectClient = false;
35
+ // Whether to reconnect the client
23
36
  /**
24
37
  * KeyvRedis constructor.
25
38
  * @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.
@@ -27,9 +40,12 @@ var KeyvRedis = class extends EventEmitter {
27
40
  */
28
41
  constructor(connect, options) {
29
42
  super();
43
+ const socket = {
44
+ reconnectStrategy: defaultReconnectStrategy
45
+ };
30
46
  if (connect) {
31
47
  if (typeof connect === "string") {
32
- this._client = createClient({ url: connect });
48
+ this._client = createClient({ url: connect, socket });
33
49
  } else if (connect.connect !== void 0) {
34
50
  this._client = this.isClientCluster(connect) ? connect : connect;
35
51
  } else if (connect instanceof Object) {
@@ -150,18 +166,50 @@ var KeyvRedis = class extends EventEmitter {
150
166
  set noNamespaceAffectsAll(value) {
151
167
  this._noNamespaceAffectsAll = value;
152
168
  }
169
+ /**
170
+ * Get the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
171
+ * @default 200
172
+ */
173
+ get connectTimeout() {
174
+ return this._connectTimeout;
175
+ }
176
+ /**
177
+ * Set the timeout for connecting to Redis in milliseconds. This is used to prevent hanging indefinitely when connecting to Redis.
178
+ * @default 200
179
+ */
180
+ set connectTimeout(value) {
181
+ if (value > 0) {
182
+ this._connectTimeout = value;
183
+ this._reconnectClient = true;
184
+ } else {
185
+ this.emit("error", "connectTimeout must be greater than 0");
186
+ }
187
+ }
153
188
  /**
154
189
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
155
190
  */
156
191
  async getClient() {
192
+ if (this._client.isOpen && !this._reconnectClient) {
193
+ return this._client;
194
+ }
195
+ if (this._reconnectClient && this._client.isOpen) {
196
+ await this._client.disconnect();
197
+ }
157
198
  try {
158
- if (!this._client.isOpen) {
159
- await this._client.connect();
160
- }
199
+ const timeoutPromise = new Promise((resolves, reject) => setTimeout(() => {
200
+ reject(new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
201
+ }, this._connectTimeout));
202
+ await Promise.race([
203
+ this._client.connect(),
204
+ timeoutPromise
205
+ ]);
206
+ this._reconnectClient = false;
207
+ this.initClient();
208
+ return this._client;
161
209
  } catch (error) {
162
210
  this.emit("error", error);
211
+ return void 0;
163
212
  }
164
- return this._client;
165
213
  }
166
214
  /**
167
215
  * Set a key value pair in the store. TTL is in milliseconds.
@@ -171,6 +219,10 @@ var KeyvRedis = class extends EventEmitter {
171
219
  */
172
220
  async set(key, value, ttl) {
173
221
  const client = await this.getClient();
222
+ if (!client) {
223
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
224
+ return;
225
+ }
174
226
  key = this.createKeyPrefix(key, this._namespace);
175
227
  if (ttl) {
176
228
  await client.set(key, value, { PX: ttl });
@@ -184,6 +236,10 @@ var KeyvRedis = class extends EventEmitter {
184
236
  */
185
237
  async setMany(entries) {
186
238
  const client = await this.getClient();
239
+ if (!client) {
240
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
241
+ return;
242
+ }
187
243
  const multi = client.multi();
188
244
  for (const { key, value, ttl } of entries) {
189
245
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -202,6 +258,10 @@ var KeyvRedis = class extends EventEmitter {
202
258
  */
203
259
  async has(key) {
204
260
  const client = await this.getClient();
261
+ if (!client) {
262
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
263
+ return false;
264
+ }
205
265
  key = this.createKeyPrefix(key, this._namespace);
206
266
  const exists = await client.exists(key);
207
267
  return exists === 1;
@@ -213,6 +273,10 @@ var KeyvRedis = class extends EventEmitter {
213
273
  */
214
274
  async hasMany(keys) {
215
275
  const client = await this.getClient();
276
+ if (!client) {
277
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
278
+ return Array.from({ length: keys.length }).fill(false);
279
+ }
216
280
  const multi = client.multi();
217
281
  for (const key of keys) {
218
282
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -228,6 +292,10 @@ var KeyvRedis = class extends EventEmitter {
228
292
  */
229
293
  async get(key) {
230
294
  const client = await this.getClient();
295
+ if (!client) {
296
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
297
+ return void 0;
298
+ }
231
299
  key = this.createKeyPrefix(key, this._namespace);
232
300
  const value = await client.get(key);
233
301
  if (value === null) {
@@ -245,8 +313,13 @@ var KeyvRedis = class extends EventEmitter {
245
313
  return [];
246
314
  }
247
315
  keys = keys.map((key) => this.createKeyPrefix(key, this._namespace));
248
- const values = await this.mget(keys);
249
- return values;
316
+ try {
317
+ const values = await this.mget(keys);
318
+ return values;
319
+ } catch (error) {
320
+ this.emit("error", error);
321
+ return Array.from({ length: keys.length }).fill(void 0);
322
+ }
250
323
  }
251
324
  /**
252
325
  * Delete a key from the store.
@@ -255,6 +328,10 @@ var KeyvRedis = class extends EventEmitter {
255
328
  */
256
329
  async delete(key) {
257
330
  const client = await this.getClient();
331
+ if (!client) {
332
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
333
+ return false;
334
+ }
258
335
  key = this.createKeyPrefix(key, this._namespace);
259
336
  let deleted = 0;
260
337
  deleted = await (this._useUnlink ? client.unlink(key) : client.del(key));
@@ -268,6 +345,10 @@ var KeyvRedis = class extends EventEmitter {
268
345
  async deleteMany(keys) {
269
346
  let result = false;
270
347
  const client = await this.getClient();
348
+ if (!client) {
349
+ this.emit("error", new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */));
350
+ return false;
351
+ }
271
352
  const multi = client.multi();
272
353
  for (const key of keys) {
273
354
  const prefixedKey = this.createKeyPrefix(key, this._namespace);
@@ -438,6 +519,9 @@ var KeyvRedis = class extends EventEmitter {
438
519
  */
439
520
  async getSlotMaster(slot) {
440
521
  const connection = await this.getClient();
522
+ if (!connection) {
523
+ throw new Error("Redis client is not connected or has failed to connect" /* RedisClientNotConnected */);
524
+ }
441
525
  if (this.isCluster()) {
442
526
  const cluster = connection;
443
527
  const mainNode = cluster.slots[slot].master;
@@ -490,23 +574,37 @@ var KeyvRedis = class extends EventEmitter {
490
574
  if (options.noNamespaceAffectsAll !== void 0) {
491
575
  this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
492
576
  }
577
+ if (options.connectTimeout !== void 0 && options.connectTimeout > 0) {
578
+ this._connectTimeout = options.connectTimeout;
579
+ }
493
580
  }
494
581
  initClient() {
495
582
  this._client.on("error", (error) => {
496
583
  this.emit("error", error);
497
584
  });
585
+ this._client.on("connect", () => {
586
+ this.emit("connect", this._client);
587
+ });
588
+ this._client.on("disconnect", () => {
589
+ this.emit("disconnect", this._client);
590
+ });
591
+ this._client.on("reconnecting", (reconnectInfo) => {
592
+ this.emit("reconnecting", reconnectInfo);
593
+ });
498
594
  }
499
595
  };
500
596
  function createKeyv(connect, options) {
501
597
  connect ??= "redis://localhost:6379";
502
598
  const adapter = new KeyvRedis(connect, options);
503
- const keyv = new Keyv({ store: adapter, namespace: options?.namespace, useKeyPrefix: false });
599
+ const keyv = new Keyv(adapter, { namespace: options?.namespace, useKeyPrefix: false });
504
600
  return keyv;
505
601
  }
506
602
  export {
507
603
  Keyv2 as Keyv,
604
+ RedisErrorMessages,
508
605
  createClient2 as createClient,
509
606
  createCluster2 as createCluster,
510
607
  createKeyv,
511
- KeyvRedis as default
608
+ KeyvRedis as default,
609
+ defaultReconnectStrategy
512
610
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/redis",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Redis storage adapter for Keyv",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -38,16 +38,17 @@
38
38
  "cluster-key-slot": "^1.1.2"
39
39
  },
40
40
  "peerDependencies": {
41
- "keyv": "^5.3.3"
41
+ "keyv": "^5.3.4"
42
42
  },
43
43
  "devDependencies": {
44
- "@vitest/coverage-v8": "^3.1.2",
44
+ "@faker-js/faker": "^9.8.0",
45
+ "@vitest/coverage-v8": "^3.2.3",
45
46
  "rimraf": "^6.0.1",
46
47
  "timekeeper": "^2.3.1",
47
48
  "tsd": "^0.32.0",
48
- "vitest": "^3.1.2",
49
- "xo": "^0.60.0",
50
- "@keyv/test-suite": "^2.0.7"
49
+ "vitest": "^3.2.3",
50
+ "xo": "^1.1.0",
51
+ "@keyv/test-suite": "^2.0.8"
51
52
  },
52
53
  "tsd": {
53
54
  "directory": "test"
@@ -62,7 +63,7 @@
62
63
  "scripts": {
63
64
  "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
64
65
  "test": "xo --fix && vitest run --coverage",
65
- "test:ci": "xo && vitest --run --sequence.setupFiles=list",
66
+ "test:ci": "xo && vitest --run --sequence.setupFiles=list --coverage",
66
67
  "clean": "rimraf ./node_modules ./coverage ./dist"
67
68
  }
68
69
  }