nest-cache-redis 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Istiak Ahmed Tashrif
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,744 @@
1
+ # nest-cache-redis
2
+
3
+ [![npm version](https://badge.fury.io/js/nest-cache-redis.svg)](https://www.npmjs.com/package/nest-cache-redis)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
6
+
7
+ A simplified, high-performance NestJS Redis cache module using ioredis with fire-and-forget support and debug logging.
8
+
9
+ ## Features
10
+
11
+ ✨ **Simple API** - Clean methods: `get`, `set`, `getOrSet`, `delKey`, `delKeys`, `delPattern`, `delPatterns`, `getKeysByPattern`, `reset`, `getStats`
12
+ 🚀 **Fire and Forget Mode** - Optional non-blocking cache operations (global + method-level override)
13
+ 🌍 **Global Module** - Register as global module by default (configurable)
14
+ 🐛 **Debug Logging** - Built-in debug mode for cache operations
15
+ ⚡ **SCAN instead of KEYS** - Non-blocking pattern deletion
16
+ ⏱️ **TTL in Seconds** - Uses Redis `EX` command for better compatibility
17
+ 🔧 **Full ioredis Config** - Access all ioredis configuration options
18
+ � **Cache Statistics** - Monitor cache performance with built-in stats
19
+ 📦� **TypeScript First** - Full type safety and autocomplete
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # npm will automatically install peer dependencies (npm 7+)
25
+ npm install nest-cache-redis
26
+ ```
27
+
28
+ **Note:** This package has the following peer dependencies that will be automatically installed with npm 7+:
29
+ - `ioredis` (^5.0.0)
30
+ - `@nestjs/common` (^9.0.0 || ^10.0.0 || ^11.0.0)
31
+ - `reflect-metadata` (^0.1.13 || ^0.2.0)
32
+ - `rxjs` (^7.0.0)
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Import the Module
37
+
38
+ ```typescript
39
+ import { Module } from "@nestjs/common";
40
+ import { RedisCacheModule } from "nest-cache-redis";
41
+
42
+ @Module({
43
+ imports: [
44
+ RedisCacheModule.forRoot({
45
+ redisOptions: {
46
+ host: "localhost",
47
+ port: 6379,
48
+ password: "your-password", // optional
49
+ },
50
+ ttl: 3600, // Default TTL in seconds (optional)
51
+ fireAndForget: false, // Set to true for non-blocking operations
52
+ debug: false, // Set to true to enable debug logging
53
+ isGlobal: true, // Register as global module (default: true)
54
+ }),
55
+ ],
56
+ })
57
+ export class AppModule {}
58
+ ```
59
+
60
+ ### 2. Use the Service
61
+
62
+ ```typescript
63
+ import { Injectable } from "@nestjs/common";
64
+ import { RedisCacheService } from "nest-cache-redis";
65
+
66
+ @Injectable()
67
+ export class UserService {
68
+ constructor(private readonly cache: RedisCacheService) {}
69
+
70
+ async getUser(id: string) {
71
+ // Try to get from cache, or compute and cache
72
+ return this.cache.getOrSet(
73
+ `user:${id}`,
74
+ async () => {
75
+ // This function only runs if cache misses
76
+ const user = await this.db.findUser(id);
77
+ return user;
78
+ },
79
+ 3600 // TTL in seconds
80
+ );
81
+ }
82
+
83
+ async updateUser(id: string, data: any) {
84
+ await this.db.updateUser(id, data);
85
+
86
+ // Invalidate cache
87
+ await this.cache.delKey(`user:${id}`);
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## API Reference
93
+
94
+ ### Configuration Options
95
+
96
+ ```typescript
97
+ interface RedisCacheModuleOptions {
98
+ redisOptions?: RedisOptions; // All ioredis options
99
+ ttl?: number; // Default TTL in seconds
100
+ fireAndForget?: boolean; // Non-blocking mode (default: false)
101
+ debug?: boolean; // Enable debug logs (default: false)
102
+ isGlobal?: boolean; // Register as global module (default: true)
103
+ }
104
+ ```
105
+
106
+ ### Methods
107
+
108
+ #### `get<T>(key: string): Promise<T | null>`
109
+
110
+ Get a value from cache.
111
+
112
+ ```typescript
113
+ const user = await cache.get<User>("user:123");
114
+ if (user) {
115
+ console.log("Cache hit!", user);
116
+ }
117
+ ```
118
+
119
+ #### `set<T>(key: string, value: T, ttl?: number, options?: { fireAndForget?: boolean }): Promise<void>`
120
+
121
+ Set a value in cache with optional TTL (in seconds). Optionally override global fireAndForget setting.
122
+
123
+ ```typescript
124
+ await cache.set("user:123", { name: "John" }, 3600);
125
+
126
+ // Override fireAndForget for this operation
127
+ await cache.set("analytics:view", data, 60, { fireAndForget: true });
128
+ ```
129
+
130
+ #### `getOrSet<T>(key: string, fn: () => Promise<T> | T, ttl?: number): Promise<T>`
131
+
132
+ Get from cache or compute and cache the result.
133
+
134
+ ```typescript
135
+ const user = await cache.getOrSet(
136
+ "user:123",
137
+ async () => await fetchUserFromDB(123),
138
+ 3600
139
+ );
140
+ ```
141
+
142
+ #### `delKey(key: string, options?: { fireAndForget?: boolean }): Promise<number>`
143
+
144
+ Delete a single key. Returns the number of keys deleted. Optionally override global fireAndForget setting.
145
+
146
+ ```typescript
147
+ const deleted = await cache.delKey("user:123");
148
+ console.log(`Deleted ${deleted} key(s)`);
149
+
150
+ // Override fireAndForget for this operation
151
+ await cache.delKey("temp:data", { fireAndForget: true });
152
+ ```
153
+
154
+ #### `delKeys(keys: string[], options?: { fireAndForget?: boolean }): Promise<number>`
155
+
156
+ Delete multiple keys at once. Optionally override global fireAndForget setting.
157
+
158
+ ```typescript
159
+ await cache.delKeys(["user:1", "user:2", "user:3"]);
160
+
161
+ // Fire and forget
162
+ await cache.delKeys(["temp:1", "temp:2"], { fireAndForget: true });
163
+ ```
164
+
165
+ #### `delPattern(pattern: string): Promise<number>`
166
+
167
+ Delete all keys matching a pattern using SCAN (non-blocking).
168
+
169
+ **Automatically handles `keyPrefix`:** If you configured a `keyPrefix` in Redis options (e.g., `keyPrefix: 'myapp:'`), the package automatically adds it when scanning and removes it when deleting. You don't need to include the prefix in your pattern!
170
+
171
+ ```typescript
172
+ // If keyPrefix is 'myapp:', this automatically scans for 'myapp:user:*'
173
+ await cache.delPattern("user:*");
174
+
175
+ // Delete specific pattern
176
+ await cache.delPattern("session:2024:*");
177
+
178
+ // With keyPrefix 'myapp:', actual Redis keys: 'myapp:session:2024:*'
179
+ // But you just pass the pattern without prefix!
180
+ ```
181
+
182
+ #### `delPatterns(patterns: string[]): Promise<number>`
183
+
184
+ Delete multiple patterns efficiently.
185
+
186
+ ```typescript
187
+ await cache.delPatterns(["user:*", "session:*", "temp:*"]);
188
+ // Each pattern automatically gets the keyPrefix if configured
189
+ ```
190
+
191
+ #### `reset(options?: { fireAndForget?: boolean }): Promise<void>`
192
+
193
+ Clear all cache (flushdb). Optionally override global fireAndForget setting.
194
+
195
+ ```typescript
196
+ await cache.reset(); // ⚠️ Use with caution!
197
+
198
+ // Fire and forget
199
+ await cache.reset({ fireAndForget: true });
200
+ ```
201
+
202
+ #### `getKeysByPattern(pattern: string): Promise<string[]>`
203
+
204
+ Get all keys matching a pattern using SCAN (non-blocking). Returns keys without the prefix.
205
+
206
+ **Automatically handles `keyPrefix`:** Just like `delPattern`, this method automatically adds the keyPrefix when scanning and removes it from the returned keys.
207
+
208
+ ```typescript
209
+ // Get all user keys (automatically handles keyPrefix)
210
+ const userKeys = await cache.getKeysByPattern("user:*");
211
+ console.log(userKeys); // ['user:1', 'user:2', 'user:3']
212
+ // Note: No prefix in returned keys, even if keyPrefix is configured!
213
+
214
+ // Get session keys for specific date
215
+ const sessionKeys = await cache.getKeysByPattern("session:2024-12:*");
216
+
217
+ // Useful for debugging or monitoring
218
+ const allKeys = await cache.getKeysByPattern("*");
219
+ console.log(`Total keys: ${allKeys.length}`);
220
+ ```
221
+
222
+ #### `getStats(): Promise<CacheStats>`
223
+
224
+ Get Redis cache statistics including key count, memory usage, and connection status.
225
+
226
+ ```typescript
227
+ const stats = await cache.getStats();
228
+ console.log(stats);
229
+ // {
230
+ // connected: true,
231
+ // keyCount: 1250,
232
+ // memoryUsed: '2.5M',
233
+ // memoryPeak: '3.1M',
234
+ // memoryFragmentationRatio: '1.05'
235
+ // }
236
+ ```
237
+
238
+ #### `getClient(): Redis`
239
+
240
+ Get the underlying ioredis client for advanced operations.
241
+
242
+ ```typescript
243
+ const client = cache.getClient();
244
+ await client.ping(); // Direct ioredis access
245
+ ```
246
+
247
+ ## Advanced Configuration
248
+
249
+ ### Async Configuration
250
+
251
+ ```typescript
252
+ import { RedisCacheModule } from "nest-cache-redis";
253
+ import { ConfigModule, ConfigService } from "@nestjs/config";
254
+
255
+ @Module({
256
+ imports: [
257
+ RedisCacheModule.forRootAsync({
258
+ imports: [ConfigModule],
259
+ useFactory: async (configService: ConfigService) => ({
260
+ redisOptions: {
261
+ host: configService.get("REDIS_HOST"),
262
+ port: configService.get("REDIS_PORT"),
263
+ password: configService.get("REDIS_PASSWORD"),
264
+ db: configService.get("REDIS_DB", 0),
265
+ keyPrefix: "myapp:", // Add prefix to all keys
266
+ retryStrategy: (times) => Math.min(times * 50, 2000),
267
+ },
268
+ ttl: 3600,
269
+ fireAndForget: false,
270
+ debug: configService.get("NODE_ENV") === "development",
271
+ }),
272
+ inject: [ConfigService],
273
+ }),
274
+ ],
275
+ })
276
+ export class AppModule {}
277
+ ```
278
+
279
+ ### Global vs Non-Global Module
280
+
281
+ By default, the module is registered as **global** (available everywhere without re-importing). You can change this:
282
+
283
+ ```typescript
284
+ // Global (default) - available in all modules
285
+ RedisCacheModule.forRoot({
286
+ isGlobal: true, // default
287
+ redisOptions: {
288
+ /* ... */
289
+ },
290
+ });
291
+
292
+ // Non-global - must import in each module that needs it
293
+ RedisCacheModule.forRoot({
294
+ isGlobal: false,
295
+ redisOptions: {
296
+ /* ... */
297
+ },
298
+ });
299
+ ```
300
+
301
+ ### Full ioredis Configuration
302
+
303
+ All ioredis options are supported:
304
+
305
+ ```typescript
306
+ RedisCacheModule.forRoot({
307
+ redisOptions: {
308
+ host: "localhost",
309
+ port: 6379,
310
+ password: "secret",
311
+ db: 0,
312
+ keyPrefix: "myapp:",
313
+
314
+ // Connection
315
+ connectTimeout: 10000,
316
+ lazyConnect: false,
317
+ keepAlive: 30000,
318
+
319
+ // Retry
320
+ retryStrategy: (times) => Math.min(times * 100, 3000),
321
+ maxRetriesPerRequest: 3,
322
+
323
+ // TLS
324
+ tls: {
325
+ ca: fs.readFileSync("ca.crt"),
326
+ },
327
+
328
+ // Sentinel
329
+ sentinels: [
330
+ { host: "sentinel1", port: 26379 },
331
+ { host: "sentinel2", port: 26379 },
332
+ ],
333
+ name: "mymaster",
334
+ },
335
+ ttl: 3600,
336
+ debug: true,
337
+ });
338
+ ```
339
+
340
+ ## Fire and Forget Mode
341
+
342
+ When `fireAndForget: true`, cache operations don't wait for Redis responses:
343
+
344
+ ```typescript
345
+ RedisCacheModule.forRoot({
346
+ fireAndForget: true, // Global non-blocking mode
347
+ redisOptions: { host: "localhost", port: 6379 },
348
+ });
349
+
350
+ // This returns immediately without waiting for Redis
351
+ await cache.set("key", "value"); // Fires and forgets
352
+
353
+ // Note: get() always waits for response
354
+ const value = await cache.get("key"); // Always waits
355
+ ```
356
+
357
+ ### Method-Level Override
358
+
359
+ You can override the global `fireAndForget` setting for individual operations:
360
+
361
+ ```typescript
362
+ // Global setting: fireAndForget = false (wait for responses)
363
+ RedisCacheModule.forRoot({
364
+ fireAndForget: false,
365
+ redisOptions: {
366
+ /* ... */
367
+ },
368
+ });
369
+
370
+ // In your service:
371
+ // Wait for this operation (uses global setting)
372
+ await cache.set("important:data", value, 3600);
373
+
374
+ // Fire and forget for this specific operation
375
+ await cache.set("analytics:event", data, 60, { fireAndForget: true });
376
+
377
+ // Same for delete operations
378
+ await cache.delKey("temp:data", { fireAndForget: true });
379
+ await cache.delKeys(["temp:1", "temp:2"], { fireAndForget: true });
380
+ await cache.reset({ fireAndForget: true });
381
+ ```
382
+
383
+ **Use cases:**
384
+
385
+ - High-throughput applications
386
+ - When cache failures shouldn't block requests
387
+ - Fire-and-forget cache warming
388
+ - Analytics/logging that doesn't need confirmation
389
+ - Temporary data that can be lost
390
+
391
+ ## Debug Mode
392
+
393
+ Enable debug logging to see all cache operations:
394
+
395
+ ```typescript
396
+ RedisCacheModule.forRoot({
397
+ debug: true,
398
+ redisOptions: { host: "localhost", port: 6379 },
399
+ });
400
+ ```
401
+
402
+ **Example debug output:**
403
+
404
+ ```
405
+ [RedisCacheService] DEBUG GET key: user:123 - HIT
406
+ [RedisCacheService] DEBUG SET key: user:456, ttl: 3600s
407
+ [RedisCacheService] DEBUG DEL pattern: session:* - deleted 25 keys
408
+ [RedisCacheService] DEBUG GETORSET key: user:789
409
+ [RedisCacheService] DEBUG GETORSET key: user:789 - computing value
410
+ [RedisCacheService] DEBUG SET key: user:789, ttl: 3600s
411
+ ```
412
+
413
+ ## Examples
414
+
415
+ ### Cache User Data
416
+
417
+ ```typescript
418
+ @Injectable()
419
+ export class UserService {
420
+ constructor(private cache: RedisCacheService) {}
421
+
422
+ async findOne(id: string): Promise<User> {
423
+ return this.cache.getOrSet(
424
+ `user:${id}`,
425
+ () => this.userRepository.findOne(id),
426
+ 3600 // 1 hour
427
+ );
428
+ }
429
+
430
+ async update(id: string, data: UpdateUserDto): Promise<User> {
431
+ const user = await this.userRepository.update(id, data);
432
+
433
+ // Invalidate cache
434
+ await this.cache.delKey(`user:${id}`);
435
+
436
+ return user;
437
+ }
438
+
439
+ async delete(id: string): Promise<void> {
440
+ await this.userRepository.delete(id);
441
+ await this.cache.delKey(`user:${id}`);
442
+ }
443
+ }
444
+ ```
445
+
446
+ ### Cache API Responses
447
+
448
+ ```typescript
449
+ @Injectable()
450
+ export class ApiService {
451
+ constructor(private cache: RedisCacheService) {}
452
+
453
+ async fetchData(endpoint: string): Promise<any> {
454
+ const cacheKey = `api:${endpoint}`;
455
+
456
+ return this.cache.getOrSet(
457
+ cacheKey,
458
+ async () => {
459
+ const response = await fetch(endpoint);
460
+ return response.json();
461
+ },
462
+ 300 // 5 minutes
463
+ );
464
+ }
465
+ }
466
+ ```
467
+
468
+ ### Batch Invalidation
469
+
470
+ ```typescript
471
+ @Injectable()
472
+ export class CacheService {
473
+ constructor(private cache: RedisCacheService) {}
474
+
475
+ async clearUserCache(userId: string): Promise<void> {
476
+ // Clear all user-related cache
477
+ await this.cache.delPatterns([
478
+ `user:${userId}:*`,
479
+ `profile:${userId}:*`,
480
+ `permissions:${userId}:*`,
481
+ ]);
482
+ }
483
+
484
+ async clearAllSessions(): Promise<void> {
485
+ const deleted = await this.cache.delPattern("session:*");
486
+ console.log(`Cleared ${deleted} sessions`);
487
+ }
488
+ }
489
+ ```
490
+
491
+ ### Session Management
492
+
493
+ ```typescript
494
+ @Injectable()
495
+ export class SessionService {
496
+ constructor(private cache: RedisCacheService) {}
497
+
498
+ async createSession(userId: string, data: any): Promise<string> {
499
+ const sessionId = generateId();
500
+ const key = `session:${sessionId}`;
501
+
502
+ await this.cache.set(key, { userId, ...data }, 86400); // 24 hours
503
+
504
+ return sessionId;
505
+ }
506
+
507
+ async getSession(sessionId: string): Promise<any> {
508
+ return this.cache.get(`session:${sessionId}`);
509
+ }
510
+
511
+ async destroySession(sessionId: string): Promise<void> {
512
+ await this.cache.delKey(`session:${sessionId}`);
513
+ }
514
+
515
+ async destroyUserSessions(userId: string): Promise<number> {
516
+ // Find and delete all sessions for user
517
+ return this.cache.delPattern(`session:*:${userId}`);
518
+ }
519
+ }
520
+ ```
521
+
522
+ ### Monitor Cache Performance
523
+
524
+ ```typescript
525
+ @Injectable()
526
+ export class MonitoringService {
527
+ constructor(private cache: RedisCacheService) {}
528
+
529
+ async getCacheHealth(): Promise<any> {
530
+ const stats = await this.cache.getStats();
531
+
532
+ return {
533
+ status: stats.connected ? "healthy" : "unhealthy",
534
+ metrics: {
535
+ totalKeys: stats.keyCount,
536
+ memoryUsed: stats.memoryUsed,
537
+ memoryPeak: stats.memoryPeak,
538
+ fragmentationRatio: stats.memoryFragmentationRatio,
539
+ },
540
+ timestamp: new Date().toISOString(),
541
+ };
542
+ }
543
+
544
+ async checkCacheLimit(): Promise<void> {
545
+ const stats = await this.cache.getStats();
546
+
547
+ if (stats.keyCount > 10000) {
548
+ this.logger.warn(`Cache has ${stats.keyCount} keys - consider cleanup`);
549
+ }
550
+
551
+ // Parse memory (e.g., "2.5M" -> 2.5)
552
+ const memoryMB = parseFloat(stats.memoryUsed);
553
+ if (memoryMB > 100) {
554
+ this.logger.warn(
555
+ `Cache using ${stats.memoryUsed} - consider optimization`
556
+ );
557
+ }
558
+ }
559
+ }
560
+ ```
561
+
562
+ ## Best Practices
563
+
564
+ ### 1. Use Consistent Key Patterns
565
+
566
+ ```typescript
567
+ // Good: namespace:entity:id
568
+ user:123
569
+ user:123:profile
570
+ user:123:permissions
571
+ session:abc123
572
+ product:456
573
+
574
+ // Makes pattern deletion easier
575
+ await cache.delPattern('user:123:*');
576
+ ```
577
+
578
+ ### 2. Set Appropriate TTLs
579
+
580
+ ```typescript
581
+ // Short-lived data
582
+ await cache.set("rate-limit:user:123", count, 60); // 1 minute
583
+
584
+ // Medium-lived data
585
+ await cache.set("user:123", user, 3600); // 1 hour
586
+
587
+ // Long-lived data
588
+ await cache.set("config:app", config, 86400); // 24 hours
589
+ ```
590
+
591
+ ### 3. Handle Cache Misses Gracefully
592
+
593
+ ```typescript
594
+ async getUser(id: string): Promise<User | null> {
595
+ try {
596
+ return await this.cache.getOrSet(
597
+ `user:${id}`,
598
+ () => this.db.findUser(id),
599
+ 3600
600
+ );
601
+ } catch (error) {
602
+ // Log error but don't fail request
603
+ this.logger.error('Cache error:', error);
604
+ return this.db.findUser(id); // Fallback to DB
605
+ }
606
+ }
607
+ ```
608
+
609
+ ### 4. Use Fire-and-Forget for Non-Critical Cache
610
+
611
+ ```typescript
612
+ // Configure fire-and-forget for specific module
613
+ @Module({
614
+ imports: [
615
+ RedisCacheModule.register({
616
+ fireAndForget: true, // Don't block on cache writes
617
+ redisOptions: {
618
+ /* ... */
619
+ },
620
+ }),
621
+ ],
622
+ })
623
+ export class AnalyticsModule {}
624
+ ```
625
+
626
+ ## TypeScript Support
627
+
628
+ Full TypeScript support with generics:
629
+
630
+ ```typescript
631
+ interface User {
632
+ id: string;
633
+ name: string;
634
+ email: string;
635
+ }
636
+
637
+ // Type-safe cache operations
638
+ const user = await cache.get<User>("user:123");
639
+ if (user) {
640
+ console.log(user.name); // TypeScript knows this is a string
641
+ }
642
+
643
+ await cache.set<User>("user:123", {
644
+ id: "123",
645
+ name: "John",
646
+ email: "john@example.com",
647
+ });
648
+
649
+ const users = await cache.get<User[]>("users:active");
650
+ ```
651
+
652
+ ## Testing
653
+
654
+ Mock the cache service in tests:
655
+
656
+ ```typescript
657
+ import { Test } from "@nestjs/testing";
658
+ import { RedisCacheService } from "nest-cache-redis";
659
+
660
+ describe("UserService", () => {
661
+ let service: UserService;
662
+ let cache: RedisCacheService;
663
+
664
+ beforeEach(async () => {
665
+ const module = await Test.createTestingModule({
666
+ providers: [
667
+ UserService,
668
+ {
669
+ provide: RedisCacheService,
670
+ useValue: {
671
+ get: jest.fn(),
672
+ set: jest.fn(),
673
+ getOrSet: jest.fn(),
674
+ delKey: jest.fn(),
675
+ },
676
+ },
677
+ ],
678
+ }).compile();
679
+
680
+ service = module.get<UserService>(UserService);
681
+ cache = module.get<RedisCacheService>(RedisCacheService);
682
+ });
683
+
684
+ it("should get user from cache", async () => {
685
+ const mockUser = { id: "123", name: "John" };
686
+ jest.spyOn(cache, "get").mockResolvedValue(mockUser);
687
+
688
+ const result = await service.getUser("123");
689
+
690
+ expect(cache.get).toHaveBeenCalledWith("user:123");
691
+ expect(result).toEqual(mockUser);
692
+ });
693
+ });
694
+ ```
695
+
696
+ ## Troubleshooting
697
+
698
+ ### Connection Issues
699
+
700
+ Enable debug mode to see connection logs:
701
+
702
+ ```typescript
703
+ RedisCacheModule.register({
704
+ debug: true,
705
+ redisOptions: {
706
+ host: "localhost",
707
+ port: 6379,
708
+ retryStrategy: (times) => {
709
+ console.log(`Retry attempt ${times}`);
710
+ return Math.min(times * 100, 3000);
711
+ },
712
+ },
713
+ });
714
+ ```
715
+
716
+ ### Memory Issues
717
+
718
+ Monitor Redis memory and set maxmemory policy:
719
+
720
+ ```bash
721
+ # In redis.conf
722
+ maxmemory 2gb
723
+ maxmemory-policy allkeys-lru
724
+ ```
725
+
726
+ ### Performance Issues
727
+
728
+ - Use `fireAndForget: true` for high-throughput scenarios
729
+ - Use `delPattern()` instead of multiple `delKey()` calls
730
+ - Batch operations with `delKeys()` instead of individual deletes
731
+ - Set appropriate TTLs to prevent memory bloat
732
+
733
+ ## License
734
+
735
+ MIT
736
+
737
+ ## Contributing
738
+
739
+ Contributions welcome! Please open an issue or PR.
740
+
741
+ ## Support
742
+
743
+ - GitHub Issues: [Report a bug](https://github.com/Istiak-A-Tashrif/nest-cache-redis/issues)
744
+ - Documentation: [Full docs](https://github.com/Istiak-A-Tashrif/nest-cache-redis#readme)
@@ -0,0 +1,4 @@
1
+ export * from './redis-cache.module';
2
+ export * from './redis-cache.service';
3
+ export * from './redis-cache.interface';
4
+ export * from './redis-cache.constants';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./redis-cache.module"), exports);
18
+ __exportStar(require("./redis-cache.service"), exports);
19
+ __exportStar(require("./redis-cache.interface"), exports);
20
+ __exportStar(require("./redis-cache.constants"), exports);
@@ -0,0 +1 @@
1
+ export declare const REDIS_CACHE_OPTIONS = "REDIS_CACHE_OPTIONS";
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REDIS_CACHE_OPTIONS = void 0;
4
+ exports.REDIS_CACHE_OPTIONS = 'REDIS_CACHE_OPTIONS';
@@ -0,0 +1,22 @@
1
+ import { ModuleMetadata, Type } from '@nestjs/common';
2
+ import { RedisOptions } from 'ioredis';
3
+ export interface CacheOperationOptions {
4
+ fireAndForget?: boolean;
5
+ }
6
+ export interface RedisCacheModuleOptions {
7
+ redisOptions?: RedisOptions;
8
+ ttl?: number;
9
+ fireAndForget?: boolean;
10
+ debug?: boolean;
11
+ isGlobal?: boolean;
12
+ }
13
+ export interface RedisCacheModuleOptionsFactory {
14
+ createRedisCacheOptions(): Promise<RedisCacheModuleOptions> | RedisCacheModuleOptions;
15
+ }
16
+ export interface RedisCacheModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
17
+ useExisting?: Type<RedisCacheModuleOptionsFactory>;
18
+ useClass?: Type<RedisCacheModuleOptionsFactory>;
19
+ useFactory?: (...args: any[]) => Promise<RedisCacheModuleOptions> | RedisCacheModuleOptions;
20
+ inject?: any[];
21
+ isGlobal?: boolean;
22
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { RedisCacheModuleAsyncOptions, RedisCacheModuleOptions } from './redis-cache.interface';
3
+ export declare class RedisCacheModule {
4
+ static forRoot(options?: RedisCacheModuleOptions): DynamicModule;
5
+ static forRootAsync(options: RedisCacheModuleAsyncOptions): DynamicModule;
6
+ private static createAsyncProviders;
7
+ private static createAsyncOptionsProvider;
8
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var RedisCacheModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RedisCacheModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const redis_cache_constants_1 = require("./redis-cache.constants");
13
+ const redis_cache_service_1 = require("./redis-cache.service");
14
+ let RedisCacheModule = RedisCacheModule_1 = class RedisCacheModule {
15
+ static forRoot(options = {}) {
16
+ const isGlobal = options.isGlobal !== undefined ? options.isGlobal : true;
17
+ return {
18
+ global: isGlobal,
19
+ module: RedisCacheModule_1,
20
+ providers: [
21
+ {
22
+ provide: redis_cache_constants_1.REDIS_CACHE_OPTIONS,
23
+ useValue: options,
24
+ },
25
+ redis_cache_service_1.RedisCacheService,
26
+ ],
27
+ exports: [redis_cache_service_1.RedisCacheService],
28
+ };
29
+ }
30
+ static forRootAsync(options) {
31
+ const isGlobal = options.isGlobal !== undefined ? options.isGlobal : true;
32
+ return {
33
+ global: isGlobal,
34
+ module: RedisCacheModule_1,
35
+ imports: options.imports || [],
36
+ providers: [
37
+ ...this.createAsyncProviders(options),
38
+ redis_cache_service_1.RedisCacheService,
39
+ ],
40
+ exports: [redis_cache_service_1.RedisCacheService],
41
+ };
42
+ }
43
+ static createAsyncProviders(options) {
44
+ if (options.useExisting || options.useFactory) {
45
+ return [this.createAsyncOptionsProvider(options)];
46
+ }
47
+ return [
48
+ this.createAsyncOptionsProvider(options),
49
+ {
50
+ provide: options.useClass,
51
+ useClass: options.useClass,
52
+ },
53
+ ];
54
+ }
55
+ static createAsyncOptionsProvider(options) {
56
+ if (options.useFactory) {
57
+ return {
58
+ provide: redis_cache_constants_1.REDIS_CACHE_OPTIONS,
59
+ useFactory: options.useFactory,
60
+ inject: options.inject || [],
61
+ };
62
+ }
63
+ return {
64
+ provide: redis_cache_constants_1.REDIS_CACHE_OPTIONS,
65
+ useFactory: async (optionsFactory) => await optionsFactory.createRedisCacheOptions(),
66
+ inject: [options.useExisting || options.useClass],
67
+ };
68
+ }
69
+ };
70
+ exports.RedisCacheModule = RedisCacheModule;
71
+ exports.RedisCacheModule = RedisCacheModule = RedisCacheModule_1 = __decorate([
72
+ (0, common_1.Module)({})
73
+ ], RedisCacheModule);
@@ -0,0 +1,32 @@
1
+ import { OnModuleDestroy } from '@nestjs/common';
2
+ import Redis from 'ioredis';
3
+ import { RedisCacheModuleOptions, CacheOperationOptions } from './redis-cache.interface';
4
+ export declare class RedisCacheService implements OnModuleDestroy {
5
+ private readonly options;
6
+ private readonly logger;
7
+ private readonly client;
8
+ private readonly defaultTtl?;
9
+ private readonly fireAndForget;
10
+ private readonly debug;
11
+ constructor(options: RedisCacheModuleOptions);
12
+ onModuleDestroy(): Promise<void>;
13
+ get<T = any>(key: string): Promise<T | null>;
14
+ set<T = any>(key: string, value: T, ttl?: number, options?: CacheOperationOptions): Promise<void>;
15
+ getOrSet<T = any>(key: string, fn: () => Promise<T> | T, ttl?: number): Promise<T>;
16
+ delKey(key: string, options?: CacheOperationOptions): Promise<number>;
17
+ delKeys(keys: string[], options?: CacheOperationOptions): Promise<number>;
18
+ delPattern(pattern: string): Promise<number>;
19
+ delPatterns(patterns: string[]): Promise<number>;
20
+ getKeysByPattern(pattern: string): Promise<string[]>;
21
+ reset(options?: CacheOperationOptions): Promise<void>;
22
+ getStats(): Promise<{
23
+ connected: boolean;
24
+ keyCount: number;
25
+ memoryUsed?: string;
26
+ memoryPeak?: string;
27
+ memoryFragmentationRatio?: string;
28
+ error?: string;
29
+ }>;
30
+ private parseMemoryInfo;
31
+ getClient(): Redis;
32
+ }
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ var RedisCacheService_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.RedisCacheService = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const ioredis_1 = __importDefault(require("ioredis"));
22
+ const redis_cache_constants_1 = require("./redis-cache.constants");
23
+ let RedisCacheService = RedisCacheService_1 = class RedisCacheService {
24
+ constructor(options) {
25
+ this.options = options;
26
+ this.logger = new common_1.Logger(RedisCacheService_1.name);
27
+ this.client = new ioredis_1.default(options.redisOptions || {});
28
+ this.defaultTtl = options.ttl;
29
+ this.fireAndForget = options.fireAndForget || false;
30
+ this.debug = options.debug || false;
31
+ this.client.on('error', (err) => {
32
+ this.logger.error('Redis Client Error', err);
33
+ });
34
+ this.client.on('connect', () => {
35
+ this.logger.log('Redis Client Connected');
36
+ });
37
+ this.client.on('reconnecting', () => {
38
+ this.logger.log('Redis reconnecting');
39
+ });
40
+ }
41
+ async onModuleDestroy() {
42
+ await this.client.quit();
43
+ }
44
+ async get(key) {
45
+ try {
46
+ const value = await this.client.get(key);
47
+ if (value === null) {
48
+ this.debug && this.logger.debug(`GET key: ${key} - MISS`);
49
+ return null;
50
+ }
51
+ this.debug && this.logger.debug(`GET key: ${key} - HIT`);
52
+ return JSON.parse(value);
53
+ }
54
+ catch (error) {
55
+ this.logger.error(`Error getting key ${key}:`, error);
56
+ throw error;
57
+ }
58
+ }
59
+ async set(key, value, ttl, options) {
60
+ try {
61
+ const serialized = JSON.stringify(value);
62
+ const ttlToUse = ttl ?? this.defaultTtl;
63
+ const useFireAndForget = options?.fireAndForget !== undefined ? options.fireAndForget : this.fireAndForget;
64
+ this.debug && this.logger.debug(`SET key: ${key}, ttl: ${ttlToUse || 'none'}s`);
65
+ const operation = ttlToUse
66
+ ? this.client.set(key, serialized, 'EX', ttlToUse)
67
+ : this.client.set(key, serialized);
68
+ if (useFireAndForget) {
69
+ operation.catch((err) => {
70
+ this.logger.error(`Error setting key ${key}:`, err);
71
+ });
72
+ }
73
+ else {
74
+ await operation;
75
+ }
76
+ }
77
+ catch (error) {
78
+ this.logger.error(`Error setting key ${key}:`, error);
79
+ throw error;
80
+ }
81
+ }
82
+ async getOrSet(key, fn, ttl) {
83
+ try {
84
+ const value = await this.client.get(key);
85
+ if (value !== null) {
86
+ this.debug && this.logger.debug(`GETORSET key: ${key} - HIT`);
87
+ return JSON.parse(value);
88
+ }
89
+ this.debug && this.logger.debug(`GETORSET key: ${key} - computing value`);
90
+ const result = await fn();
91
+ await this.set(key, result, ttl);
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ this.logger.error(`Error in getOrSet for key ${key}:`, error);
96
+ throw error;
97
+ }
98
+ }
99
+ async delKey(key, options) {
100
+ try {
101
+ const useFireAndForget = options?.fireAndForget !== undefined ? options.fireAndForget : this.fireAndForget;
102
+ this.debug && this.logger.debug(`DEL key: ${key}`);
103
+ const operation = this.client.del(key);
104
+ if (useFireAndForget) {
105
+ operation.catch((err) => {
106
+ this.logger.error(`Error deleting key ${key}:`, err);
107
+ });
108
+ return 0;
109
+ }
110
+ const result = await operation;
111
+ this.debug && this.logger.debug(`DEL key: ${key} - deleted: ${result}`);
112
+ return result;
113
+ }
114
+ catch (error) {
115
+ this.logger.error(`Error deleting key ${key}:`, error);
116
+ throw error;
117
+ }
118
+ }
119
+ async delKeys(keys, options) {
120
+ if (keys.length === 0) {
121
+ return 0;
122
+ }
123
+ try {
124
+ const useFireAndForget = options?.fireAndForget !== undefined ? options.fireAndForget : this.fireAndForget;
125
+ this.debug && this.logger.debug(`DEL keys: [${keys.join(', ')}] (${keys.length} keys)`);
126
+ const operation = this.client.del(...keys);
127
+ if (useFireAndForget) {
128
+ operation.catch((err) => {
129
+ this.logger.error(`Error deleting keys:`, err);
130
+ });
131
+ return 0;
132
+ }
133
+ const result = await operation;
134
+ this.debug && this.logger.debug(`DEL keys - deleted: ${result}`);
135
+ return result;
136
+ }
137
+ catch (error) {
138
+ this.logger.error(`Error deleting keys:`, error);
139
+ throw error;
140
+ }
141
+ }
142
+ async delPattern(pattern) {
143
+ try {
144
+ const keyPrefix = this.client.options.keyPrefix || '';
145
+ const fullPattern = `${keyPrefix}${pattern}`;
146
+ let cursor = '0';
147
+ const allKeys = [];
148
+ do {
149
+ const result = await this.client.scan(cursor, 'MATCH', fullPattern, 'COUNT', 100);
150
+ cursor = result[0];
151
+ const keys = result[1];
152
+ if (keys.length > 0) {
153
+ const keysWithoutPrefix = keyPrefix
154
+ ? keys.map(key => key.replace(keyPrefix, ''))
155
+ : keys;
156
+ allKeys.push(...keysWithoutPrefix);
157
+ }
158
+ } while (cursor !== '0');
159
+ if (allKeys.length === 0) {
160
+ this.debug && this.logger.debug(`DEL pattern: ${pattern} - no keys found`);
161
+ return 0;
162
+ }
163
+ const operation = this.client.del(...allKeys);
164
+ let totalDeleted;
165
+ if (this.fireAndForget) {
166
+ operation.catch((err) => {
167
+ this.logger.error(`Error deleting pattern ${pattern}:`, err);
168
+ });
169
+ totalDeleted = 0;
170
+ }
171
+ else {
172
+ totalDeleted = await operation;
173
+ }
174
+ this.debug && this.logger.debug(`DEL pattern: ${pattern} - deleted ${totalDeleted} keys`);
175
+ return totalDeleted;
176
+ }
177
+ catch (error) {
178
+ this.logger.error(`Error deleting pattern ${pattern}:`, error);
179
+ throw error;
180
+ }
181
+ }
182
+ async delPatterns(patterns) {
183
+ if (patterns.length === 0) {
184
+ return 0;
185
+ }
186
+ try {
187
+ this.debug && this.logger.debug(`DEL patterns: [${patterns.join(', ')}]`);
188
+ let totalDeleted = 0;
189
+ for (const pattern of patterns) {
190
+ const deleted = await this.delPattern(pattern);
191
+ totalDeleted += deleted;
192
+ }
193
+ this.debug && this.logger.debug(`DEL patterns - total deleted: ${totalDeleted}`);
194
+ return totalDeleted;
195
+ }
196
+ catch (error) {
197
+ this.logger.error(`Error deleting patterns:`, error);
198
+ throw error;
199
+ }
200
+ }
201
+ async getKeysByPattern(pattern) {
202
+ try {
203
+ const keyPrefix = this.client.options.keyPrefix || '';
204
+ const fullPattern = `${keyPrefix}${pattern}`;
205
+ let cursor = '0';
206
+ const allKeys = [];
207
+ do {
208
+ const result = await this.client.scan(cursor, 'MATCH', fullPattern, 'COUNT', 100);
209
+ cursor = result[0];
210
+ const keys = result[1];
211
+ if (keys.length > 0) {
212
+ const keysWithoutPrefix = keyPrefix
213
+ ? keys.map(key => key.replace(keyPrefix, ''))
214
+ : keys;
215
+ allKeys.push(...keysWithoutPrefix);
216
+ }
217
+ } while (cursor !== '0');
218
+ this.debug && this.logger.debug(`GET keys by pattern: ${pattern} - found ${allKeys.length} keys`);
219
+ return allKeys;
220
+ }
221
+ catch (error) {
222
+ this.logger.error(`Error getting keys by pattern ${pattern}:`, error);
223
+ throw error;
224
+ }
225
+ }
226
+ async reset(options) {
227
+ try {
228
+ const useFireAndForget = options?.fireAndForget !== undefined ? options.fireAndForget : this.fireAndForget;
229
+ this.debug && this.logger.debug('RESET - clearing all cache');
230
+ const operation = this.client.flushdb();
231
+ if (useFireAndForget) {
232
+ operation.catch((err) => {
233
+ this.logger.error('Error resetting cache:', err);
234
+ });
235
+ }
236
+ else {
237
+ await operation;
238
+ }
239
+ this.debug && this.logger.debug('RESET - cache cleared');
240
+ }
241
+ catch (error) {
242
+ this.logger.error('Error resetting cache:', error);
243
+ throw error;
244
+ }
245
+ }
246
+ async getStats() {
247
+ try {
248
+ const connected = this.client.status === 'ready';
249
+ if (!connected) {
250
+ return {
251
+ connected: false,
252
+ keyCount: 0,
253
+ error: 'Redis not connected',
254
+ };
255
+ }
256
+ const keyCount = await this.client.dbsize();
257
+ const memoryInfo = await this.client.info('memory');
258
+ const memoryUsed = this.parseMemoryInfo(memoryInfo, 'used_memory_human');
259
+ const memoryPeak = this.parseMemoryInfo(memoryInfo, 'used_memory_peak_human');
260
+ const memoryFragmentationRatio = this.parseMemoryInfo(memoryInfo, 'mem_fragmentation_ratio');
261
+ this.debug && this.logger.debug(`STATS - keys: ${keyCount}, memory: ${memoryUsed}`);
262
+ return {
263
+ connected,
264
+ keyCount,
265
+ memoryUsed,
266
+ memoryPeak,
267
+ memoryFragmentationRatio,
268
+ };
269
+ }
270
+ catch (error) {
271
+ this.logger.error('Error getting cache stats:', error);
272
+ return {
273
+ connected: false,
274
+ keyCount: 0,
275
+ error: error.message,
276
+ };
277
+ }
278
+ }
279
+ parseMemoryInfo(info, key) {
280
+ const regex = new RegExp(`${key}:(.+)`);
281
+ const match = info.match(regex);
282
+ return match ? match[1].trim() : undefined;
283
+ }
284
+ getClient() {
285
+ return this.client;
286
+ }
287
+ };
288
+ exports.RedisCacheService = RedisCacheService;
289
+ exports.RedisCacheService = RedisCacheService = RedisCacheService_1 = __decorate([
290
+ (0, common_1.Injectable)(),
291
+ __param(0, (0, common_1.Inject)(redis_cache_constants_1.REDIS_CACHE_OPTIONS)),
292
+ __metadata("design:paramtypes", [Object])
293
+ ], RedisCacheService);
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "nest-cache-redis",
3
+ "version": "1.0.0",
4
+ "description": "A simplified, high-performance NestJS Redis cache module with fire-and-forget support, automatic keyPrefix handling, pattern-based deletion, and TypeScript-first design",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "author": "Istiak Ahmed Tashrif <istiaktashrif@gmail.com>",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/Istiak-A-Tashrif/nest-cache-redis.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/Istiak-A-Tashrif/nest-cache-redis/issues"
18
+ },
19
+ "homepage": "https://github.com/Istiak-A-Tashrif/nest-cache-redis/#readme",
20
+ "keywords": [
21
+ "nestjs",
22
+ "redis",
23
+ "ioredis",
24
+ "cache",
25
+ "nest",
26
+ "caching",
27
+ "typescript"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "peerDependencies": {
34
+ "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
35
+ "ioredis": "^5.0.0",
36
+ "reflect-metadata": "^0.1.13 || ^0.2.0",
37
+ "rxjs": "^7.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@nestjs/common": "^11.0.1",
41
+ "@types/node": "^22.10.7",
42
+ "ioredis": "^5.8.2",
43
+ "reflect-metadata": "^0.2.2",
44
+ "rxjs": "^7.8.1",
45
+ "typescript": "^5.7.3"
46
+ }
47
+ }