@keyv/redis 4.0.2 → 4.2.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,6 +26,7 @@ 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
30
  * [Performance Considerations](#performance-considerations)
30
31
  * [High Memory Usage on Redis Server](#high-memory-usage-on-redis-server)
31
32
  * [Using Cacheable with Redis](#using-cacheable-with-redis)
@@ -110,14 +111,42 @@ keyv.namespace = 'my-namespace';
110
111
 
111
112
  NOTE: If you plan to do many clears or deletes, it is recommended to read the [Performance Considerations](#performance-considerations) section.
112
113
 
114
+ ## Typescript
115
+
116
+ When initializing `KeyvRedis`, you can specify the type of the values you are storing and you can also specify types when calling methods:
117
+
118
+ ```typescript
119
+ import Keyv from 'keyv';
120
+ import KeyvRedis, { createClient } from '@keyv/redis';
121
+
122
+
123
+ interface User {
124
+ id: number
125
+ name: string
126
+ }
127
+
128
+ const redis = createClient('redis://user:pass@localhost:6379');
129
+
130
+ const keyvRedis = new KeyvRedis<User>(redis);
131
+ const keyv = new Keyv({ store: keyvRedis });
132
+
133
+ await keyv.set("user:1", { id: 1, name: "Alice" })
134
+ const user = await keyv.get("user:1")
135
+ console.log(user.name) // 'Alice'
136
+
137
+ // specify types when calling methods
138
+ const user = await keyv.get<User>("user:1")
139
+ console.log(user.name) // 'Alice'
140
+ ```
141
+
113
142
  # Performance Considerations
114
143
 
115
144
  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.
145
+ * `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
146
 
118
147
  * `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
148
 
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.
149
+ * `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
150
 
122
151
  * `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
152
 
@@ -183,8 +212,6 @@ const cluster = createCluster({
183
212
  const keyv = new Keyv({ store: new KeyvRedis(cluster) });
184
213
  ```
185
214
 
186
- There are some features that are not supported in clustering such as `clear()` and `iterator()`. This is because the `SCAN` command is not supported in clustering. If you need to clear or delete keys you can use the `deleteMany()` method.
187
-
188
215
  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.
189
216
 
190
217
  Here is an example of how to use TLS:
@@ -217,6 +244,7 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
217
244
  * **keyPrefixSeparator** - The separator to use between the namespace and key.
218
245
  * **clearBatchSize** - The number of keys to delete in a single batch.
219
246
  * **useUnlink** - Use the `UNLINK` command for deleting keys isntead of `DEL`.
247
+ * **noNamespaceAffectsAll**: Whether to allow clearing all keys when no namespace is set (default is `false`).
220
248
  * **set** - Set a key.
221
249
  * **setMany** - Set multiple keys.
222
250
  * **get** - Get a key.
@@ -225,9 +253,9 @@ const keyv = new Keyv({ store: new KeyvRedis(tlsOptions) });
225
253
  * **hasMany** - Check if multiple keys exist.
226
254
  * **delete** - Delete a key.
227
255
  * **deleteMany** - Delete multiple keys.
228
- * **clear** - Clear all keys. If the `namespace` is set it will only clear keys with that namespace.
256
+ * **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`.
229
257
  * **disconnect** - Disconnect from the Redis server.
230
- * **iterator** - Create a new iterator for the keys.
258
+ * **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`.
231
259
 
232
260
  # Migrating from v3 to v4
233
261
 
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ module.exports = __toCommonJS(src_exports);
40
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
46
  var KeyvRedis = class extends import_node_events.default {
@@ -48,6 +49,7 @@ var KeyvRedis = class extends import_node_events.default {
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.
@@ -92,6 +94,7 @@ var KeyvRedis = class extends import_node_events.default {
92
94
  namespace: this._namespace,
93
95
  keyPrefixSeparator: this._keyPrefixSeparator,
94
96
  clearBatchSize: this._clearBatchSize,
97
+ noNamespaceAffectsAll: this._noNamespaceAffectsAll,
95
98
  dialect: "redis",
96
99
  url
97
100
  };
@@ -155,6 +158,21 @@ var KeyvRedis = class extends import_node_events.default {
155
158
  set useUnlink(value) {
156
159
  this._useUnlink = value;
157
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
+ }
158
176
  /**
159
177
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
160
178
  */
@@ -242,14 +260,12 @@ var KeyvRedis = class extends import_node_events.default {
242
260
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
243
261
  */
244
262
  async getMany(keys) {
245
- const client = await this.getClient();
246
- const multi = client.multi();
247
- for (const key of keys) {
248
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
249
- multi.get(prefixedKey);
263
+ if (keys.length === 0) {
264
+ return [];
250
265
  }
251
- const values = await multi.exec();
252
- 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;
253
269
  }
254
270
  /**
255
271
  * Delete a key from the store.
@@ -329,42 +345,44 @@ var KeyvRedis = class extends import_node_events.default {
329
345
  return this.isClientCluster(this._client);
330
346
  }
331
347
  /**
332
- * 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.
333
- * @param {string} [namespace] - the namespace to iterate over
334
- * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
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
335
351
  */
336
- async *iterator(namespace) {
352
+ async getMasterNodes() {
337
353
  if (this.isCluster()) {
338
- throw new Error("Iterating over keys in a cluster is not supported.");
339
- } else {
340
- yield* this.iteratorClient(namespace);
354
+ const cluster = await this.getClient();
355
+ return Promise.all(cluster.masters.map(async (main) => cluster.nodeClient(main)));
341
356
  }
357
+ return [await this.getClient()];
342
358
  }
343
359
  /**
344
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.
345
361
  * @param {string} [namespace] - the namespace to iterate over
346
362
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
347
363
  */
348
- async *iteratorClient(namespace) {
349
- const client = await this.getClient();
350
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
351
- let cursor = "0";
352
- do {
353
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
354
- cursor = result.cursor.toString();
355
- let { keys } = result;
356
- if (!namespace) {
357
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
358
- }
359
- if (keys.length > 0) {
360
- const values = await client.mGet(keys);
361
- for (const [i] of keys.entries()) {
362
- const key = this.getKeyWithoutPrefix(keys[i], namespace);
363
- const value = values ? values[i] : void 0;
364
- yield [key, value];
364
+ async *iterator(namespace) {
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));
365
375
  }
366
- }
367
- } 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
+ }
368
386
  }
369
387
  /**
370
388
  * Clear all keys in the store.
@@ -374,38 +392,95 @@ var KeyvRedis = class extends import_node_events.default {
374
392
  * @returns {Promise<void>}
375
393
  */
376
394
  async clear() {
377
- await (this.isCluster() ? this.clearNamespaceCluster(this._namespace) : this.clearNamespace(this._namespace));
378
- }
379
- async clearNamespace(namespace) {
380
395
  try {
381
- let cursor = "0";
382
- const batchSize = this._clearBatchSize;
383
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
384
- const client = await this.getClient();
385
- do {
386
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
387
- cursor = result.cursor.toString();
388
- let { keys } = result;
389
- if (keys.length === 0) {
390
- continue;
391
- }
392
- if (!namespace) {
393
- 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;
394
401
  }
395
- if (keys.length > 0) {
396
- if (this._useUnlink) {
397
- await client.unlink(keys);
398
- } else {
399
- 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;
400
412
  }
401
- }
402
- } 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
+ }));
403
420
  } catch (error) {
404
421
  this.emit("error", error);
405
422
  }
406
423
  }
407
- async clearNamespaceCluster(namespace) {
408
- throw new Error("Clearing all keys in a cluster is not supported.");
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;
409
484
  }
410
485
  isClientCluster(client) {
411
486
  if (client.options === void 0 && client.scan === void 0) {
@@ -429,6 +504,9 @@ var KeyvRedis = class extends import_node_events.default {
429
504
  if (options.useUnlink !== void 0) {
430
505
  this._useUnlink = options.useUnlink;
431
506
  }
507
+ if (options.noNamespaceAffectsAll !== void 0) {
508
+ this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
509
+ }
432
510
  }
433
511
  initClient() {
434
512
  this._client.on("error", (error) => {
package/dist/index.d.cts CHANGED
@@ -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
  /**
@@ -47,12 +53,13 @@ type KeyvRedisEntry<T> = {
47
53
  ttl?: number;
48
54
  };
49
55
  type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
50
- declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
56
+ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
51
57
  private _client;
52
58
  private _namespace;
53
59
  private _keyPrefixSeparator;
54
60
  private _clearBatchSize;
55
61
  private _useUnlink;
62
+ private _noNamespaceAffectsAll;
56
63
  /**
57
64
  * KeyvRedis constructor.
58
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.
@@ -111,6 +118,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
111
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.
112
119
  */
113
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);
114
132
  /**
115
133
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
116
134
  */
@@ -144,13 +162,13 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
144
162
  * @param {string} key - the key to get
145
163
  * @returns {Promise<string | undefined>} - the value or undefined if the key does not exist
146
164
  */
147
- get<T>(key: string): Promise<T | undefined>;
165
+ get<U = T>(key: string): Promise<U | undefined>;
148
166
  /**
149
167
  * Get many values from the store. If a key does not exist, it will return undefined.
150
168
  * @param {Array<string>} keys - the keys to get
151
169
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
152
170
  */
153
- getMany<T>(keys: string[]): Promise<Array<T | undefined>>;
171
+ getMany<U = T>(keys: string[]): Promise<Array<U | undefined>>;
154
172
  /**
155
173
  * Delete a key from the store.
156
174
  * @param {string} key - the key to delete
@@ -188,17 +206,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
188
206
  */
189
207
  isCluster(): boolean;
190
208
  /**
191
- * 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.
192
- * @param {string} [namespace] - the namespace to iterate over
193
- * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
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
194
212
  */
195
- iterator<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
213
+ getMasterNodes(): Promise<RedisClientType[]>;
196
214
  /**
197
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.
198
216
  * @param {string} [namespace] - the namespace to iterate over
199
217
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
200
218
  */
201
- iteratorClient<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
219
+ iterator<U = T>(namespace?: string): AsyncGenerator<[string, U | undefined], void, unknown>;
202
220
  /**
203
221
  * Clear all keys in the store.
204
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.
@@ -207,8 +225,27 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
207
225
  * @returns {Promise<void>}
208
226
  */
209
227
  clear(): Promise<void>;
210
- private clearNamespace;
211
- private clearNamespaceCluster;
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;
212
249
  private isClientCluster;
213
250
  private setOptions;
214
251
  private initClient;
package/dist/index.d.ts CHANGED
@@ -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
  /**
@@ -47,12 +53,13 @@ type KeyvRedisEntry<T> = {
47
53
  ttl?: number;
48
54
  };
49
55
  type RedisClientConnectionType = RedisClientType | RedisClusterType<RedisModules, RedisFunctions, RedisScripts>;
50
- declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
56
+ declare class KeyvRedis<T> extends EventEmitter implements KeyvStoreAdapter {
51
57
  private _client;
52
58
  private _namespace;
53
59
  private _keyPrefixSeparator;
54
60
  private _clearBatchSize;
55
61
  private _useUnlink;
62
+ private _noNamespaceAffectsAll;
56
63
  /**
57
64
  * KeyvRedis constructor.
58
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.
@@ -111,6 +118,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
111
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.
112
119
  */
113
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);
114
132
  /**
115
133
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
116
134
  */
@@ -144,13 +162,13 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
144
162
  * @param {string} key - the key to get
145
163
  * @returns {Promise<string | undefined>} - the value or undefined if the key does not exist
146
164
  */
147
- get<T>(key: string): Promise<T | undefined>;
165
+ get<U = T>(key: string): Promise<U | undefined>;
148
166
  /**
149
167
  * Get many values from the store. If a key does not exist, it will return undefined.
150
168
  * @param {Array<string>} keys - the keys to get
151
169
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
152
170
  */
153
- getMany<T>(keys: string[]): Promise<Array<T | undefined>>;
171
+ getMany<U = T>(keys: string[]): Promise<Array<U | undefined>>;
154
172
  /**
155
173
  * Delete a key from the store.
156
174
  * @param {string} key - the key to delete
@@ -188,17 +206,17 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
188
206
  */
189
207
  isCluster(): boolean;
190
208
  /**
191
- * 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.
192
- * @param {string} [namespace] - the namespace to iterate over
193
- * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
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
194
212
  */
195
- iterator<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
213
+ getMasterNodes(): Promise<RedisClientType[]>;
196
214
  /**
197
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.
198
216
  * @param {string} [namespace] - the namespace to iterate over
199
217
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
200
218
  */
201
- iteratorClient<Value>(namespace?: string): AsyncGenerator<[string, Value | undefined], void, unknown>;
219
+ iterator<U = T>(namespace?: string): AsyncGenerator<[string, U | undefined], void, unknown>;
202
220
  /**
203
221
  * Clear all keys in the store.
204
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.
@@ -207,8 +225,27 @@ declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
207
225
  * @returns {Promise<void>}
208
226
  */
209
227
  clear(): Promise<void>;
210
- private clearNamespace;
211
- private clearNamespaceCluster;
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;
212
249
  private isClientCluster;
213
250
  private setOptions;
214
251
  private initClient;
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  createCluster
6
6
  } from "redis";
7
7
  import { Keyv } from "keyv";
8
+ import calculateSlot from "cluster-key-slot";
8
9
  import {
9
10
  createClient as createClient2,
10
11
  createCluster as createCluster2
@@ -18,6 +19,7 @@ var KeyvRedis = class extends EventEmitter {
18
19
  _keyPrefixSeparator = "::";
19
20
  _clearBatchSize = 1e3;
20
21
  _useUnlink = true;
22
+ _noNamespaceAffectsAll = false;
21
23
  /**
22
24
  * KeyvRedis constructor.
23
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.
@@ -62,6 +64,7 @@ var KeyvRedis = class extends EventEmitter {
62
64
  namespace: this._namespace,
63
65
  keyPrefixSeparator: this._keyPrefixSeparator,
64
66
  clearBatchSize: this._clearBatchSize,
67
+ noNamespaceAffectsAll: this._noNamespaceAffectsAll,
65
68
  dialect: "redis",
66
69
  url
67
70
  };
@@ -125,6 +128,21 @@ var KeyvRedis = class extends EventEmitter {
125
128
  set useUnlink(value) {
126
129
  this._useUnlink = value;
127
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
+ }
128
146
  /**
129
147
  * Get the Redis URL used to connect to the server. This is used to get a connected client.
130
148
  */
@@ -212,14 +230,12 @@ var KeyvRedis = class extends EventEmitter {
212
230
  * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
213
231
  */
214
232
  async getMany(keys) {
215
- const client = await this.getClient();
216
- const multi = client.multi();
217
- for (const key of keys) {
218
- const prefixedKey = this.createKeyPrefix(key, this._namespace);
219
- multi.get(prefixedKey);
233
+ if (keys.length === 0) {
234
+ return [];
220
235
  }
221
- const values = await multi.exec();
222
- 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;
223
239
  }
224
240
  /**
225
241
  * Delete a key from the store.
@@ -299,42 +315,44 @@ var KeyvRedis = class extends EventEmitter {
299
315
  return this.isClientCluster(this._client);
300
316
  }
301
317
  /**
302
- * 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.
303
- * @param {string} [namespace] - the namespace to iterate over
304
- * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
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
305
321
  */
306
- async *iterator(namespace) {
322
+ async getMasterNodes() {
307
323
  if (this.isCluster()) {
308
- throw new Error("Iterating over keys in a cluster is not supported.");
309
- } else {
310
- yield* this.iteratorClient(namespace);
324
+ const cluster = await this.getClient();
325
+ return Promise.all(cluster.masters.map(async (main) => cluster.nodeClient(main)));
311
326
  }
327
+ return [await this.getClient()];
312
328
  }
313
329
  /**
314
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.
315
331
  * @param {string} [namespace] - the namespace to iterate over
316
332
  * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
317
333
  */
318
- async *iteratorClient(namespace) {
319
- const client = await this.getClient();
320
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
321
- let cursor = "0";
322
- do {
323
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
324
- cursor = result.cursor.toString();
325
- let { keys } = result;
326
- if (!namespace) {
327
- keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
328
- }
329
- if (keys.length > 0) {
330
- const values = await client.mGet(keys);
331
- for (const [i] of keys.entries()) {
332
- const key = this.getKeyWithoutPrefix(keys[i], namespace);
333
- const value = values ? values[i] : void 0;
334
- yield [key, value];
334
+ async *iterator(namespace) {
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));
335
345
  }
336
- }
337
- } 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
+ }
338
356
  }
339
357
  /**
340
358
  * Clear all keys in the store.
@@ -344,38 +362,95 @@ var KeyvRedis = class extends EventEmitter {
344
362
  * @returns {Promise<void>}
345
363
  */
346
364
  async clear() {
347
- await (this.isCluster() ? this.clearNamespaceCluster(this._namespace) : this.clearNamespace(this._namespace));
348
- }
349
- async clearNamespace(namespace) {
350
365
  try {
351
- let cursor = "0";
352
- const batchSize = this._clearBatchSize;
353
- const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
354
- const client = await this.getClient();
355
- do {
356
- const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
357
- cursor = result.cursor.toString();
358
- let { keys } = result;
359
- if (keys.length === 0) {
360
- continue;
361
- }
362
- if (!namespace) {
363
- 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;
364
371
  }
365
- if (keys.length > 0) {
366
- if (this._useUnlink) {
367
- await client.unlink(keys);
368
- } else {
369
- 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;
370
382
  }
371
- }
372
- } 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
+ }));
373
390
  } catch (error) {
374
391
  this.emit("error", error);
375
392
  }
376
393
  }
377
- async clearNamespaceCluster(namespace) {
378
- throw new Error("Clearing all keys in a cluster is not supported.");
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;
379
454
  }
380
455
  isClientCluster(client) {
381
456
  if (client.options === void 0 && client.scan === void 0) {
@@ -399,6 +474,9 @@ var KeyvRedis = class extends EventEmitter {
399
474
  if (options.useUnlink !== void 0) {
400
475
  this._useUnlink = options.useUnlink;
401
476
  }
477
+ if (options.noNamespaceAffectsAll !== void 0) {
478
+ this._noNamespaceAffectsAll = options.noNamespaceAffectsAll;
479
+ }
402
480
  }
403
481
  initClient() {
404
482
  this._client.on("error", (error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyv/redis",
3
- "version": "4.0.2",
3
+ "version": "4.2.0",
4
4
  "description": "Redis storage adapter for Keyv",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -34,17 +34,18 @@
34
34
  },
35
35
  "homepage": "https://github.com/jaredwray/keyv",
36
36
  "dependencies": {
37
- "keyv": "*",
38
- "redis": "^4.7.0"
37
+ "cluster-key-slot": "^1.1.2",
38
+ "redis": "^4.7.0",
39
+ "keyv": "^5.2.2"
39
40
  },
40
41
  "devDependencies": {
41
- "@keyv/test-suite": "*",
42
- "@vitest/coverage-v8": "^2.1.5",
42
+ "@vitest/coverage-v8": "^2.1.8",
43
43
  "rimraf": "^6.0.1",
44
44
  "timekeeper": "^2.3.1",
45
45
  "tsd": "^0.31.2",
46
- "vitest": "^2.1.5",
47
- "xo": "^0.59.3"
46
+ "vitest": "^2.1.8",
47
+ "xo": "^0.60.0",
48
+ "@keyv/test-suite": "^2.0.3"
48
49
  },
49
50
  "tsd": {
50
51
  "directory": "test"
@@ -58,8 +59,8 @@
58
59
  ],
59
60
  "scripts": {
60
61
  "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
61
- "test": "xo --fix && vitest run --coverage",
62
- "test:ci": "xo && vitest --run --sequence.setupFiles=list",
62
+ "test": "xo --fix && vitest run --coverage --typecheck",
63
+ "test:ci": "xo && vitest --run --sequence.setupFiles=list --typecheck",
63
64
  "clean": "rimraf ./node_modules ./coverage ./dist"
64
65
  }
65
66
  }