@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 +6 -5
- package/dist/index.cjs +167 -60
- package/dist/index.d.cts +61 -10
- package/dist/index.d.ts +61 -10
- package/dist/index.js +172 -62
- package/package.json +7 -21
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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,
|
|
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 |
|
|
68
|
+
constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
|
|
61
69
|
/**
|
|
62
70
|
* Get the Redis client.
|
|
63
71
|
*/
|
|
64
|
-
get client():
|
|
72
|
+
get client(): RedisClientConnectionType;
|
|
65
73
|
/**
|
|
66
74
|
* Set the Redis client.
|
|
67
75
|
*/
|
|
68
|
-
set client(value:
|
|
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<
|
|
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
|
-
|
|
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,
|
|
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 |
|
|
68
|
+
constructor(connect?: string | RedisClientOptions | RedisClusterOptions | RedisClientConnectionType, options?: KeyvRedisOptions);
|
|
61
69
|
/**
|
|
62
70
|
* Get the Redis client.
|
|
63
71
|
*/
|
|
64
|
-
get client():
|
|
72
|
+
get client(): RedisClientConnectionType;
|
|
65
73
|
/**
|
|
66
74
|
* Set the Redis client.
|
|
67
75
|
*/
|
|
68
|
-
set client(value:
|
|
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<
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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"
|