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 +21 -0
- package/README.md +744 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/redis-cache.constants.d.ts +1 -0
- package/dist/redis-cache.constants.js +4 -0
- package/dist/redis-cache.interface.d.ts +22 -0
- package/dist/redis-cache.interface.js +2 -0
- package/dist/redis-cache.module.d.ts +8 -0
- package/dist/redis-cache.module.js +73 -0
- package/dist/redis-cache.service.d.ts +32 -0
- package/dist/redis-cache.service.js +293 -0
- package/package.json +47 -0
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
|
+
[](https://www.npmjs.com/package/nest-cache-redis)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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)
|
package/dist/index.d.ts
ADDED
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,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,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
|
+
}
|