ioredis-toolkit 0.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +645 -0
  3. package/dist/cache.d.ts +298 -0
  4. package/dist/cache.js +606 -0
  5. package/dist/client.d.ts +177 -0
  6. package/dist/client.js +958 -0
  7. package/dist/cluster-slot.d.ts +4 -0
  8. package/dist/cluster-slot.js +31 -0
  9. package/dist/cluster.d.ts +79 -0
  10. package/dist/cluster.js +156 -0
  11. package/dist/errors.d.ts +30 -0
  12. package/dist/errors.js +63 -0
  13. package/dist/health.d.ts +39 -0
  14. package/dist/health.js +106 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +44 -0
  17. package/dist/lock.d.ts +215 -0
  18. package/dist/lock.js +385 -0
  19. package/dist/logger.d.ts +12 -0
  20. package/dist/logger.js +40 -0
  21. package/dist/pubsub.d.ts +171 -0
  22. package/dist/pubsub.js +285 -0
  23. package/dist/ratelimiter.d.ts +162 -0
  24. package/dist/ratelimiter.js +289 -0
  25. package/dist/session/index.d.ts +23 -0
  26. package/dist/session/index.js +16 -0
  27. package/dist/session/revocation-store.d.ts +171 -0
  28. package/dist/session/revocation-store.js +310 -0
  29. package/dist/session/scripts/cleanup-index.lua +21 -0
  30. package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
  31. package/dist/session/scripts/conditional-update.lua +63 -0
  32. package/dist/session/scripts/create.lua +68 -0
  33. package/dist/session/scripts/delete-by-user.lua +29 -0
  34. package/dist/session/scripts/delete.lua +15 -0
  35. package/dist/session/scripts/enforce-limit.lua +38 -0
  36. package/dist/session/scripts/revoke.lua +61 -0
  37. package/dist/session/scripts/rotate-encrypted.lua +107 -0
  38. package/dist/session/scripts/rotate.lua +119 -0
  39. package/dist/session/scripts/touch-encrypted.lua +89 -0
  40. package/dist/session/scripts/touch.lua +72 -0
  41. package/dist/session/scripts/validate.lua +90 -0
  42. package/dist/session/session-circuit-breaker.d.ts +42 -0
  43. package/dist/session/session-circuit-breaker.js +129 -0
  44. package/dist/session/session-config.d.ts +335 -0
  45. package/dist/session/session-config.js +162 -0
  46. package/dist/session/session-cookie.d.ts +72 -0
  47. package/dist/session/session-cookie.js +101 -0
  48. package/dist/session/session-encryption.d.ts +87 -0
  49. package/dist/session/session-encryption.js +139 -0
  50. package/dist/session/session-errors.d.ts +85 -0
  51. package/dist/session/session-errors.js +145 -0
  52. package/dist/session/session-health.d.ts +38 -0
  53. package/dist/session/session-health.js +60 -0
  54. package/dist/session/session-keys.d.ts +51 -0
  55. package/dist/session/session-keys.js +113 -0
  56. package/dist/session/session-manager.d.ts +59 -0
  57. package/dist/session/session-manager.js +94 -0
  58. package/dist/session/session-metrics.d.ts +33 -0
  59. package/dist/session/session-metrics.js +112 -0
  60. package/dist/session/session-repository.d.ts +161 -0
  61. package/dist/session/session-repository.js +683 -0
  62. package/dist/session/session-scripts.d.ts +36 -0
  63. package/dist/session/session-scripts.js +130 -0
  64. package/dist/session/session-serializer.d.ts +42 -0
  65. package/dist/session/session-serializer.js +248 -0
  66. package/dist/session/session-service.d.ts +104 -0
  67. package/dist/session/session-service.js +611 -0
  68. package/dist/session/session-token.d.ts +38 -0
  69. package/dist/session/session-token.js +86 -0
  70. package/dist/session/session-types.d.ts +253 -0
  71. package/dist/session/session-types.js +16 -0
  72. package/dist/types.d.ts +782 -0
  73. package/dist/types.js +140 -0
  74. package/package.json +97 -0
