@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.js CHANGED
@@ -1,132 +1,388 @@
1
1
  // src/index.ts
2
2
  import EventEmitter from "events";
3
- import Redis from "ioredis";
3
+ import { createClient } from "redis";
4
+ import { Keyv } from "keyv";
5
+ import {
6
+ createClient as createClient2,
7
+ createCluster
8
+ } from "redis";
9
+ import {
10
+ Keyv as Keyv2
11
+ } from "keyv";
4
12
  var KeyvRedis = class extends EventEmitter {
5
- ttlSupport = true;
6
- namespace;
7
- opts;
8
- redis;
9
- constructor(uri, options) {
13
+ _client = createClient();
14
+ _namespace;
15
+ _keyPrefixSeparator = "::";
16
+ _clearBatchSize = 1e3;
17
+ _useUnlink = true;
18
+ /**
19
+ * KeyvRedis constructor.
20
+ * @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.
21
+ * @param {KeyvRedisOptions} [options] Options for the adapter such as namespace, keyPrefixSeparator, and clearBatchSize.
22
+ */
23
+ constructor(connect, options) {
10
24
  super();
11
- this.opts = {};
12
- this.opts.useRedisSets = true;
13
- this.opts.dialect = "redis";
14
- if (typeof uri !== "string" && uri.options && ("family" in uri.options || uri.isCluster)) {
15
- this.redis = uri;
25
+ if (connect) {
26
+ if (typeof connect === "string") {
27
+ this._client = createClient({ url: connect });
28
+ } else if (connect.connect !== void 0) {
29
+ this._client = connect;
30
+ } else if (connect instanceof Object) {
31
+ this._client = createClient(connect);
32
+ }
33
+ }
34
+ this.setOptions(options);
35
+ this.initClient();
36
+ }
37
+ /**
38
+ * Get the Redis client.
39
+ */
40
+ get client() {
41
+ return this._client;
42
+ }
43
+ /**
44
+ * Set the Redis client.
45
+ */
46
+ set client(value) {
47
+ this._client = value;
48
+ this.initClient();
49
+ }
50
+ /**
51
+ * Get the options for the adapter.
52
+ */
53
+ get opts() {
54
+ return {
55
+ namespace: this._namespace,
56
+ keyPrefixSeparator: this._keyPrefixSeparator,
57
+ clearBatchSize: this._clearBatchSize,
58
+ dialect: "redis",
59
+ url: this._client?.options?.url ?? "redis://localhost:6379"
60
+ };
61
+ }
62
+ /**
63
+ * Set the options for the adapter.
64
+ */
65
+ set opts(options) {
66
+ this.setOptions(options);
67
+ }
68
+ /**
69
+ * Get the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
70
+ * @default undefined
71
+ */
72
+ get namespace() {
73
+ return this._namespace;
74
+ }
75
+ /**
76
+ * Set the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
77
+ */
78
+ set namespace(value) {
79
+ this._namespace = value;
80
+ }
81
+ /**
82
+ * Get the separator between the namespace and key.
83
+ * @default '::'
84
+ */
85
+ get keyPrefixSeparator() {
86
+ return this._keyPrefixSeparator;
87
+ }
88
+ /**
89
+ * Set the separator between the namespace and key.
90
+ */
91
+ set keyPrefixSeparator(value) {
92
+ this._keyPrefixSeparator = value;
93
+ }
94
+ /**
95
+ * Get the number of keys to delete in a single batch.
96
+ * @default 1000
97
+ */
98
+ get clearBatchSize() {
99
+ return this._clearBatchSize;
100
+ }
101
+ /**
102
+ * Set the number of keys to delete in a single batch.
103
+ */
104
+ set clearBatchSize(value) {
105
+ this._clearBatchSize = value;
106
+ }
107
+ /**
108
+ * Get if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
109
+ * @default true
110
+ */
111
+ get useUnlink() {
112
+ return this._useUnlink;
113
+ }
114
+ /**
115
+ * Set if Unlink is used instead of Del for clearing keys. This is more performant but may not be supported by all Redis versions.
116
+ */
117
+ set useUnlink(value) {
118
+ this._useUnlink = value;
119
+ }
120
+ /**
121
+ * Get the Redis URL used to connect to the server. This is used to get a connected client.
122
+ */
123
+ async getClient() {
124
+ if (!this._client.isOpen) {
125
+ await this._client.connect();
126
+ }
127
+ return this._client;
128
+ }
129
+ /**
130
+ * Set a key value pair in the store. TTL is in milliseconds.
131
+ * @param {string} key - the key to set
132
+ * @param {string} value - the value to set
133
+ * @param {number} [ttl] - the time to live in milliseconds
134
+ */
135
+ async set(key, value, ttl) {
136
+ const client = await this.getClient();
137
+ key = this.createKeyPrefix(key, this._namespace);
138
+ if (ttl) {
139
+ await client.set(key, value, { PX: ttl });
16
140
  } else {
17
- options = { ...typeof uri === "string" ? { uri } : uri, ...options };
18
- this.redis = new Redis(options.uri, options);
141
+ await client.set(key, value);
19
142
  }
20
- if (options !== void 0 && options.useRedisSets === false) {
21
- this.opts.useRedisSets = false;
143
+ }
144
+ /**
145
+ * Will set many key value pairs in the store. TTL is in milliseconds. This will be done as a single transaction.
146
+ * @param {Array<KeyvRedisEntry<string>>} entries - the key value pairs to set with optional ttl
147
+ */
148
+ async setMany(entries) {
149
+ const client = await this.getClient();
150
+ const multi = client.multi();
151
+ for (const { key, value, ttl } of entries) {
152
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
153
+ if (ttl) {
154
+ multi.set(prefixedKey, value, { PX: ttl });
155
+ } else {
156
+ multi.set(prefixedKey, value);
157
+ }
22
158
  }
23
- this.redis.on("error", (error) => this.emit("error", error));
159
+ await multi.exec();
24
160
  }
25
- _getNamespace() {
26
- return `namespace:${this.namespace}`;
161
+ /**
162
+ * Check if a key exists in the store.
163
+ * @param {string} key - the key to check
164
+ * @returns {Promise<boolean>} - true if the key exists, false if not
165
+ */
166
+ async has(key) {
167
+ const client = await this.getClient();
168
+ key = this.createKeyPrefix(key, this._namespace);
169
+ const exists = await client.exists(key);
170
+ return exists === 1;
27
171
  }
28
- _getKeyName = (key) => {
29
- if (!this.opts.useRedisSets) {
30
- return `sets:${this._getNamespace()}:${key}`;
172
+ /**
173
+ * Check if many keys exist in the store. This will be done as a single transaction.
174
+ * @param {Array<string>} keys - the keys to check
175
+ * @returns {Promise<Array<boolean>>} - array of booleans for each key if it exists
176
+ */
177
+ async hasMany(keys) {
178
+ const client = await this.getClient();
179
+ const multi = client.multi();
180
+ for (const key of keys) {
181
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
182
+ multi.exists(prefixedKey);
31
183
  }
32
- return key;
33
- };
184
+ const results = await multi.exec();
185
+ return results.map((result) => result === 1);
186
+ }
187
+ /**
188
+ * Get a value from the store. If the key does not exist, it will return undefined.
189
+ * @param {string} key - the key to get
190
+ * @returns {Promise<string | undefined>} - the value or undefined if the key does not exist
191
+ */
34
192
  async get(key) {
35
- key = this._getKeyName(key);
36
- const value = await this.redis.get(key);
193
+ const client = await this.getClient();
194
+ key = this.createKeyPrefix(key, this._namespace);
195
+ const value = await client.get(key);
37
196
  if (value === null) {
38
197
  return void 0;
39
198
  }
40
199
  return value;
41
200
  }
201
+ /**
202
+ * Get many values from the store. If a key does not exist, it will return undefined.
203
+ * @param {Array<string>} keys - the keys to get
204
+ * @returns {Promise<Array<string | undefined>>} - array of values or undefined if the key does not exist
205
+ */
42
206
  async getMany(keys) {
43
- keys = keys.map(this._getKeyName);
44
- return this.redis.mget(keys);
45
- }
46
- async set(key, value, ttl) {
47
- if (value === void 0) {
48
- return void 0;
49
- }
50
- key = this._getKeyName(key);
51
- const set = async (redis) => {
52
- if (typeof ttl === "number") {
53
- await redis.set(key, value, "PX", ttl);
54
- } else {
55
- await redis.set(key, value);
56
- }
57
- };
58
- if (this.opts.useRedisSets) {
59
- const trx = await this.redis.multi();
60
- await set(trx);
61
- await trx.sadd(this._getNamespace(), key);
62
- await trx.exec();
63
- } else {
64
- await set(this.redis);
207
+ const client = await this.getClient();
208
+ const multi = client.multi();
209
+ for (const key of keys) {
210
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
211
+ multi.get(prefixedKey);
65
212
  }
213
+ const values = await multi.exec();
214
+ return values.map((value) => value === null ? void 0 : value);
66
215
  }
216
+ /**
217
+ * Delete a key from the store.
218
+ * @param {string} key - the key to delete
219
+ * @returns {Promise<boolean>} - true if the key was deleted, false if not
220
+ */
67
221
  async delete(key) {
68
- key = this._getKeyName(key);
69
- let items = 0;
70
- const unlink = async (redis) => redis.unlink(key);
71
- if (this.opts.useRedisSets) {
72
- const trx = this.redis.multi();
73
- await unlink(trx);
74
- await trx.srem(this._getNamespace(), key);
75
- const r = await trx.exec();
76
- items = r[0][1];
222
+ const client = await this.getClient();
223
+ key = this.createKeyPrefix(key, this._namespace);
224
+ let deleted = 0;
225
+ if (this._useUnlink) {
226
+ deleted = await client.unlink(key);
77
227
  } else {
78
- items = await unlink(this.redis);
228
+ deleted = await client.del(key);
79
229
  }
80
- return items > 0;
230
+ return deleted > 0;
81
231
  }
232
+ /**
233
+ * Delete many keys from the store. This will be done as a single transaction.
234
+ * @param {Array<string>} keys - the keys to delete
235
+ * @returns {Promise<boolean>} - true if any key was deleted, false if not
236
+ */
82
237
  async deleteMany(keys) {
83
- const deletePromises = keys.map(async (key) => this.delete(key));
84
- const results = await Promise.allSettled(deletePromises);
85
- return results.every((result) => result.value);
86
- }
87
- async clear() {
88
- if (this.opts.useRedisSets) {
89
- const keys = await this.redis.smembers(this._getNamespace());
90
- if (keys.length > 0) {
91
- await Promise.all([
92
- this.redis.unlink([...keys]),
93
- this.redis.srem(this._getNamespace(), [...keys])
94
- ]);
238
+ let result = false;
239
+ const client = await this.getClient();
240
+ const multi = client.multi();
241
+ for (const key of keys) {
242
+ const prefixedKey = this.createKeyPrefix(key, this._namespace);
243
+ if (this._useUnlink) {
244
+ multi.unlink(prefixedKey);
245
+ } else {
246
+ multi.del(prefixedKey);
95
247
  }
96
- } else {
97
- const pattern = `sets:${this._getNamespace()}:*`;
98
- const keys = await this.redis.keys(pattern);
99
- if (keys.length > 0) {
100
- await this.redis.unlink(keys);
248
+ }
249
+ const results = await multi.exec();
250
+ for (const deleted of results) {
251
+ if (typeof deleted === "number" && deleted > 0) {
252
+ result = true;
101
253
  }
102
254
  }
255
+ return result;
256
+ }
257
+ /**
258
+ * Disconnect from the Redis server.
259
+ * @returns {Promise<void>}
260
+ */
261
+ async disconnect() {
262
+ if (this._client.isOpen) {
263
+ await this._client.disconnect();
264
+ }
265
+ }
266
+ /**
267
+ * Helper function to create a key with a namespace.
268
+ * @param {string} key - the key to prefix
269
+ * @param {string} namespace - the namespace to prefix the key with
270
+ * @returns {string} - the key with the namespace such as 'namespace::key'
271
+ */
272
+ createKeyPrefix(key, namespace) {
273
+ if (namespace) {
274
+ return `${namespace}${this._keyPrefixSeparator}${key}`;
275
+ }
276
+ return key;
277
+ }
278
+ /**
279
+ * Helper function to get a key without the namespace.
280
+ * @param {string} key - the key to remove the namespace from
281
+ * @param {string} namespace - the namespace to remove from the key
282
+ * @returns {string} - the key without the namespace such as 'key'
283
+ */
284
+ getKeyWithoutPrefix(key, namespace) {
285
+ if (namespace) {
286
+ return key.replace(`${namespace}${this._keyPrefixSeparator}`, "");
287
+ }
288
+ return key;
103
289
  }
290
+ /**
291
+ * 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
+ * @param {string} [namespace] - the namespace to iterate over
293
+ * @returns {AsyncGenerator<[string, T | undefined], void, unknown>} - async iterator with key value pairs
294
+ */
104
295
  async *iterator(namespace) {
105
- const scan = this.redis.scan.bind(this.redis);
106
- const get = this.redis.mget.bind(this.redis);
296
+ const client = await this.getClient();
297
+ const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
107
298
  let cursor = "0";
108
299
  do {
109
- const [curs, keys] = await scan(cursor, "MATCH", `${namespace}:*`);
110
- cursor = curs;
300
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, TYPE: "string" });
301
+ cursor = result.cursor.toString();
302
+ let { keys } = result;
303
+ if (!namespace) {
304
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
305
+ }
111
306
  if (keys.length > 0) {
112
- const values = await get(keys);
307
+ const values = await client.mGet(keys);
113
308
  for (const [i] of keys.entries()) {
114
- const key = keys[i];
115
- const value = values[i];
309
+ const key = this.getKeyWithoutPrefix(keys[i], namespace);
310
+ const value = values ? values[i] : void 0;
116
311
  yield [key, value];
117
312
  }
118
313
  }
119
314
  } while (cursor !== "0");
120
315
  }
121
- async has(key) {
122
- const value = await this.redis.exists(key);
123
- return value !== 0;
316
+ /**
317
+ * 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.
319
+ * If a namespace is not set it will clear all keys with no prefix.
320
+ * If a namespace is set it will clear all keys with that namespace.
321
+ * @returns {Promise<void>}
322
+ */
323
+ async clear() {
324
+ await this.clearNamespace(this._namespace);
124
325
  }
125
- async disconnect() {
126
- return this.redis.disconnect();
326
+ async clearNamespace(namespace) {
327
+ try {
328
+ let cursor = "0";
329
+ const batchSize = this._clearBatchSize;
330
+ const match = namespace ? `${namespace}${this._keyPrefixSeparator}*` : "*";
331
+ const client = await this.getClient();
332
+ do {
333
+ const result = await client.scan(Number.parseInt(cursor, 10), { MATCH: match, COUNT: batchSize, TYPE: "string" });
334
+ cursor = result.cursor.toString();
335
+ let { keys } = result;
336
+ if (keys.length === 0) {
337
+ continue;
338
+ }
339
+ if (!namespace) {
340
+ keys = keys.filter((key) => !key.includes(this._keyPrefixSeparator));
341
+ }
342
+ if (keys.length > 0) {
343
+ if (this._useUnlink) {
344
+ await client.unlink(keys);
345
+ } else {
346
+ await client.del(keys);
347
+ }
348
+ }
349
+ } while (cursor !== "0");
350
+ } catch (error) {
351
+ this.emit("error", error);
352
+ }
353
+ }
354
+ setOptions(options) {
355
+ if (!options) {
356
+ return;
357
+ }
358
+ if (options.namespace) {
359
+ this._namespace = options.namespace;
360
+ }
361
+ if (options.keyPrefixSeparator) {
362
+ this._keyPrefixSeparator = options.keyPrefixSeparator;
363
+ }
364
+ if (options.clearBatchSize) {
365
+ this._clearBatchSize = options.clearBatchSize;
366
+ }
367
+ if (options.useUnlink !== void 0) {
368
+ this._useUnlink = options.useUnlink;
369
+ }
370
+ }
371
+ initClient() {
372
+ this._client.on("error", (error) => {
373
+ this.emit("error", error);
374
+ });
127
375
  }
128
376
  };
129
- var src_default = KeyvRedis;
377
+ function createKeyv(connect, options) {
378
+ const adapter = new KeyvRedis(connect, options);
379
+ const keyv = new Keyv({ store: adapter, namespace: options?.namespace, useKeyPrefix: false });
380
+ return keyv;
381
+ }
130
382
  export {
131
- src_default as default
383
+ Keyv2 as Keyv,
384
+ createClient2 as createClient,
385
+ createCluster,
386
+ createKeyv,
387
+ KeyvRedis as default
132
388
  };
package/package.json CHANGED
@@ -1,80 +1,80 @@
1
1
  {
2
- "name": "@keyv/redis",
3
- "version": "3.0.1",
4
- "description": "Redis storage adapter for Keyv",
5
- "type": "module",
6
- "main": "dist/index.cjs",
7
- "module": "dist/index.js",
8
- "types": "dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "require": "./dist/index.cjs",
12
- "import": "./dist/index.js"
13
- }
14
- },
15
- "scripts": {
16
- "build": "rm -rf dist && tsup src/index.ts --format cjs,esm --dts --clean",
17
- "prepare": "yarn build",
18
- "test": "xo --fix && vitest run --coverage",
19
- "test:ci": "xo && vitest --run --sequence.setupFiles=list",
20
- "clean": "rm -rf node_modules && rm -rf ./coverage"
21
- },
22
- "xo": {
23
- "rules": {
24
- "import/no-named-as-default": "off",
25
- "unicorn/prefer-module": "off",
26
- "unicorn/prefer-event-target": "off",
27
- "unicorn/prefer-node-protocol": "off",
28
- "unicorn/no-typeof-undefined": "off",
29
- "import/extensions": "off",
30
- "@typescript-eslint/no-unsafe-call": "off",
31
- "@typescript-eslint/no-unsafe-assignment": "off",
32
- "@typescript-eslint/no-unsafe-return": "off",
33
- "unicorn/prefer-ternary": "off",
34
- "unicorn/no-array-callback-reference": "off",
35
- "import/no-extraneous-dependencies": "off",
36
- "@typescript-eslint/no-confusing-void-expression": "off"
37
- }
38
- },
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/jaredwray/keyv.git"
42
- },
43
- "keywords": [
44
- "redis",
45
- "keyv",
46
- "storage",
47
- "adapter",
48
- "key",
49
- "value",
50
- "store",
51
- "cache",
52
- "ttl"
53
- ],
54
- "author": "Jared Wray <me@jaredwray.com> (http://jaredwray.com)",
55
- "license": "MIT",
56
- "bugs": {
57
- "url": "https://github.com/jaredwray/keyv/issues"
58
- },
59
- "homepage": "https://github.com/jaredwray/keyv",
60
- "dependencies": {
61
- "ioredis": "^5.4.1"
62
- },
63
- "devDependencies": {
64
- "@keyv/test-suite": "*",
65
- "keyv": "^5.0.0",
66
- "timekeeper": "^2.3.1",
67
- "tsd": "^0.31.1",
68
- "xo": "^0.59.3"
69
- },
70
- "tsd": {
71
- "directory": "test"
72
- },
73
- "engines": {
74
- "node": ">= 18"
75
- },
76
- "files": [
77
- "dist",
78
- "LICENSE"
79
- ]
80
- }
2
+ "name": "@keyv/redis",
3
+ "version": "4.0.1",
4
+ "description": "Redis storage adapter for Keyv",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.js"
13
+ }
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
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/jaredwray/keyv.git"
35
+ },
36
+ "keywords": [
37
+ "redis",
38
+ "keyv",
39
+ "storage",
40
+ "adapter",
41
+ "key",
42
+ "value",
43
+ "store",
44
+ "cache",
45
+ "ttl"
46
+ ],
47
+ "author": "Jared Wray <me@jaredwray.com> (http://jaredwray.com)",
48
+ "license": "MIT",
49
+ "bugs": {
50
+ "url": "https://github.com/jaredwray/keyv/issues"
51
+ },
52
+ "homepage": "https://github.com/jaredwray/keyv",
53
+ "dependencies": {
54
+ "redis": "^4.7.0",
55
+ "keyv": "*"
56
+ },
57
+ "devDependencies": {
58
+ "@keyv/test-suite": "*",
59
+ "rimraf": "^6.0.1",
60
+ "timekeeper": "^2.3.1",
61
+ "tsd": "^0.31.2",
62
+ "xo": "^0.59.3"
63
+ },
64
+ "tsd": {
65
+ "directory": "test"
66
+ },
67
+ "engines": {
68
+ "node": ">= 18"
69
+ },
70
+ "files": [
71
+ "dist",
72
+ "LICENSE"
73
+ ],
74
+ "scripts": {
75
+ "build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
76
+ "test": "xo --fix && vitest run --coverage",
77
+ "test:ci": "xo && vitest --run --sequence.setupFiles=list",
78
+ "clean": "rimraf ./node_modules ./coverage ./dist"
79
+ }
80
+ }