@keyv/redis 3.0.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,134 +30,392 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
- default: () => src_default
33
+ Keyv: () => import_keyv2.Keyv,
34
+ createClient: () => import_redis2.createClient,
35
+ createCluster: () => import_redis2.createCluster,
36
+ createKeyv: () => createKeyv,
37
+ default: () => KeyvRedis
34
38
  });
35
39
  module.exports = __toCommonJS(src_exports);
36
40
  var import_events = __toESM(require("events"), 1);
37
- var import_ioredis = __toESM(require("ioredis"), 1);
41
+ var import_redis = require("redis");
42
+ var import_keyv = require("keyv");
43
+ var import_redis2 = require("redis");
44
+ var import_keyv2 = require("keyv");
38
45
  var KeyvRedis = class extends import_events.default {
39
- ttlSupport = true;
40
- namespace;
41
- opts;
42
- redis;
43
- constructor(uri, options) {
46
+ _client = (0, import_redis.createClient)();
47
+ _namespace;
48
+ _keyPrefixSeparator = "::";
49
+ _clearBatchSize = 1e3;
50
+ _useUnlink = true;
51
+ /**
52
+ * KeyvRedis constructor.
53
+ * @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.
54
+ * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
55
+ */
56
+ constructor(connect, options) {
44
57
  super();
45
- this.opts = {};
46
- this.opts.useRedisSets = true;
47
- this.opts.dialect = "redis";
48
- if (typeof uri !== "string" && uri.options && ("family" in uri.options || uri.isCluster)) {
49
- this.redis = uri;
58
+ if (connect) {
59
+ if (typeof connect === "string") {
60
+ this._client = (0, import_redis.createClient)({ url: connect });
61
+ } else if (connect.connect !== void 0) {
62
+ this._client = connect;
63
+ } else if (connect instanceof Object) {
64
+ this._client = (0, import_redis.createClient)(connect);
65
+ }
66
+ }
67
+ this.setOptions(options);
68
+ this.initClient();
69
+ }
70
+ /**
71
+ * Get the Redis client.
72
+ */
73
+ get client() {
74
+ return this._client;
75
+ }
76
+ /**
77
+ * Set the Redis client.
78
+ */
79
+ set client(value) {
80
+ this._client = value;
81
+ this.initClient();
82
+ }
83
+ /**
84
+ * Get the options for the adapter.
85
+ */
86
+ get opts() {
87
+ return {
88
+ namespace: this._namespace,
89
+ keyPrefixSeparator: this._keyPrefixSeparator,
90
+ clearBatchSize: this._clearBatchSize,
91
+ dialect: "redis",
92
+ url: this._client?.options?.url ?? "redis://localhost:6379"
93
+ };
94
+ }
95
+ /**
96
+ * Set the options for the adapter.
97
+ */
98
+ set opts(options) {
99
+ this.setOptions(options);
100
+ }
101
+ /**
102
+ * Get the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
103
+ * @default undefined
104
+ */
105
+ get namespace() {
106
+ return this._namespace;
107
+ }
108
+ /**
109
+ * Set the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
110
+ */
111
+ set namespace(value) {
112
+ this._namespace = value;
113
+ }
114
+ /**
115
+ * Get the separator between the namespace and key.
116
+ * @default '::'
117
+ */
118
+ get keyPrefixSeparator() {
119
+ return this._keyPrefixSeparator;
120
+ }
121
+ /**
122
+ * Set the separator between the namespace and key.
123
+ */
124
+ set keyPrefixSeparator(value) {
125
+ this._keyPrefixSeparator = value;
126
+ }
127
+ /**
128
+ * Get the number of keys to delete in a single batch.
129
+ * @default 1000
130
+ */
131
+ get clearBatchSize() {
132
+ return this._clearBatchSize;
133
+ }
134
+ /**
135
+ * Set the number of keys to delete in a single batch.
136
+ */
137
+ set clearBatchSize(value) {
138
+ this._clearBatchSize = value;
139
+ }
140
+ /**
141
+ * Get if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
142
+ * @default true
143
+ */
144
+ get useUnlink() {
145
+ return this._useUnlink;
146
+ }
147
+ /**
148
+ * Set if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
149
+ */
150
+ set useUnlink(value) {
151
+ this._useUnlink = value;
152
+ }
153
+ /**
154
+ * Get the Redis URL used to connect to the server. This is used to get a connected client.
155
+ */
156
+ async getClient() {
157
+ if (!this._client.isOpen) {
158
+ await this._client.connect();
159
+ }
160
+ return this._client;
161
+ }
162
+ /**
163
+ * Set a key value pair in the store. TTL is in milliseconds.
164
+ * @param {string} key - the key to set
165
+ * @param {string} value - the value to set
166
+ * @param {number} [ttl] - the time to live in milliseconds
167
+ */
168
+ async set(key, value, ttl) {
169
+ const client = await this.getClient();
170
+ key = this.createKeyPrefix(key, this._namespace);
171
+ if (ttl) {
172
+ await client.set(key, value, { PX: ttl });
50
173
  } else {
51
- options = { ...typeof uri === "string" ? { uri } : uri, ...options };
52
- this.redis = new import_ioredis.default(options.uri, options);
174
+ await client.set(key, value);
53
175
  }
54
- if (options !== void 0 && options.useRedisSets === false) {
55
- this.opts.useRedisSets = false;
176
+ }
177
+ /**
178
+ * Will set many key value pairs in the store. TTL is in milliseconds. This will be done as a single transaction.
179
+ * @param {Array<KeyvRedisEntry<string>>} entries - the key value pairs to set with optional ttl
180
+ */
181
+ async setMany(entries) {
182
+ const client = await this.getClient();
183
+ const multi = client.multi();
184
+ for (const { key, value, ttl } of entries) {
185
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
186
+ if (ttl) {
187
+ multi.set(prefixedKey, value, { PX: ttl });
188
+ } else {
189
+ multi.set(prefixedKey, value);
190
+ }
56
191
  }
57
- this.redis.on("error", (error) => this.emit("error", error));
192
+ await multi.exec();
58
193
  }
59
- _getNamespace() {
60
- return `namespace:${this.namespace}`;
194
+ /**
195
+ * Check if a key exists in the store.
196
+ * @param {string} key - the key to check
197
+ * @returns {Promise<boolean>} - true if the key exists, false if not
198
+ */
199
+ async has(key) {
200
+ const client = await this.getClient();
201
+ key = this.createKeyPrefix(key, this._namespace);
202
+ const exists = await client.exists(key);
203
+ return exists === 1;
61
204
  }
62
- _getKeyName = (key) => {
63
- if (!this.opts.useRedisSets) {
64
- return `sets:${this._getNamespace()}:${key}`;
205
+ /**
206
+ * Check if many keys exist in the store. This will be done as a single transaction.
207
+ * @param {Array<string>} keys - the keys to check
208
+ * @returns {Promise<Array<boolean>>} - array of booleans for each key if it exists
209
+ */
210
+ async hasMany(keys) {
211
+ const client = await this.getClient();
212
+ const multi = client.multi();
213
+ for (const key of keys) {
214
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
215
+ multi.exists(prefixedKey);
65
216
  }
66
- return key;
67
- };
217
+ const results = await multi.exec();
218
+ return results.map((result) => result === 1);
219
+ }
220
+ /**
221
+ * Get a value from the store. If the key does not exist, it will return undefined.
222
+ * @param {string} key - the key to get
223
+ * @returns {Promise<string | undefined>} - the value or undefined if the key does not exist
224
+ */
68
225
  async get(key) {
69
- key = this._getKeyName(key);
70
- const value = await this.redis.get(key);
226
+ const client = await this.getClient();
227
+ key = this.createKeyPrefix(key, this._namespace);
228
+ const value = await client.get(key);
71
229
  if (value === null) {
72
230
  return void 0;
73
231
  }
74
232
  return value;
75
233
  }
234
+ /**
235
+ * Get many values from the store. If a key does not exist, it will return undefined.
236
+ * @param {Array<string>} keys - the keys to get
237
+ * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
238
+ */
76
239
  async getMany(keys) {
77
- keys = keys.map(this._getKeyName);
78
- return this.redis.mget(keys);
79
- }
80
- async set(key, value, ttl) {
81
- if (value === void 0) {
82
- return void 0;
83
- }
84
- key = this._getKeyName(key);
85
- const set = async (redis) => {
86
- if (typeof ttl === "number") {
87
- await redis.set(key, value, "PX", ttl);
88
- } else {
89
- await redis.set(key, value);
90
- }
91
- };
92
- if (this.opts.useRedisSets) {
93
- const trx = await this.redis.multi();
94
- await set(trx);
95
- await trx.sadd(this._getNamespace(), key);
96
- await trx.exec();
97
- } else {
98
- await set(this.redis);
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);
99
245
  }
246
+ const values = await multi.exec();
247
+ return values.map((value) => value === null ? void 0 : value);
100
248
  }
249
+ /**
250
+ * Delete a key from the store.
251
+ * @param {string} key - the key to delete
252
+ * @returns {Promise<boolean>} - true if the key was deleted, false if not
253
+ */
101
254
  async delete(key) {
102
- key = this._getKeyName(key);
103
- let items = 0;
104
- const unlink = async (redis) => redis.unlink(key);
105
- if (this.opts.useRedisSets) {
106
- const trx = this.redis.multi();
107
- await unlink(trx);
108
- await trx.srem(this._getNamespace(), key);
109
- const r = await trx.exec();
110
- items = r[0][1];
255
+ const client = await this.getClient();
256
+ key = this.createKeyPrefix(key, this._namespace);
257
+ let deleted = 0;
258
+ if (this._useUnlink) {
259
+ deleted = await client.unlink(key);
111
260
  } else {
112
- items = await unlink(this.redis);
261
+ deleted = await client.del(key);
113
262
  }
114
- return items > 0;
263
+ return deleted > 0;
115
264
  }
265
+ /**
266
+ * Delete many keys from the store. This will be done as a single transaction.
267
+ * @param {Array<string>} keys - the keys to delete
268
+ * @returns {Promise<boolean>} - true if any key was deleted, false if not
269
+ */
116
270
  async deleteMany(keys) {
117
- const deletePromises = keys.map(async (key) => this.delete(key));
118
- const results = await Promise.allSettled(deletePromises);
119
- return results.every((result) => result.value);
120
- }
121
- async clear() {
122
- if (this.opts.useRedisSets) {
123
- const keys = await this.redis.smembers(this._getNamespace());
124
- if (keys.length > 0) {
125
- await Promise.all([
126
- this.redis.unlink([...keys]),
127
- this.redis.srem(this._getNamespace(), [...keys])
128
- ]);
271
+ let result = false;
272
+ const client = await this.getClient();
273
+ const multi = client.multi();
274
+ for (const key of keys) {
275
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
276
+ if (this._useUnlink) {
277
+ multi.unlink(prefixedKey);
278
+ } else {
279
+ multi.del(prefixedKey);
129
280
  }
130
- } else {
131
- const pattern = `sets:${this._getNamespace()}:*`;
132
- const keys = await this.redis.keys(pattern);
133
- if (keys.length > 0) {
134
- await this.redis.unlink(keys);
281
+ }
282
+ const results = await multi.exec();
283
+ for (const deleted of results) {
284
+ if (typeof deleted === "number" && deleted > 0) {
285
+ result = true;
135
286
  }
136
287
  }
288
+ return result;
289
+ }
290
+ /**
291
+ * Disconnect from the Redis server.
292
+ * @returns {Promise<void>}
293
+ */
294
+ async disconnect() {
295
+ if (this._client.isOpen) {
296
+ await this._client.disconnect();
297
+ }
298
+ }
299
+ /**
300
+ * Helper function to create a key with a namespace.
301
+ * @param {string} key - the key to prefix
302
+ * @param {string} namespace - the namespace to prefix the key with
303
+ * @returns {string} - the key with the namespace such as 'namespace::key'
304
+ */
305
+ createKeyPrefix(key, namespace) {
306
+ if (namespace) {
307
+ return `${namespace}${this._keyPrefixSeparator}${key}`;
308
+ }
309
+ return key;
310
+ }
311
+ /**
312
+ * Helper function to get a key without the namespace.
313
+ * @param {string} key - the key to remove the namespace from
314
+ * @param {string} namespace - the namespace to remove from the key
315
+ * @returns {string} - the key without the namespace such as 'key'
316
+ */
317
+ getKeyWithoutPrefix(key, namespace) {
318
+ if (namespace) {
319
+ return key.replace(`${namespace}${this._keyPrefixSeparator}`, "");
320
+ }
321
+ return key;
137
322
  }
323
+ /**
324
+ * 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
+ * @param {string} [namespace] - the namespace to iterate over
326
+ * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
327
+ */
138
328
  async *iterator(namespace) {
139
- const scan = this.redis.scan.bind(this.redis);
140
- const get = this.redis.mget.bind(this.redis);
329
+ const client = await this.getClient();
330
+ const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
141
331
  let cursor = "0";
142
332
  do {
143
- const [curs, keys] = await scan(cursor, "MATCH", `${namespace}:*`);
144
- cursor = curs;
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
+ }
145
339
  if (keys.length > 0) {
146
- const values = await get(keys);
340
+ const values = await client.mGet(keys);
147
341
  for (const [i] of keys.entries()) {
148
- const key = keys[i];
149
- const value = values[i];
342
+ const key = this.getKeyWithoutPrefix(keys[i], namespace);
343
+ const value = values ? values[i] : void 0;
150
344
  yield [key, value];
151
345
  }
152
346
  }
153
347
  } while (cursor !== "0");
154
348
  }
155
- async has(key) {
156
- const value = await this.redis.exists(key);
157
- return value !== 0;
349
+ /**
350
+ * 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.
352
+ * If a namespace is not set it will clear all keys with no prefix.
353
+ * If a namespace is set it will clear all keys with that namespace.
354
+ * @returns {Promise<void>}
355
+ */
356
+ async clear() {
357
+ await this.clearNamespace(this._namespace);
158
358
  }
159
- async disconnect() {
160
- return this.redis.disconnect();
359
+ async clearNamespace(namespace) {
360
+ 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));
374
+ }
375
+ if (keys.length > 0) {
376
+ if (this._useUnlink) {
377
+ await client.unlink(keys);
378
+ } else {
379
+ await client.del(keys);
380
+ }
381
+ }
382
+ } while (cursor !== "0");
383
+ } catch (error) {
384
+ this.emit("error", error);
385
+ }
386
+ }
387
+ setOptions(options) {
388
+ if (!options) {
389
+ return;
390
+ }
391
+ if (options.namespace) {
392
+ this._namespace = options.namespace;
393
+ }
394
+ if (options.keyPrefixSeparator) {
395
+ this._keyPrefixSeparator = options.keyPrefixSeparator;
396
+ }
397
+ if (options.clearBatchSize) {
398
+ this._clearBatchSize = options.clearBatchSize;
399
+ }
400
+ if (options.useUnlink !== void 0) {
401
+ this._useUnlink = options.useUnlink;
402
+ }
403
+ }
404
+ initClient() {
405
+ this._client.on("error", (error) => {
406
+ this.emit("error", error);
407
+ });
161
408
  }
162
409
  };
163
- var src_default = KeyvRedis;
410
+ function createKeyv(connect, options) {
411
+ const adapter = new KeyvRedis(connect, options);
412
+ const keyv = new import_keyv.Keyv({ store: adapter, namespace: options?.namespace, useKeyPrefix: false });
413
+ return keyv;
414
+ }
415
+ // Annotate the CommonJS export names for ESM import in node:
416
+ 0 && (module.exports = {
417
+ Keyv,
418
+ createClient,
419
+ createCluster,
420
+ createKeyv
421
+ });