package/dist/cache.js ADDED
@@ -0,0 +1,606 @@
1
+ import zlib from 'node:zlib';
2
+ import { promisify } from 'node:util';
3
+ import { defaultLogger } from './logger.js';
4
+ const gzip = promisify(zlib.gzip);
5
+ const gunzip = promisify(zlib.gunzip);
6
+ /**
7
+ * Cache layer on top of {@link RedisClientWrapper} with JSON serialization,
8
+ * optional gzip compression and namespace support.
9
+ *
10
+ * Works in all three modes (standalone, sentinel, cluster): multi-key operations
11
+ * are slot-aware and pattern scans cover every cluster node.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const cache = new Cache(client, { defaultTTL: 3600, compressionThreshold: 1024 });
16
+ * await cache.set('user:1', { name: 'alice' });
17
+ * const user = await cache.get('user:1');
18
+ * ```
19
+ */
20
+ export class Cache {
21
+ client;
22
+ logger;
23
+ // private config: RedisConfig;
24
+ defaultTTL;
25
+ compressionThreshold;
26
+ /**
27
+ * Creates a cache bound to a Redis client.
28
+ *
29
+ * @param client - The underlying {@link RedisClientWrapper}.
30
+ * @param config - Redis config; `defaultTTL` (seconds) and `compressionThreshold` (bytes)
31
+ * control cache behavior.
32
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const cache = new Cache(client, { defaultTTL: 600, compressionThreshold: 2048 });
37
+ * ```
38
+ */
39
+ constructor(client, config, logger = defaultLogger) {
40
+ this.client = client;
41
+ this.logger = logger.child({ component: 'Cache' });
42
+ // this.config = config;
43
+ this.defaultTTL = config.defaultTTL || 3600;
44
+ this.compressionThreshold = config.compressionThreshold || 1024;
45
+ }
46
+ async serialize(value) {
47
+ // Convert to Buffer
48
+ let data;
49
+ if (Buffer.isBuffer(value)) {
50
+ data = value;
51
+ }
52
+ else if (typeof value === 'string') {
53
+ data = Buffer.from(value);
54
+ }
55
+ else if (typeof value === 'number' || typeof value === 'boolean') {
56
+ data = Buffer.from(String(value));
57
+ }
58
+ else {
59
+ // JSON for objects
60
+ data = Buffer.from(JSON.stringify(value));
61
+ }
62
+ // Compress if large enough
63
+ if (data.length > this.compressionThreshold) {
64
+ try {
65
+ const compressed = await gzip(data);
66
+ return { data: compressed, compressed: true };
67
+ }
68
+ catch (error) {
69
+ this.logger.warn('Compression failed, storing uncompressed');
70
+ return { data, compressed: false };
71
+ }
72
+ }
73
+ return { data, compressed: false };
74
+ }
75
+ async deserialize(data, compressed) {
76
+ let buffer = data;
77
+ if (compressed) {
78
+ try {
79
+ buffer = await gunzip(data);
80
+ }
81
+ catch (error) {
82
+ this.logger.warn('Decompression failed, trying raw data');
83
+ // Attempt to use raw data if decompression fails
84
+ }
85
+ }
86
+ // Try to parse as JSON if it looks like JSON
87
+ const str = buffer.toString();
88
+ try {
89
+ if (str.startsWith('{') || str.startsWith('[')) {
90
+ return JSON.parse(str);
91
+ }
92
+ }
93
+ catch {
94
+ // Not JSON, return as string
95
+ }
96
+ return str;
97
+ }
98
+ getKey(key, namespace) {
99
+ return namespace ? `${namespace}:${key}` : key;
100
+ }
101
+ /**
102
+ * Reads a cached value.
103
+ *
104
+ * Objects are parsed from JSON and compressed values are transparently
105
+ * decompressed. Strings that are not JSON are returned as-is.
106
+ *
107
+ * @param key - Cache key.
108
+ * @param namespace - Optional namespace prefix (`namespace:key`).
109
+ * @returns The stored value, or `null` when missing.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const user = await cache.get<User>('user:1');
114
+ * const token = await cache.get('token', 'auth');
115
+ * ```
116
+ */
117
+ async get(key, namespace) {
118
+ const fullKey = this.getKey(key, namespace);
119
+ const raw = await this.client.get(fullKey);
120
+ if (!raw)
121
+ return null;
122
+ try {
123
+ // Check if stored with metadata
124
+ const parsed = JSON.parse(raw);
125
+ if (parsed._compressed && parsed._data) {
126
+ const data = Buffer.from(parsed._data, 'base64');
127
+ return this.deserialize(data, parsed._compressed);
128
+ }
129
+ // Legacy format - try to parse as JSON
130
+ return JSON.parse(raw);
131
+ }
132
+ catch {
133
+ // Raw string value
134
+ return raw;
135
+ }
136
+ }
137
+ /**
138
+ * Stores a value in the cache.
139
+ *
140
+ * @param key - Cache key.
141
+ * @param value - Any serializable value (string, number, boolean, Buffer, object).
142
+ * @param options - `ttl` in seconds (defaults to `defaultTTL`), `namespace`,
143
+ * and `compress` (default `true`). Values larger than `compressionThreshold`
144
+ * bytes are gzip-compressed.
145
+ * @returns `true` when stored successfully.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * await cache.set('user:1', user, { ttl: 300 });
150
+ * await cache.set('token', 'abc', { namespace: 'auth', compress: false });
151
+ * ```
152
+ */
153
+ async set(key, value, options = {}) {
154
+ const fullKey = this.getKey(key, options.namespace);
155
+ const ttl = options.ttl || this.defaultTTL;
156
+ const shouldCompress = options.compress !== undefined ? options.compress : true;
157
+ try {
158
+ let rawValue;
159
+ if (shouldCompress) {
160
+ const { data, compressed } = await this.serialize(value);
161
+ if (compressed) {
162
+ // Store with metadata
163
+ rawValue = JSON.stringify({
164
+ _compressed: true,
165
+ _data: data.toString('base64'),
166
+ });
167
+ }
168
+ else {
169
+ rawValue = data;
170
+ }
171
+ }
172
+ else {
173
+ if (typeof value === 'string') {
174
+ rawValue = value;
175
+ }
176
+ else if (Buffer.isBuffer(value)) {
177
+ rawValue = value;
178
+ }
179
+ else {
180
+ rawValue = JSON.stringify(value);
181
+ }
182
+ }
183
+ const result = await this.client.set(fullKey, rawValue, ttl);
184
+ this.logger.debug('Cache set', { key: fullKey, ttl, compressed: shouldCompress });
185
+ return result === 'OK';
186
+ }
187
+ catch (error) {
188
+ this.logger.error('Cache set failed:', error);
189
+ return false;
190
+ }
191
+ }
192
+ /**
193
+ * Stores a value only if the key does not exist yet (`SETNX`).
194
+ *
195
+ * @param key - Cache key.
196
+ * @param value - The value to store.
197
+ * @param options - `ttl` in seconds and `namespace`.
198
+ * @returns `true` only when the value was actually stored.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const claimed = await cache.setNX('job:1', 'worker-1', { ttl: 60 });
203
+ * ```
204
+ */
205
+ async setNX(key, value, options = {}) {
206
+ const fullKey = this.getKey(key, options.namespace);
207
+ const ttl = options.ttl || this.defaultTTL;
208
+ try {
209
+ const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
210
+ const result = await this.client.setnx(fullKey, rawValue, ttl);
211
+ return result === 1;
212
+ }
213
+ catch (error) {
214
+ this.logger.error('Cache setNX failed:', error);
215
+ return false;
216
+ }
217
+ }
218
+ /**
219
+ * Stores a value only if the key does not exist yet, atomically with the TTL
220
+ * (`SET ... EX NX`).
221
+ *
222
+ * @param key - Cache key.
223
+ * @param value - The value to store.
224
+ * @param options - `ttl` in seconds and `namespace`.
225
+ * @returns `true` only when the value was actually stored.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * const locked = await cache.setEXNX('lock:order:42', 'txn-id', { ttl: 30 });
230
+ * ```
231
+ */
232
+ async setEXNX(key, value, options = {}) {
233
+ const fullKey = this.getKey(key, options.namespace);
234
+ const ttl = options.ttl || this.defaultTTL;
235
+ try {
236
+ const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
237
+ const result = await this.client.setexnx(fullKey, rawValue, ttl);
238
+ return result === 'OK';
239
+ }
240
+ catch (error) {
241
+ this.logger.error('Cache setEXNX failed:', error);
242
+ return false;
243
+ }
244
+ }
245
+ // Old mget - CROSSSLOT error in cluster mode when keys span different slots
246
+ // async mget<T = any>(keys: string[], namespace?: string): Promise<(T | null)[]> {
247
+ // const fullKeys = keys.map(k => this.getKey(k, namespace));
248
+ // const raw = await this.client.mget(...fullKeys);
249
+ //
250
+ // return Promise.all(
251
+ // raw.map(async (item) => {
252
+ // if (!item) return null;
253
+ // try {
254
+ // const parsed = JSON.parse(item);
255
+ // if (parsed._compressed && parsed._data) {
256
+ // const data = Buffer.from(parsed._data, 'base64');
257
+ // return this.deserialize<T>(data, parsed._compressed);
258
+ // }
259
+ // return parsed;
260
+ // } catch {
261
+ // return item as T;
262
+ // }
263
+ // })
264
+ // );
265
+ // }
266
+ // Cluster-safe: groups keys by slot via mgetClusterAware
267
+ /**
268
+ * Reads multiple cache keys in one call.
269
+ *
270
+ * Cluster-safe: keys are grouped by hash slot under the hood.
271
+ *
272
+ * @param keys - Cache keys to read.
273
+ * @param namespace - Optional namespace prefix applied to every key.
274
+ * @returns Values in input order; `null` for missing keys.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * const [a, b] = await cache.mget(['user:1', 'user:2']);
279
+ * ```
280
+ */
281
+ async mget(keys, namespace) {
282
+ const fullKeys = keys.map(k => this.getKey(k, namespace));
283
+ const raw = await this.client.mgetClusterAware(fullKeys);
284
+ return Promise.all(raw.map(async (item) => {
285
+ if (!item)
286
+ return null;
287
+ try {
288
+ const parsed = JSON.parse(item);
289
+ if (parsed._compressed && parsed._data) {
290
+ const data = Buffer.from(parsed._data, 'base64');
291
+ return this.deserialize(data, parsed._compressed);
292
+ }
293
+ return parsed;
294
+ }
295
+ catch {
296
+ return item;
297
+ }
298
+ }));
299
+ }
300
+ // Old mset - a pipeline whose keys span different slots is rejected in cluster mode
301
+ // async mset<T>(
302
+ // entries: Record<string, T>,
303
+ // options: CacheOptions = {}
304
+ // ): Promise<boolean> {
305
+ // const ttl = options.ttl || this.defaultTTL;
306
+ // const namespace = options.namespace;
307
+ //
308
+ // try {
309
+ // const pipeline = this.client.pipeline();
310
+ //
311
+ // for (const [key, value] of Object.entries(entries)) {
312
+ // const fullKey = this.getKey(key, namespace);
313
+ // const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
314
+ // pipeline.set(fullKey, rawValue, 'EX', ttl);
315
+ // }
316
+ //
317
+ // const results = await pipeline.exec();
318
+ // return !!results?.every((result: any) => result[1] === 'OK');
319
+ // } catch (error) {
320
+ // this.logger.error('Cache mset failed:', error as Record<string, any>);
321
+ // return false;
322
+ // }
323
+ // }
324
+ // Cluster-safe: one pipeline per hash slot
325
+ /**
326
+ * Stores multiple key/value entries in one call.
327
+ *
328
+ * Cluster-safe: entries are grouped by hash slot, one pipeline per slot.
329
+ *
330
+ * @param entries - Object mapping cache keys to values.
331
+ * @param options - `ttl` in seconds (defaults to `defaultTTL`) and `namespace`.
332
+ * @returns `true` when every entry was stored.
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * await cache.mset({ 'user:1': alice, 'user:2': bob }, { ttl: 300 });
337
+ * ```
338
+ */
339
+ async mset(entries, options = {}) {
340
+ const ttl = options.ttl || this.defaultTTL;
341
+ const namespace = options.namespace;
342
+ try {
343
+ const groups = new Map();
344
+ for (const [key, value] of Object.entries(entries)) {
345
+ const fullKey = this.getKey(key, namespace);
346
+ const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
347
+ const slot = this.client.calculateSlot(fullKey);
348
+ if (!groups.has(slot)) {
349
+ groups.set(slot, []);
350
+ }
351
+ groups.get(slot).push([fullKey, rawValue]);
352
+ }
353
+ for (const group of groups.values()) {
354
+ const pipeline = this.client.pipeline();
355
+ for (const [fullKey, rawValue] of group) {
356
+ pipeline.set(fullKey, rawValue, 'EX', ttl);
357
+ }
358
+ const results = await pipeline.exec();
359
+ if (!results?.every((result) => result[1] === 'OK')) {
360
+ return false;
361
+ }
362
+ }
363
+ return true;
364
+ }
365
+ catch (error) {
366
+ this.logger.error('Cache mset failed:', error);
367
+ return false;
368
+ }
369
+ }
370
+ /**
371
+ * Deletes a cache key.
372
+ *
373
+ * @param key - Cache key.
374
+ * @param namespace - Optional namespace prefix.
375
+ * @returns `true` if the key existed and was deleted.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * const removed = await cache.delete('user:1');
380
+ * ```
381
+ */
382
+ async delete(key, namespace) {
383
+ const fullKey = this.getKey(key, namespace);
384
+ const result = await this.client.del(fullKey);
385
+ return result > 0;
386
+ }
387
+ /**
388
+ * Checks whether a cache key exists.
389
+ *
390
+ * @param key - Cache key.
391
+ * @param namespace - Optional namespace prefix.
392
+ * @returns `true` if the key exists.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const cached = await cache.exists('user:1');
397
+ * ```
398
+ */
399
+ async exists(key, namespace) {
400
+ const fullKey = this.getKey(key, namespace);
401
+ const result = await this.client.exists(fullKey);
402
+ return result === 1;
403
+ }
404
+ /**
405
+ * Sets the TTL of an existing cache key.
406
+ *
407
+ * @param key - Cache key.
408
+ * @param ttl - TTL in seconds.
409
+ * @param namespace - Optional namespace prefix.
410
+ * @returns `true` if the TTL was applied.
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const extended = await cache.expire('session:42', 3600);
415
+ * ```
416
+ */
417
+ async expire(key, ttl, namespace) {
418
+ const fullKey = this.getKey(key, namespace);
419
+ const result = await this.client.expire(fullKey, ttl);
420
+ return result === 1;
421
+ }
422
+ /**
423
+ * Returns the remaining TTL of a cache key in seconds.
424
+ *
425
+ * @param key - Cache key.
426
+ * @param namespace - Optional namespace prefix.
427
+ * @returns Remaining TTL in seconds (`-2` if missing, `-1` if no TTL).
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * const secondsLeft = await cache.ttl('session:42');
432
+ * ```
433
+ */
434
+ async ttl(key, namespace) {
435
+ const fullKey = this.getKey(key, namespace);
436
+ return this.client.ttl(fullKey);
437
+ }
438
+ /**
439
+ * Atomically increments a cache counter.
440
+ *
441
+ * @param key - Counter key.
442
+ * @param by - Amount to increment by (default `1`; ignored by Redis, kept for API parity).
443
+ * @param namespace - Optional namespace prefix.
444
+ * @returns The new counter value.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * const visits = await cache.increment('stats:visits');
449
+ * ```
450
+ */
451
+ async increment(key, by = 1, namespace) {
452
+ const fullKey = this.getKey(key, namespace);
453
+ return this.client.incr(fullKey);
454
+ }
455
+ /**
456
+ * Atomically decrements a cache counter.
457
+ *
458
+ * @param key - Counter key.
459
+ * @param by - Amount to decrement by (default `1`; ignored by Redis, kept for API parity).
460
+ * @param namespace - Optional namespace prefix.
461
+ * @returns The new counter value.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * const stock = await cache.decrement('inventory:sku-1');
466
+ * ```
467
+ */
468
+ async decrement(key, by = 1, namespace) {
469
+ const fullKey = this.getKey(key, namespace);
470
+ return this.client.decr(fullKey);
471
+ }
472
+ // Hash helpers
473
+ /**
474
+ * Reads a field from a hash-style cache key.
475
+ *
476
+ * @param key - Cache key.
477
+ * @param field - Hash field.
478
+ * @param namespace - Optional namespace prefix.
479
+ * @returns The field value (JSON-parsed when possible), or `null`.
480
+ *
481
+ * @example
482
+ * ```ts
483
+ * const name = await cache.hget('user:1', 'name');
484
+ * ```
485
+ */
486
+ async hget(key, field, namespace) {
487
+ const fullKey = this.getKey(key, namespace);
488
+ const result = await this.client.hget(fullKey, field);
489
+ if (!result)
490
+ return null;
491
+ try {
492
+ return JSON.parse(result);
493
+ }
494
+ catch {
495
+ return result;
496
+ }
497
+ }
498
+ /**
499
+ * Writes a field into a hash-style cache key.
500
+ *
501
+ * @param key - Cache key.
502
+ * @param field - Hash field.
503
+ * @param value - Any serializable value (JSON-stringified unless it is a string).
504
+ * @param namespace - Optional namespace prefix.
505
+ * @returns `true` if a new field was created.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * await cache.hset('user:1', 'age', 30);
510
+ * ```
511
+ */
512
+ async hset(key, field, value, namespace) {
513
+ const fullKey = this.getKey(key, namespace);
514
+ const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
515
+ const result = await this.client.hset(fullKey, field, rawValue);
516
+ return result === 1;
517
+ }
518
+ /**
519
+ * Returns every field of a hash-style cache key.
520
+ *
521
+ * @param key - Cache key.
522
+ * @param namespace - Optional namespace prefix.
523
+ * @returns Object mapping fields to values (JSON-parsed when possible).
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const profile = await cache.hgetall('user:1');
528
+ * ```
529
+ */
530
+ async hgetall(key, namespace) {
531
+ const fullKey = this.getKey(key, namespace);
532
+ const result = await this.client.hgetall(fullKey);
533
+ const parsed = {};
534
+ for (const [field, value] of Object.entries(result)) {
535
+ try {
536
+ parsed[field] = JSON.parse(value);
537
+ }
538
+ catch {
539
+ parsed[field] = value;
540
+ }
541
+ }
542
+ return parsed;
543
+ }
544
+ // Delete by pattern
545
+ /**
546
+ * Deletes every cache key matching a glob pattern.
547
+ *
548
+ * Cluster-safe: scans every node before deleting.
549
+ *
550
+ * @param pattern - Glob pattern, e.g. `'user:*'`.
551
+ * @param namespace - Optional namespace prefix (`namespace:pattern`).
552
+ * @returns The number of deleted keys.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * const removed = await cache.deletePattern('temp:*');
557
+ * ```
558
+ */
559
+ async deletePattern(pattern, namespace) {
560
+ const fullPattern = namespace ? `${namespace}:${pattern}` : pattern;
561
+ let deleted = 0;
562
+ for await (const key of this.client.scanIterator(fullPattern)) {
563
+ const result = await this.client.del(key);
564
+ deleted += result;
565
+ }
566
+ return deleted;
567
+ }
568
+ // Get all keys matching pattern
569
+ /**
570
+ * Lists every cache key matching a glob pattern.
571
+ *
572
+ * Cluster-safe: scans every node.
573
+ *
574
+ * @param pattern - Glob pattern, e.g. `'session:*'`.
575
+ * @param namespace - Optional namespace prefix (`namespace:pattern`).
576
+ * @returns Matching keys.
577
+ *
578
+ * @example
579
+ * ```ts
580
+ * const sessions = await cache.keys('session:*');
581
+ * ```
582
+ */
583
+ async keys(pattern, namespace) {
584
+ const fullPattern = namespace ? `${namespace}:${pattern}` : pattern;
585
+ const keys = [];
586
+ for await (const key of this.client.scanIterator(fullPattern)) {
587
+ keys.push(key);
588
+ }
589
+ return keys;
590
+ }
591
+ // Clear entire namespace
592
+ /**
593
+ * Deletes every key inside a namespace.
594
+ *
595
+ * @param namespace - Namespace to wipe (`namespace:*`).
596
+ * @returns The number of deleted keys.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * const cleared = await cache.clearNamespace('sessions');
601
+ * ```
602
+ */
603
+ async clearNamespace(namespace) {
604
+ return this.deletePattern('*', namespace);
605
+ }
606
+ }