@keyv/redis 4.0.1 → 4.1.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
@@ -113,11 +113,11 @@ NOTE: If you plan to do many clears or deletes, it is recommended to read the [P
113
113
  # Performance Considerations
114
114
 
115
115
  With namespaces being prefix based it is critical to understand some of the performance considerations we have made:
116
- * `clear()` - We use the `SCAN` command to iterate over keys. This is a non-blocking command that is more efficient than `KEYS`. In addition we are using `UNLINK` by default instead of `DEL`. Even with that if you are iterating over a large dataset it can still be slow. It is highly recommended to use the `namespace` option to limit the keys that are being cleared and if possible to not use the `clear()` method in high performance environments.
116
+ * `clear()` - We use the `SCAN` command to iterate over keys. This is a non-blocking command that is more efficient than `KEYS`. In addition we are using `UNLINK` by default instead of `DEL`. Even with that if you are iterating over a large dataset it can still be slow. It is highly recommended to use the `namespace` option to limit the keys that are being cleared and if possible to not use the `clear()` method in high performance environments. If you don't set namespaces, you can enable `noNamespaceAffectsAll` to clear all keys using the `FLUSHDB` command which is faster and can be used in production environments.
117
117
 
118
118
  * `delete()` - By default we are now using `UNLINK` instead of `DEL` for deleting keys. This is a non-blocking command that is more efficient than `DEL`. If you are deleting a large number of keys it is recommended to use the `deleteMany()` method instead of `delete()`.
119
119
 
120
- * `clearBatchSize` - The `clearBatchSize` option is set to `1000` by default. This is because Redis has a limit of 1000 keys that can be deleted in a single batch.
120
+ * `clearBatchSize` - The `clearBatchSize` option is set to `1000` by default. This is because Redis has a limit of 1000 keys that can be deleted in a single batch. If no namespace is defined and noNamespaceAffectsAll is set to `true` this option will be ignored and the `FLUSHDB` command will be used instead.
121
121
 
122
122
  * `useUnlink` - This option is set to `true` by default. This is because `UNLINK` is a non-blocking command that is more efficient than `DEL`. If you are not using `UNLINK` and are doing a lot of deletes it is recommended to set this option to `true`.
123
123
 
@@ -183,7 +183,7 @@ const cluster = createCluster({
183
183
  const keyv = new Keyv({ store: new KeyvRedis(cluster) });
184
184
  ```
185
185
 
186
- 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.
186
+ 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.
187
187
 
188
188
  Here is an example of how to use TLS:
189
189
 
@@ -215,6 +215,7 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
215
215
  * **keyPrefixSeparator** - The separator to use between the namespace and key.
216
216
  * **clearBatchSize** - The number of keys to delete in a single batch.
217
217
  * **useUnlink** - Use the `UNLINK` command for deleting keys isntead of `DEL`.
218
+ * **noNamespaceAffectsAll**: Whether to allow clearing all keys when no namespace is set (default is `false`).
218
219
  * **set** - Set a key.
219
220
  * **setMany** - Set multiple keys.
220
221
  * **get** - Get a key.
@@ -223,9 +224,9 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
223
224
  * **hasMany** - Check if multiple keys exist.
224
225
  * **delete** - Delete a key.
225
226
  * **deleteMany** - Delete multiple keys.
226
- * **clear** - Clear all keys. If the `namespace` is set it will only clear keys with that namespace.
227
+ * **clear** - Clear all keys in the namespace. If the namespace is not set it will clear all keys that are not prefixed with a namespace unless `noNamespaceAffectsAll` is set to `true`.
227
228
  * **disconnect** - Disconnect from the Redis server.
228
- * **iterator** - Create a new iterator for the keys.
229
+ * **iterator** - Create a new iterator for the keys. If the namespace is not set it will iterate over all keys that are not prefixed with a namespace unless `noNamespaceAffectsAll` is set to `true`.
229
230
 
230
231
  # Migrating from v3 to v4
231
232
 
package/dist/index.cjs CHANGED
@@ -37,17 +37,19 @@ __export(src_exports, {
37
37
  default: () => KeyvRedis
38
38
  });
39
39
  module.exports = __toCommonJS(src_exports);
40
- var import_events = __toESM(require("events"), 1);
40
+ var import_node_events = __toESM(require("events"), 1);
41
41
  var import_redis = require("redis");
42
42
  var import_keyv = require("keyv");
43
+ var import_cluster_key_slot = __toESM(require("cluster-key-slot"), 1);
43
44
  var import_redis2 = require("redis");
44
45
  var import_keyv2 = require("keyv");
45
- var KeyvRedis = class extends import_events.default {
46
+ var KeyvRedis = class extends import_node_events.default {
46
47
  _client = (0, import_redis.createClient)();
47
48
  _namespace;
48
49
  _keyPrefixSeparator = "::";
49
50
  _clearBatchSize = 1e3;
50
51
  _useUnlink = true;
52
+ _noNamespaceAffectsAll = false;
51
53
  /**
52
54
  * KeyvRedis constructor.
53
55
  * @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.
@@ -59,9 +61,9 @@ var KeyvRedis = class extends import_events.default {
59
61
  if (typeof connect === "string") {
60
62
  this._client = (0, import_redis.createClient)({ url: connect });
61
63
  } else if (connect.connect !== void 0) {
62
- this._client = connect;
64
+ this._client = this.isClientCluster(connect) ? connect : connect;
63
65
  } else if (connect instanceof Object) {
64
- this._client = (0, import_redis.createClient)(connect);
66
+ this._client = connect.rootNodes === void 0 ? (0, import_redis.createClient)(connect) : (0, import_redis.createCluster)(connect);
65
67
  }
66
68
  }
67
69
  this.setOptions(options);
@@ -84,13 +86,19 @@ var KeyvRedis = class extends import_events.default {
84
86
  * Get the options for the adapter.
85
87
  */
86
88
  get opts() {
87
- return {
89
+ let url = "";
90
+ if (this._client.options) {
91
+ url = this._client.options?.url ?? "redis://localhost:6379";
92
+ }
93
+ const results = {
88
94
  namespace: this._namespace,
89
95
  keyPrefixSeparator: this._keyPrefixSeparator,
90
96
  clearBatchSize: this._clearBatchSize,
97
+ noNamespaceAffectsAll: this._noNamespaceAffectsAll,
91
98
  dialect: "redis",
92
- url: this._client?.options?.url ?? "redis://localhost:6379"
99
+ url
93
100
  };
101
+ return results;
94
102
  }
95
103
  /**
96
104
  * Set the options for the adapter.
@@ -150,6 +158,21 @@ var KeyvRedis = class extends import_events.default {
150
158
  set useUnlink(value) {
151
159
  this._useUnlink = value;
152
160
  }
161
+ /**
162
+ * Get if no namespace affects all keys.
163
+ * Whether to allow clearing all keys when no namespace is set.
164
+ * If set to true and no namespace is set, iterate() will return all keys.
165
+ * @default false
166
+ */
167
+ get noNamespaceAffectsAll() {
168
+ return this._noNamespaceAffectsAll;
169
+ }
170
+ /**
171
+ * Set if not namespace affects all keys.
172
+ */
173
+ set noNamespaceAffectsAll(value) {
174
+ this._noNamespaceAffectsAll = value;
175
+ }
153
176
  /**
154
177
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
155
178
  */
@@ -237,14 +260,12 @@ var KeyvRedis = class extends import_events.default {
237
260
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
238
261
  */
239
262
  async getMany(keys) {
240
- const client = await this.getClient();
241
- const multi = client.multi();
242
- for (const key of keys) {
243
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
244
- multi.get(prefixedKey);
263
+ if (keys.length === 0) {
264
+ return [];
245
265
  }
246
- const values = await multi.exec();
247
- return values.map((value) => value === null ? void 0 : value);
266
+ keys = keys.map((key) => this.createKeyPrefix(key, this._namespace));
267
+ const values = await this.mget(keys);
268
+ return values;
248
269
  }
249
270
  /**
250
271
  * Delete a key from the store.
@@ -255,11 +276,7 @@ var KeyvRedis = class extends import_events.default {
255
276
  const client = await this.getClient();
256
277
  key = this.createKeyPrefix(key, this._namespace);
257
278
  let deleted = 0;
258
- if (this._useUnlink) {
259
- deleted = await client.unlink(key);
260
- } else {
261
- deleted = await client.del(key);
262
- }
279
+ deleted = await (this._useUnlink ? client.unlink(key) : client.del(key));
263
280
  return deleted > 0;
264
281
  }
265
282
  /**
@@ -320,70 +337,157 @@ var KeyvRedis = class extends import_events.default {
320
337
  }
321
338
  return key;
322
339
  }
340
+ /**
341
+ * Is the client a cluster.
342
+ * @returns {boolean} - true if the client is a cluster, false if not
343
+ */
344
+ isCluster() {
345
+ return this.isClientCluster(this._client);
346
+ }
347
+ /**
348
+ * Get the master nodes in the cluster. If not a cluster, it will return the single client.
349
+ *
350
+ * @returns {Promise<RedisClientType[]>} - array of master nodes
351
+ */
352
+ async getMasterNodes() {
353
+ if (this.isCluster()) {
354
+ const cluster = await this.getClient();
355
+ return Promise.all(cluster.masters.map(async (main) => cluster.nodeClient(main)));
356
+ }
357
+ return [await this.getClient()];
358
+ }
323
359
  /**
324
360
  * Get an async iterator for the keys and values in the store. If a namespace is provided, it will only iterate over keys with that namespace.
325
361
  * @param {string} [namespace] - the namespace to iterate over
326
362
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
327
363
  */
328
364
  async *iterator(namespace) {
329
- const client = await this.getClient();
330
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
331
- let cursor = "0";
332
- do {
333
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
334
- cursor = result.cursor.toString();
335
- let { keys } = result;
336
- if (!namespace) {
337
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
338
- }
339
- if (keys.length > 0) {
340
- const values = await client.mGet(keys);
341
- for (const [i] of keys.entries()) {
342
- const key = this.getKeyWithoutPrefix(keys[i], namespace);
343
- const value = values ? values[i] : void 0;
344
- yield [key, value];
365
+ const clients = await this.getMasterNodes();
366
+ for (const client of clients) {
367
+ const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
368
+ let cursor = "0";
369
+ do {
370
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
371
+ cursor = result.cursor.toString();
372
+ let { keys } = result;
373
+ if (!namespace && !this._noNamespaceAffectsAll) {
374
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
345
375
  }
346
- }
347
- } while (cursor !== "0");
376
+ if (keys.length > 0) {
377
+ const values = await this.mget(keys);
378
+ for (const i of keys.keys()) {
379
+ const key = this.getKeyWithoutPrefix(keys[i], namespace);
380
+ const value = values[i];
381
+ yield [key, value];
382
+ }
383
+ }
384
+ } while (cursor !== "0");
385
+ }
348
386
  }
349
387
  /**
350
388
  * Clear all keys in the store.
351
- * IMPORTANT: this can cause performance issues if there are a large number of keys in the store. Use with caution as not recommended for production.
389
+ * IMPORTANT: this can cause performance issues if there are a large number of keys in the store and worse with clusters. Use with caution as not recommended for production.
352
390
  * If a namespace is not set it will clear all keys with no prefix.
353
391
  * If a namespace is set it will clear all keys with that namespace.
354
392
  * @returns {Promise<void>}
355
393
  */
356
394
  async clear() {
357
- await this.clearNamespace(this._namespace);
358
- }
359
- async clearNamespace(namespace) {
360
395
  try {
361
- let cursor = "0";
362
- const batchSize = this._clearBatchSize;
363
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
364
- const client = await this.getClient();
365
- do {
366
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
367
- cursor = result.cursor.toString();
368
- let { keys } = result;
369
- if (keys.length === 0) {
370
- continue;
371
- }
372
- if (!namespace) {
373
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
396
+ const clients = await this.getMasterNodes();
397
+ await Promise.all(clients.map(async (client) => {
398
+ if (!this._namespace && this._noNamespaceAffectsAll) {
399
+ await client.flushDb();
400
+ return;
374
401
  }
375
- if (keys.length > 0) {
376
- if (this._useUnlink) {
377
- await client.unlink(keys);
378
- } else {
379
- await client.del(keys);
402
+ let cursor = "0";
403
+ const batchSize = this._clearBatchSize;
404
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
405
+ const deletePromises = [];
406
+ do {
407
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
408
+ cursor = result.cursor.toString();
409
+ let { keys } = result;
410
+ if (keys.length === 0) {
411
+ continue;
380
412
  }
381
- }
382
- } while (cursor !== "0");
413
+ if (!this._namespace) {
414
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
415
+ }
416
+ deletePromises.push(this.clearWithClusterSupport(keys));
417
+ } while (cursor !== "0");
418
+ await Promise.all(deletePromises);
419
+ }));
383
420
  } catch (error) {
384
421
  this.emit("error", error);
385
422
  }
386
423
  }
424
+ /**
425
+ * Get many keys. If the instance is a cluster, it will do multiple MGET calls
426
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
427
+ */
428
+ async mget(keys) {
429
+ const slotMap = this.getSlotMap(keys);
430
+ const valueMap = /* @__PURE__ */ new Map();
431
+ await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
432
+ const client = await this.getSlotMaster(slot);
433
+ const values = await client.mGet(keys2);
434
+ for (const [index, value] of values.entries()) {
435
+ valueMap.set(keys2[index], value ?? void 0);
436
+ }
437
+ }));
438
+ return keys.map((key) => valueMap.get(key));
439
+ }
440
+ /**
441
+ * Clear all keys in the store with a specific namespace. If the instance is a cluster, it will clear all keys
442
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
443
+ */
444
+ async clearWithClusterSupport(keys) {
445
+ if (keys.length > 0) {
446
+ const slotMap = this.getSlotMap(keys);
447
+ await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
448
+ const client = await this.getSlotMaster(slot);
449
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
450
+ }));
451
+ }
452
+ }
453
+ /**
454
+ * Returns the master node client for a given slot or the instance's client if it's not a cluster.
455
+ */
456
+ async getSlotMaster(slot) {
457
+ const connection = await this.getClient();
458
+ if (this.isCluster()) {
459
+ const cluster = connection;
460
+ const mainNode = cluster.slots[slot].master;
461
+ return cluster.nodeClient(mainNode);
462
+ }
463
+ return connection;
464
+ }
465
+ /**
466
+ * Group keys by their slot.
467
+ *
468
+ * @param {string[]} keys - the keys to group
469
+ * @returns {Map<number, string[]>} - map of slot to keys
470
+ */
471
+ getSlotMap(keys) {
472
+ const slotMap = /* @__PURE__ */ new Map();
473
+ if (this.isCluster()) {
474
+ for (const key of keys) {
475
+ const slot = (0, import_cluster_key_slot.default)(key);
476
+ const slotKeys = slotMap.get(slot) ?? [];
477
+ slotKeys.push(key);
478
+ slotMap.set(slot, slotKeys);
479
+ }
480
+ } else {
481
+ slotMap.set(0, keys);
482
+ }
483
+ return slotMap;
484
+ }
485
+ isClientCluster(client) {
486
+ if (client.options === void 0 && client.scan === void 0) {
487
+ return true;
488
+ }
489
+ return false;
490
+ }
387
491
  setOptions(options) {
388
492
  if (!options) {
389
493
  return;
@@ -400,6 +504,9 @@ var KeyvRedis = class extends import_events.default {
400
504
  if (options.useUnlink !== void 0) {
401
505
  this._useUnlink = options.useUnlink;
402
506
  }
507
+ if (options.noNamespaceAffectsAll !== void 0) {
508
+ this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
509
+ }
403
510
  }
404
511
  initClient() {
405
512
  this._client.on("error", (error) => {
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import EventEmitter from 'events';
2
- import { RedisClientOptions, RedisClientType } from 'redis';
3
- export { RedisClientOptions, RedisClientType, createClient, createCluster } from 'redis';
1
+ import EventEmitter from 'node:events';
2
+ import { RedisClientType, RedisClusterType, RedisModules, RedisFunctions, RedisScripts, RedisClientOptions, RedisClusterOptions } from 'redis';
3
+ export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, createClient, createCluster } from 'redis';
4
4
  import { KeyvStoreAdapter, Keyv } from 'keyv';
5
5
  export { Keyv } from 'keyv';
6
6
 
@@ -21,6 +21,12 @@ type KeyvRedisOptions = {
21
21
  * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
22
22
  */
23
23
  useUnlink?: boolean;
24
+ /**
25
+ * Whether to allow clearing all keys when no namespace is set.
26
+ * If set to true and no namespace is set, iterate() will return all keys.
27
+ * Defaults to `false`.
28
+ */
29
+ noNamespaceAffectsAll?: boolean;
24
30
  };
25
31
  type KeyvRedisPropertyOptions = KeyvRedisOptions & {
26
32
  /**
@@ -46,26 +52,28 @@ type KeyvRedisEntry<T> = {
46
52
  */
47
53
  ttl?: number;
48
54
  };
55
+ type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
49
56
  declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
50
57
  private _client;
51
58
  private _namespace;
52
59
  private _keyPrefixSeparator;
53
60
  private _clearBatchSize;
54
61
  private _useUnlink;
62
+ private _noNamespaceAffectsAll;
55
63
  /**
56
64
  * KeyvRedis constructor.
57
65
  * @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.
58
66
  * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
59
67
  */
60
- constructor(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions);
68
+ constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
61
69
  /**
62
70
  * Get the Redis client.
63
71
  */
64
- get client(): RedisClientType;
72
+ get client(): RedisClientConnectionType;
65
73
  /**
66
74
  * Set the Redis client.
67
75
  */
68
- set client(value: RedisClientType);
76
+ set client(value: RedisClientConnectionType);
69
77
  /**
70
78
  * Get the options for the adapter.
71
79
  */
@@ -110,10 +118,21 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
110
118
  * Set if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
111
119
  */
112
120
  set useUnlink(value: boolean);
121
+ /**
122
+ * Get if no namespace affects all keys.
123
+ * Whether to allow clearing all keys when no namespace is set.
124
+ * If set to true and no namespace is set, iterate() will return all keys.
125
+ * @default false
126
+ */
127
+ get noNamespaceAffectsAll(): boolean;
128
+ /**
129
+ * Set if not namespace affects all keys.
130
+ */
131
+ set noNamespaceAffectsAll(value: boolean);
113
132
  /**
114
133
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
115
134
  */
116
- getClient(): Promise<RedisClientType>;
135
+ getClient(): Promise<RedisClientConnectionType>;
117
136
  /**
118
137
  * Set a key value pair in the store. TTL is in milliseconds.
119
138
  * @param {string} key - the key to set
@@ -181,6 +200,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
181
200
  * @returns {string} - the key without the namespace such as 'key'
182
201
  */
183
202
  getKeyWithoutPrefix(key: string, namespace?: string): string;
203
+ /**
204
+ * Is the client a cluster.
205
+ * @returns {boolean} - true if the client is a cluster, false if not
206
+ */
207
+ isCluster(): boolean;
208
+ /**
209
+ * Get the master nodes in the cluster. If not a cluster, it will return the single client.
210
+ *
211
+ * @returns {Promise<RedisClientType[]>} - array of master nodes
212
+ */
213
+ getMasterNodes(): Promise<RedisClientType[]>;
184
214
  /**
185
215
  * Get an async iterator for the keys and values in the store. If a namespace is provided, it will only iterate over keys with that namespace.
186
216
  * @param {string} [namespace] - the namespace to iterate over
@@ -189,13 +219,34 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
189
219
  iterator<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
190
220
  /**
191
221
  * Clear all keys in the store.
192
- * IMPORTANT: this can cause performance issues if there are a large number of keys in the store. Use with caution as not recommended for production.
222
+ * IMPORTANT: this can cause performance issues if there are a large number of keys in the store and worse with clusters. Use with caution as not recommended for production.
193
223
  * If a namespace is not set it will clear all keys with no prefix.
194
224
  * If a namespace is set it will clear all keys with that namespace.
195
225
  * @returns {Promise<void>}
196
226
  */
197
227
  clear(): Promise<void>;
198
- private clearNamespace;
228
+ /**
229
+ * Get many keys. If the instance is a cluster, it will do multiple MGET calls
230
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
231
+ */
232
+ private mget;
233
+ /**
234
+ * Clear all keys in the store with a specific namespace. If the instance is a cluster, it will clear all keys
235
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
236
+ */
237
+ private clearWithClusterSupport;
238
+ /**
239
+ * Returns the master node client for a given slot or the instance's client if it's not a cluster.
240
+ */
241
+ private getSlotMaster;
242
+ /**
243
+ * Group keys by their slot.
244
+ *
245
+ * @param {string[]} keys - the keys to group
246
+ * @returns {Map<number, string[]>} - map of slot to keys
247
+ */
248
+ private getSlotMap;
249
+ private isClientCluster;
199
250
  private setOptions;
200
251
  private initClient;
201
252
  }
@@ -207,4 +258,4 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
207
258
  */
208
259
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
209
260
 
210
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, createKeyv, KeyvRedis as default };
261
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, createKeyv, KeyvRedis as default };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import EventEmitter from 'events';
2
- import { RedisClientOptions, RedisClientType } from 'redis';
3
- export { RedisClientOptions, RedisClientType, createClient, createCluster } from 'redis';
1
+ import EventEmitter from 'node:events';
2
+ import { RedisClientType, RedisClusterType, RedisModules, RedisFunctions, RedisScripts, RedisClientOptions, RedisClusterOptions } from 'redis';
3
+ export { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType, createClient, createCluster } from 'redis';
4
4
  import { KeyvStoreAdapter, Keyv } from 'keyv';
5
5
  export { Keyv } from 'keyv';
6
6
 
@@ -21,6 +21,12 @@ type KeyvRedisOptions = {
21
21
  * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
22
22
  */
23
23
  useUnlink?: boolean;
24
+ /**
25
+ * Whether to allow clearing all keys when no namespace is set.
26
+ * If set to true and no namespace is set, iterate() will return all keys.
27
+ * Defaults to `false`.
28
+ */
29
+ noNamespaceAffectsAll?: boolean;
24
30
  };
25
31
  type KeyvRedisPropertyOptions = KeyvRedisOptions & {
26
32
  /**
@@ -46,26 +52,28 @@ type KeyvRedisEntry<T> = {
46
52
  */
47
53
  ttl?: number;
48
54
  };
55
+ type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
49
56
  declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
50
57
  private _client;
51
58
  private _namespace;
52
59
  private _keyPrefixSeparator;
53
60
  private _clearBatchSize;
54
61
  private _useUnlink;
62
+ private _noNamespaceAffectsAll;
55
63
  /**
56
64
  * KeyvRedis constructor.
57
65
  * @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.
58
66
  * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
59
67
  */
60
- constructor(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions);
68
+ constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
61
69
  /**
62
70
  * Get the Redis client.
63
71
  */
64
- get client(): RedisClientType;
72
+ get client(): RedisClientConnectionType;
65
73
  /**
66
74
  * Set the Redis client.
67
75
  */
68
- set client(value: RedisClientType);
76
+ set client(value: RedisClientConnectionType);
69
77
  /**
70
78
  * Get the options for the adapter.
71
79
  */
@@ -110,10 +118,21 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
110
118
  * Set if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
111
119
  */
112
120
  set useUnlink(value: boolean);
121
+ /**
122
+ * Get if no namespace affects all keys.
123
+ * Whether to allow clearing all keys when no namespace is set.
124
+ * If set to true and no namespace is set, iterate() will return all keys.
125
+ * @default false
126
+ */
127
+ get noNamespaceAffectsAll(): boolean;
128
+ /**
129
+ * Set if not namespace affects all keys.
130
+ */
131
+ set noNamespaceAffectsAll(value: boolean);
113
132
  /**
114
133
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
115
134
  */
116
- getClient(): Promise<RedisClientType>;
135
+ getClient(): Promise<RedisClientConnectionType>;
117
136
  /**
118
137
  * Set a key value pair in the store. TTL is in milliseconds.
119
138
  * @param {string} key - the key to set
@@ -181,6 +200,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
181
200
  * @returns {string} - the key without the namespace such as 'key'
182
201
  */
183
202
  getKeyWithoutPrefix(key: string, namespace?: string): string;
203
+ /**
204
+ * Is the client a cluster.
205
+ * @returns {boolean} - true if the client is a cluster, false if not
206
+ */
207
+ isCluster(): boolean;
208
+ /**
209
+ * Get the master nodes in the cluster. If not a cluster, it will return the single client.
210
+ *
211
+ * @returns {Promise<RedisClientType[]>} - array of master nodes
212
+ */
213
+ getMasterNodes(): Promise<RedisClientType[]>;
184
214
  /**
185
215
  * Get an async iterator for the keys and values in the store. If a namespace is provided, it will only iterate over keys with that namespace.
186
216
  * @param {string} [namespace] - the namespace to iterate over
@@ -189,13 +219,34 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
189
219
  iterator<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
190
220
  /**
191
221
  * Clear all keys in the store.
192
- * IMPORTANT: this can cause performance issues if there are a large number of keys in the store. Use with caution as not recommended for production.
222
+ * IMPORTANT: this can cause performance issues if there are a large number of keys in the store and worse with clusters. Use with caution as not recommended for production.
193
223
  * If a namespace is not set it will clear all keys with no prefix.
194
224
  * If a namespace is set it will clear all keys with that namespace.
195
225
  * @returns {Promise<void>}
196
226
  */
197
227
  clear(): Promise<void>;
198
- private clearNamespace;
228
+ /**
229
+ * Get many keys. If the instance is a cluster, it will do multiple MGET calls
230
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
231
+ */
232
+ private mget;
233
+ /**
234
+ * Clear all keys in the store with a specific namespace. If the instance is a cluster, it will clear all keys
235
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
236
+ */
237
+ private clearWithClusterSupport;
238
+ /**
239
+ * Returns the master node client for a given slot or the instance's client if it's not a cluster.
240
+ */
241
+ private getSlotMaster;
242
+ /**
243
+ * Group keys by their slot.
244
+ *
245
+ * @param {string[]} keys - the keys to group
246
+ * @returns {Map<number, string[]>} - map of slot to keys
247
+ */
248
+ private getSlotMap;
249
+ private isClientCluster;
199
250
  private setOptions;
200
251
  private initClient;
201
252
  }
@@ -207,4 +258,4 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
207
258
  */
208
259
  declare function createKeyv(connect?: string | RedisClientOptions | RedisClientType, options?: KeyvRedisOptions): Keyv;
209
260
 
210
- export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, createKeyv, KeyvRedis as default };
261
+ export { type KeyvRedisEntry, type KeyvRedisOptions, type KeyvRedisPropertyOptions, type RedisClientConnectionType, createKeyv, KeyvRedis as default };
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  // src/index.ts
2
- import EventEmitter from "events";
3
- import { createClient } from "redis";
2
+ import EventEmitter from "node:events";
3
+ import {
4
+ createClient,
5
+ createCluster
6
+ } from "redis";
4
7
  import { Keyv } from "keyv";
8
+ import calculateSlot from "cluster-key-slot";
5
9
  import {
6
10
  createClient as createClient2,
7
- createCluster
11
+ createCluster as createCluster2
8
12
  } from "redis";
9
13
  import {
10
14
  Keyv as Keyv2
@@ -15,6 +19,7 @@ var KeyvRedis = class extends EventEmitter {
15
19
  _keyPrefixSeparator = "::";
16
20
  _clearBatchSize = 1e3;
17
21
  _useUnlink = true;
22
+ _noNamespaceAffectsAll = false;
18
23
  /**
19
24
  * KeyvRedis constructor.
20
25
  * @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.
@@ -26,9 +31,9 @@ var KeyvRedis = class extends EventEmitter {
26
31
  if (typeof connect === "string") {
27
32
  this._client = createClient({ url: connect });
28
33
  } else if (connect.connect !== void 0) {
29
- this._client = connect;
34
+ this._client = this.isClientCluster(connect) ? connect : connect;
30
35
  } else if (connect instanceof Object) {
31
- this._client = createClient(connect);
36
+ this._client = connect.rootNodes === void 0 ? createClient(connect) : createCluster(connect);
32
37
  }
33
38
  }
34
39
  this.setOptions(options);
@@ -51,13 +56,19 @@ var KeyvRedis = class extends EventEmitter {
51
56
  * Get the options for the adapter.
52
57
  */
53
58
  get opts() {
54
- return {
59
+ let url = "";
60
+ if (this._client.options) {
61
+ url = this._client.options?.url ?? "redis://localhost:6379";
62
+ }
63
+ const results = {
55
64
  namespace: this._namespace,
56
65
  keyPrefixSeparator: this._keyPrefixSeparator,
57
66
  clearBatchSize: this._clearBatchSize,
67
+ noNamespaceAffectsAll: this._noNamespaceAffectsAll,
58
68
  dialect: "redis",
59
- url: this._client?.options?.url ?? "redis://localhost:6379"
69
+ url
60
70
  };
71
+ return results;
61
72
  }
62
73
  /**
63
74
  * Set the options for the adapter.
@@ -117,6 +128,21 @@ var KeyvRedis = class extends EventEmitter {
117
128
  set useUnlink(value) {
118
129
  this._useUnlink = value;
119
130
  }
131
+ /**
132
+ * Get if no namespace affects all keys.
133
+ * Whether to allow clearing all keys when no namespace is set.
134
+ * If set to true and no namespace is set, iterate() will return all keys.
135
+ * @default false
136
+ */
137
+ get noNamespaceAffectsAll() {
138
+ return this._noNamespaceAffectsAll;
139
+ }
140
+ /**
141
+ * Set if not namespace affects all keys.
142
+ */
143
+ set noNamespaceAffectsAll(value) {
144
+ this._noNamespaceAffectsAll = value;
145
+ }
120
146
  /**
121
147
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
122
148
  */
@@ -204,14 +230,12 @@ var KeyvRedis = class extends EventEmitter {
204
230
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
205
231
  */
206
232
  async getMany(keys) {
207
- const client = await this.getClient();
208
- const multi = client.multi();
209
- for (const key of keys) {
210
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
211
- multi.get(prefixedKey);
233
+ if (keys.length === 0) {
234
+ return [];
212
235
  }
213
- const values = await multi.exec();
214
- return values.map((value) => value === null ? void 0 : value);
236
+ keys = keys.map((key) => this.createKeyPrefix(key, this._namespace));
237
+ const values = await this.mget(keys);
238
+ return values;
215
239
  }
216
240
  /**
217
241
  * Delete a key from the store.
@@ -222,11 +246,7 @@ var KeyvRedis = class extends EventEmitter {
222
246
  const client = await this.getClient();
223
247
  key = this.createKeyPrefix(key, this._namespace);
224
248
  let deleted = 0;
225
- if (this._useUnlink) {
226
- deleted = await client.unlink(key);
227
- } else {
228
- deleted = await client.del(key);
229
- }
249
+ deleted = await (this._useUnlink ? client.unlink(key) : client.del(key));
230
250
  return deleted > 0;
231
251
  }
232
252
  /**
@@ -287,70 +307,157 @@ var KeyvRedis = class extends EventEmitter {
287
307
  }
288
308
  return key;
289
309
  }
310
+ /**
311
+ * Is the client a cluster.
312
+ * @returns {boolean} - true if the client is a cluster, false if not
313
+ */
314
+ isCluster() {
315
+ return this.isClientCluster(this._client);
316
+ }
317
+ /**
318
+ * Get the master nodes in the cluster. If not a cluster, it will return the single client.
319
+ *
320
+ * @returns {Promise<RedisClientType[]>} - array of master nodes
321
+ */
322
+ async getMasterNodes() {
323
+ if (this.isCluster()) {
324
+ const cluster = await this.getClient();
325
+ return Promise.all(cluster.masters.map(async (main) => cluster.nodeClient(main)));
326
+ }
327
+ return [await this.getClient()];
328
+ }
290
329
  /**
291
330
  * Get an async iterator for the keys and values in the store. If a namespace is provided, it will only iterate over keys with that namespace.
292
331
  * @param {string} [namespace] - the namespace to iterate over
293
332
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
294
333
  */
295
334
  async *iterator(namespace) {
296
- const client = await this.getClient();
297
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
298
- let cursor = "0";
299
- do {
300
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
301
- cursor = result.cursor.toString();
302
- let { keys } = result;
303
- if (!namespace) {
304
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
305
- }
306
- if (keys.length > 0) {
307
- const values = await client.mGet(keys);
308
- for (const [i] of keys.entries()) {
309
- const key = this.getKeyWithoutPrefix(keys[i], namespace);
310
- const value = values ? values[i] : void 0;
311
- yield [key, value];
335
+ const clients = await this.getMasterNodes();
336
+ for (const client of clients) {
337
+ const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
338
+ let cursor = "0";
339
+ do {
340
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
341
+ cursor = result.cursor.toString();
342
+ let { keys } = result;
343
+ if (!namespace && !this._noNamespaceAffectsAll) {
344
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
312
345
  }
313
- }
314
- } while (cursor !== "0");
346
+ if (keys.length > 0) {
347
+ const values = await this.mget(keys);
348
+ for (const i of keys.keys()) {
349
+ const key = this.getKeyWithoutPrefix(keys[i], namespace);
350
+ const value = values[i];
351
+ yield [key, value];
352
+ }
353
+ }
354
+ } while (cursor !== "0");
355
+ }
315
356
  }
316
357
  /**
317
358
  * Clear all keys in the store.
318
- * IMPORTANT: this can cause performance issues if there are a large number of keys in the store. Use with caution as not recommended for production.
359
+ * IMPORTANT: this can cause performance issues if there are a large number of keys in the store and worse with clusters. Use with caution as not recommended for production.
319
360
  * If a namespace is not set it will clear all keys with no prefix.
320
361
  * If a namespace is set it will clear all keys with that namespace.
321
362
  * @returns {Promise<void>}
322
363
  */
323
364
  async clear() {
324
- await this.clearNamespace(this._namespace);
325
- }
326
- async clearNamespace(namespace) {
327
365
  try {
328
- let cursor = "0";
329
- const batchSize = this._clearBatchSize;
330
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
331
- const client = await this.getClient();
332
- do {
333
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
334
- cursor = result.cursor.toString();
335
- let { keys } = result;
336
- if (keys.length === 0) {
337
- continue;
338
- }
339
- if (!namespace) {
340
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
366
+ const clients = await this.getMasterNodes();
367
+ await Promise.all(clients.map(async (client) => {
368
+ if (!this._namespace && this._noNamespaceAffectsAll) {
369
+ await client.flushDb();
370
+ return;
341
371
  }
342
- if (keys.length > 0) {
343
- if (this._useUnlink) {
344
- await client.unlink(keys);
345
- } else {
346
- await client.del(keys);
372
+ let cursor = "0";
373
+ const batchSize = this._clearBatchSize;
374
+ const match = this._namespace ? `${this._namespace}${this._keyPrefixSeparator}*` : "*";
375
+ const deletePromises = [];
376
+ do {
377
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
378
+ cursor = result.cursor.toString();
379
+ let { keys } = result;
380
+ if (keys.length === 0) {
381
+ continue;
347
382
  }
348
- }
349
- } while (cursor !== "0");
383
+ if (!this._namespace) {
384
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
385
+ }
386
+ deletePromises.push(this.clearWithClusterSupport(keys));
387
+ } while (cursor !== "0");
388
+ await Promise.all(deletePromises);
389
+ }));
350
390
  } catch (error) {
351
391
  this.emit("error", error);
352
392
  }
353
393
  }
394
+ /**
395
+ * Get many keys. If the instance is a cluster, it will do multiple MGET calls
396
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
397
+ */
398
+ async mget(keys) {
399
+ const slotMap = this.getSlotMap(keys);
400
+ const valueMap = /* @__PURE__ */ new Map();
401
+ await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
402
+ const client = await this.getSlotMaster(slot);
403
+ const values = await client.mGet(keys2);
404
+ for (const [index, value] of values.entries()) {
405
+ valueMap.set(keys2[index], value ?? void 0);
406
+ }
407
+ }));
408
+ return keys.map((key) => valueMap.get(key));
409
+ }
410
+ /**
411
+ * Clear all keys in the store with a specific namespace. If the instance is a cluster, it will clear all keys
412
+ * by separating the keys by slot to solve the CROSS-SLOT restriction.
413
+ */
414
+ async clearWithClusterSupport(keys) {
415
+ if (keys.length > 0) {
416
+ const slotMap = this.getSlotMap(keys);
417
+ await Promise.all(Array.from(slotMap.entries(), async ([slot, keys2]) => {
418
+ const client = await this.getSlotMaster(slot);
419
+ return this._useUnlink ? client.unlink(keys2) : client.del(keys2);
420
+ }));
421
+ }
422
+ }
423
+ /**
424
+ * Returns the master node client for a given slot or the instance's client if it's not a cluster.
425
+ */
426
+ async getSlotMaster(slot) {
427
+ const connection = await this.getClient();
428
+ if (this.isCluster()) {
429
+ const cluster = connection;
430
+ const mainNode = cluster.slots[slot].master;
431
+ return cluster.nodeClient(mainNode);
432
+ }
433
+ return connection;
434
+ }
435
+ /**
436
+ * Group keys by their slot.
437
+ *
438
+ * @param {string[]} keys - the keys to group
439
+ * @returns {Map<number, string[]>} - map of slot to keys
440
+ */
441
+ getSlotMap(keys) {
442
+ const slotMap = /* @__PURE__ */ new Map();
443
+ if (this.isCluster()) {
444
+ for (const key of keys) {
445
+ const slot = calculateSlot(key);
446
+ const slotKeys = slotMap.get(slot) ?? [];
447
+ slotKeys.push(key);
448
+ slotMap.set(slot, slotKeys);
449
+ }
450
+ } else {
451
+ slotMap.set(0, keys);
452
+ }
453
+ return slotMap;
454
+ }
455
+ isClientCluster(client) {
456
+ if (client.options === void 0 && client.scan === void 0) {
457
+ return true;
458
+ }
459
+ return false;
460
+ }
354
461
  setOptions(options) {
355
462
  if (!options) {
356
463
  return;
@@ -367,6 +474,9 @@ var KeyvRedis = class extends EventEmitter {
367
474
  if (options.useUnlink !== void 0) {
368
475
  this._useUnlink = options.useUnlink;
369
476
  }
477
+ if (options.noNamespaceAffectsAll !== void 0) {
478
+ this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
479
+ }
370
480
  }
371
481
  initClient() {
372
482
  this._client.on("error", (error) => {
@@ -382,7 +492,7 @@ function createKeyv(connect, options) {
382
492
  export {
383
493
  Keyv2 as Keyv,
384
494
  createClient2 as createClient,
385
- createCluster,
495
+ createCluster2 as createCluster,
386
496
  createKeyv,
387
497
  KeyvRedis as default
388
498
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/redis",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "Redis storage adapter for Keyv",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -12,23 +12,6 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "xo": {
16
- "rules": {
17
- "import/no-named-as-default": "off",
18
- "unicorn/prefer-module": "off",
19
- "unicorn/prefer-event-target": "off",
20
- "unicorn/prefer-node-protocol": "off",
21
- "unicorn/no-typeof-undefined": "off",
22
- "import/extensions": "off",
23
- "@typescript-eslint/no-unsafe-call": "off",
24
- "@typescript-eslint/no-unsafe-assignment": "off",
25
- "@typescript-eslint/no-unsafe-return": "off",
26
- "unicorn/prefer-ternary": "off",
27
- "unicorn/no-array-callback-reference": "off",
28
- "import/no-extraneous-dependencies": "off",
29
- "@typescript-eslint/no-confusing-void-expression": "off"
30
- }
31
- },
32
15
  "repository": {
33
16
  "type": "git",
34
17
  "url": "git+https://github.com/jaredwray/keyv.git"
@@ -51,15 +34,18 @@
51
34
  },
52
35
  "homepage": "https://github.com/jaredwray/keyv",
53
36
  "dependencies": {
37
+ "cluster-key-slot": "^1.1.2",
54
38
  "redis": "^4.7.0",
55
- "keyv": "*"
39
+ "keyv": "^5.2.1"
56
40
  },
57
41
  "devDependencies": {
58
- "@keyv/test-suite": "*",
42
+ "@vitest/coverage-v8": "^2.1.8",
59
43
  "rimraf": "^6.0.1",
60
44
  "timekeeper": "^2.3.1",
61
45
  "tsd": "^0.31.2",
62
- "xo": "^0.59.3"
46
+ "vitest": "^2.1.8",
47
+ "xo": "^0.60.0",
48
+ "@keyv/test-suite": "^2.0.3"
63
49
  },
64
50
  "tsd": {
65
51
  "directory": "test"