@node-ts-cache/core 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 +763 -0
- package/dist/decorator/cache.decorator.d.ts +7 -0
- package/dist/decorator/cache.decorator.js +63 -0
- package/dist/decorator/cache.decorator.js.map +1 -0
- package/dist/decorator/multicache.decorator.d.ts +8 -0
- package/dist/decorator/multicache.decorator.js +110 -0
- package/dist/decorator/multicache.decorator.js.map +1 -0
- package/dist/decorator/synccache.decorator.d.ts +3 -0
- package/dist/decorator/synccache.decorator.js +43 -0
- package/dist/decorator/synccache.decorator.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/fs/fs.json.storage.d.ts +11 -0
- package/dist/storage/fs/fs.json.storage.js +47 -0
- package/dist/storage/fs/fs.json.storage.js.map +1 -0
- package/dist/storage/fs/index.d.ts +1 -0
- package/dist/storage/fs/index.js +2 -0
- package/dist/storage/fs/index.js.map +1 -0
- package/dist/storage/memory/index.d.ts +1 -0
- package/dist/storage/memory/index.js +2 -0
- package/dist/storage/memory/index.js.map +1 -0
- package/dist/storage/memory/memory.storage.d.ts +7 -0
- package/dist/storage/memory/memory.storage.js +15 -0
- package/dist/storage/memory/memory.storage.js.map +1 -0
- package/dist/strategy/caching/abstract.base.strategy.d.ts +8 -0
- package/dist/strategy/caching/abstract.base.strategy.js +6 -0
- package/dist/strategy/caching/abstract.base.strategy.js.map +1 -0
- package/dist/strategy/caching/expiration.strategy.d.ts +11 -0
- package/dist/strategy/caching/expiration.strategy.js +43 -0
- package/dist/strategy/caching/expiration.strategy.js.map +1 -0
- package/dist/strategy/key/json.stringify.strategy.d.ts +4 -0
- package/dist/strategy/key/json.stringify.strategy.js +6 -0
- package/dist/strategy/key/json.stringify.strategy.js.map +1 -0
- package/dist/types/cache.types.d.ts +57 -0
- package/dist/types/cache.types.js +2 -0
- package/dist/types/cache.types.js.map +1 -0
- package/dist/types/key.strategy.types.d.ts +6 -0
- package/dist/types/key.strategy.types.js +2 -0
- package/dist/types/key.strategy.types.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
# @node-ts-cache/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.org/package/@node-ts-cache/core)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/simllll/node-ts-cache/actions/workflows/test.yml)
|
|
6
|
+
|
|
7
|
+
Simple and extensible caching module for TypeScript/Node.js with decorator support.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Decorators](#decorators)
|
|
14
|
+
- [@Cache](#cache)
|
|
15
|
+
- [@SyncCache](#synccache)
|
|
16
|
+
- [@MultiCache](#multicache)
|
|
17
|
+
- [Direct API Usage](#direct-api-usage)
|
|
18
|
+
- [Strategies](#strategies)
|
|
19
|
+
- [ExpirationStrategy](#expirationstrategy)
|
|
20
|
+
- [Storages](#storages)
|
|
21
|
+
- [Built-in Storages](#built-in-storages)
|
|
22
|
+
- [Additional Storages](#additional-storages)
|
|
23
|
+
- [Custom Key Strategies](#custom-key-strategies)
|
|
24
|
+
- [Interface Definitions](#interface-definitions)
|
|
25
|
+
- [Advanced Usage](#advanced-usage)
|
|
26
|
+
- [Environment Variables](#environment-variables)
|
|
27
|
+
- [Testing](#testing)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @node-ts-cache/core
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { Cache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core';
|
|
39
|
+
|
|
40
|
+
// Create a caching strategy with in-memory storage
|
|
41
|
+
const cacheStrategy = new ExpirationStrategy(new MemoryStorage());
|
|
42
|
+
|
|
43
|
+
class UserService {
|
|
44
|
+
@Cache(cacheStrategy, { ttl: 60 })
|
|
45
|
+
async getUser(id: string): Promise<User> {
|
|
46
|
+
// Expensive operation - result will be cached for 60 seconds
|
|
47
|
+
return await database.findUser(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Decorators
|
|
53
|
+
|
|
54
|
+
### @Cache
|
|
55
|
+
|
|
56
|
+
Caches async method responses. The cache key is generated from the class name, method name, and stringified arguments.
|
|
57
|
+
|
|
58
|
+
**Signature:**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
@Cache(strategy: IAsynchronousCacheType | ISynchronousCacheType, options?: ExpirationOptions, keyStrategy?: IAsyncKeyStrategy)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Parameters:**
|
|
65
|
+
|
|
66
|
+
- `strategy` - A caching strategy instance (e.g., `ExpirationStrategy`)
|
|
67
|
+
- `options` - Options passed to the strategy (see [ExpirationStrategy](#expirationstrategy))
|
|
68
|
+
- `keyStrategy` - Optional custom key generation strategy
|
|
69
|
+
|
|
70
|
+
**Important:** `@Cache` always returns a Promise, even for synchronous methods, because cache operations may be asynchronous.
|
|
71
|
+
|
|
72
|
+
**Example:**
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { Cache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core';
|
|
76
|
+
|
|
77
|
+
const strategy = new ExpirationStrategy(new MemoryStorage());
|
|
78
|
+
|
|
79
|
+
class ProductService {
|
|
80
|
+
@Cache(strategy, { ttl: 300 })
|
|
81
|
+
async getProduct(id: string): Promise<Product> {
|
|
82
|
+
console.log('Fetching product from database...');
|
|
83
|
+
return await db.products.findById(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Cache(strategy, { ttl: 3600, isCachedForever: false })
|
|
87
|
+
async getCategories(): Promise<Category[]> {
|
|
88
|
+
return await db.categories.findAll();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Usage
|
|
93
|
+
const service = new ProductService();
|
|
94
|
+
|
|
95
|
+
// First call - hits database
|
|
96
|
+
const product1 = await service.getProduct('123');
|
|
97
|
+
|
|
98
|
+
// Second call with same args - returns cached result
|
|
99
|
+
const product2 = await service.getProduct('123');
|
|
100
|
+
|
|
101
|
+
// Different args - hits database again
|
|
102
|
+
const product3 = await service.getProduct('456');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### @SyncCache
|
|
106
|
+
|
|
107
|
+
Caches synchronous method responses without converting to Promises. Use this when your storage is synchronous (like `MemoryStorage` or `LRUStorage`).
|
|
108
|
+
|
|
109
|
+
**Signature:**
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
@SyncCache(strategy: ISynchronousCacheType, options?: ExpirationOptions, keyStrategy?: ISyncKeyStrategy)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Example:**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { SyncCache, ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core';
|
|
119
|
+
|
|
120
|
+
const strategy = new ExpirationStrategy(new MemoryStorage());
|
|
121
|
+
|
|
122
|
+
class ConfigService {
|
|
123
|
+
@SyncCache(strategy, { ttl: 60 })
|
|
124
|
+
getConfig(key: string): ConfigValue {
|
|
125
|
+
// Expensive computation
|
|
126
|
+
return computeConfig(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Usage - returns value directly, not a Promise
|
|
131
|
+
const config = new ConfigService().getConfig('theme');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### @MultiCache
|
|
135
|
+
|
|
136
|
+
Enables multi-tier caching with batch operations. Useful for:
|
|
137
|
+
|
|
138
|
+
- Caching array-based lookups efficiently
|
|
139
|
+
- Implementing local + remote cache tiers
|
|
140
|
+
- Reducing database queries for batch operations
|
|
141
|
+
|
|
142
|
+
**Signature:**
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
@MultiCache(
|
|
146
|
+
strategies: Array<IMultiSynchronousCacheType | IMultiIAsynchronousCacheType>,
|
|
147
|
+
parameterIndex: number,
|
|
148
|
+
cacheKeyFn?: (element: any) => string,
|
|
149
|
+
options?: ExpirationOptions
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
|
|
155
|
+
- `strategies` - Array of cache strategies, checked in order (first = fastest, last = slowest)
|
|
156
|
+
- `parameterIndex` - Index of the array parameter in the method signature
|
|
157
|
+
- `cacheKeyFn` - Optional function to generate cache keys for each element
|
|
158
|
+
- `options` - Options passed to strategies
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { MultiCache, ExpirationStrategy } from '@node-ts-cache/core';
|
|
164
|
+
import NodeCacheStorage from '@node-ts-cache/node-cache-storage';
|
|
165
|
+
import RedisIOStorage from '@node-ts-cache/ioredis-storage';
|
|
166
|
+
|
|
167
|
+
// Local cache (fastest) -> Redis (shared) -> Database (slowest)
|
|
168
|
+
const localCache = new ExpirationStrategy(new NodeCacheStorage());
|
|
169
|
+
const redisCache = new RedisIOStorage(() => redisClient, { maxAge: 3600 });
|
|
170
|
+
|
|
171
|
+
class UserService {
|
|
172
|
+
@MultiCache([localCache, redisCache], 0, userId => `user:${userId}`, { ttl: 300 })
|
|
173
|
+
async getUsersByIds(userIds: string[]): Promise<User[]> {
|
|
174
|
+
// This only runs for IDs not found in any cache
|
|
175
|
+
// IMPORTANT: Return results in the same order as input IDs
|
|
176
|
+
return await db.users.findByIds(userIds);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Usage
|
|
181
|
+
const service = new UserService();
|
|
182
|
+
|
|
183
|
+
// First call - checks local, then redis, then hits database
|
|
184
|
+
const users = await service.getUsersByIds(['1', '2', '3']);
|
|
185
|
+
|
|
186
|
+
// Second call - user 1 & 2 from local cache, user 4 from database
|
|
187
|
+
const moreUsers = await service.getUsersByIds(['1', '2', '4']);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Return Value Requirements:**
|
|
191
|
+
|
|
192
|
+
- Return an array with the same length and order as the input array
|
|
193
|
+
- Use `null` for entries that exist but are empty
|
|
194
|
+
- Use `undefined` for entries that should be re-queried next time
|
|
195
|
+
|
|
196
|
+
## Direct API Usage
|
|
197
|
+
|
|
198
|
+
You can use the caching strategy directly without decorators:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core';
|
|
202
|
+
|
|
203
|
+
const cache = new ExpirationStrategy(new MemoryStorage());
|
|
204
|
+
|
|
205
|
+
class DataService {
|
|
206
|
+
async getData(key: string): Promise<Data> {
|
|
207
|
+
// Check cache first
|
|
208
|
+
const cached = await cache.getItem<Data>(key);
|
|
209
|
+
if (cached !== undefined) {
|
|
210
|
+
return cached;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fetch fresh data
|
|
214
|
+
const data = await fetchData(key);
|
|
215
|
+
|
|
216
|
+
// Store in cache
|
|
217
|
+
await cache.setItem(key, data, { ttl: 300 });
|
|
218
|
+
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async invalidate(key: string): Promise<void> {
|
|
223
|
+
await cache.setItem(key, undefined);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async clearAll(): Promise<void> {
|
|
227
|
+
await cache.clear();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Strategies
|
|
233
|
+
|
|
234
|
+
### ExpirationStrategy
|
|
235
|
+
|
|
236
|
+
Time-based cache expiration strategy. Items are automatically invalidated after a specified TTL (Time To Live).
|
|
237
|
+
|
|
238
|
+
**Constructor:**
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
new ExpirationStrategy(storage: IAsynchronousCacheType | ISynchronousCacheType)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Options:**
|
|
245
|
+
|
|
246
|
+
| Option | Type | Default | Description |
|
|
247
|
+
| ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
248
|
+
| `ttl` | `number` | `60` | Time to live in **seconds** |
|
|
249
|
+
| `isLazy` | `boolean` | `true` | If `true`, items are deleted when accessed after expiration. If `false`, items are deleted automatically via `setTimeout` |
|
|
250
|
+
| `isCachedForever` | `boolean` | `false` | If `true`, items never expire (ignores `ttl`) |
|
|
251
|
+
|
|
252
|
+
**Example:**
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { ExpirationStrategy, MemoryStorage } from '@node-ts-cache/core';
|
|
256
|
+
|
|
257
|
+
const storage = new MemoryStorage();
|
|
258
|
+
const strategy = new ExpirationStrategy(storage);
|
|
259
|
+
|
|
260
|
+
// Cache for 5 minutes with lazy expiration
|
|
261
|
+
await strategy.setItem('key1', 'value', { ttl: 300, isLazy: true });
|
|
262
|
+
|
|
263
|
+
// Cache forever
|
|
264
|
+
await strategy.setItem('key2', 'value', { isCachedForever: true });
|
|
265
|
+
|
|
266
|
+
// Cache for 10 seconds with eager expiration (auto-delete)
|
|
267
|
+
await strategy.setItem('key3', 'value', { ttl: 10, isLazy: false });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Lazy vs Eager Expiration:**
|
|
271
|
+
|
|
272
|
+
- **Lazy (`isLazy: true`)**: Expired items remain in storage until accessed. Memory is freed on read. Better for large caches.
|
|
273
|
+
- **Eager (`isLazy: false`)**: Items are deleted via `setTimeout` after TTL. Frees memory automatically but uses timers.
|
|
274
|
+
|
|
275
|
+
## Storages
|
|
276
|
+
|
|
277
|
+
### Built-in Storages
|
|
278
|
+
|
|
279
|
+
These storages are included in the core package:
|
|
280
|
+
|
|
281
|
+
#### MemoryStorage
|
|
282
|
+
|
|
283
|
+
Simple in-memory storage using a JavaScript object. Best for development and simple use cases.
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { MemoryStorage, ExpirationStrategy } from '@node-ts-cache/core';
|
|
287
|
+
|
|
288
|
+
const storage = new MemoryStorage();
|
|
289
|
+
const strategy = new ExpirationStrategy(storage);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Characteristics:**
|
|
293
|
+
|
|
294
|
+
- Synchronous operations
|
|
295
|
+
- No external dependencies
|
|
296
|
+
- Data lost on process restart
|
|
297
|
+
- No size limits (can cause memory issues)
|
|
298
|
+
|
|
299
|
+
#### FsJsonStorage
|
|
300
|
+
|
|
301
|
+
File-based storage that persists cache to a JSON file. Useful for persistent local caching.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { FsJsonStorage, ExpirationStrategy } from '@node-ts-cache/core';
|
|
305
|
+
|
|
306
|
+
const storage = new FsJsonStorage('/tmp/cache.json');
|
|
307
|
+
const strategy = new ExpirationStrategy(storage);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Characteristics:**
|
|
311
|
+
|
|
312
|
+
- Asynchronous operations
|
|
313
|
+
- Survives process restarts
|
|
314
|
+
- Slower than memory storage
|
|
315
|
+
- Good for development/single-instance deployments
|
|
316
|
+
|
|
317
|
+
### Additional Storages
|
|
318
|
+
|
|
319
|
+
Install these separately based on your needs:
|
|
320
|
+
|
|
321
|
+
#### NodeCacheStorage
|
|
322
|
+
|
|
323
|
+
Wrapper for [node-cache](https://www.npmjs.com/package/node-cache) - a simple in-memory cache with TTL support.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
npm install @node-ts-cache/node-cache-storage
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { ExpirationStrategy } from '@node-ts-cache/core';
|
|
331
|
+
import NodeCacheStorage from '@node-ts-cache/node-cache-storage';
|
|
332
|
+
|
|
333
|
+
const storage = new NodeCacheStorage({
|
|
334
|
+
stdTTL: 100, // Default TTL in seconds
|
|
335
|
+
checkperiod: 120, // Cleanup interval in seconds
|
|
336
|
+
maxKeys: 1000 // Maximum number of keys
|
|
337
|
+
});
|
|
338
|
+
const strategy = new ExpirationStrategy(storage);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Characteristics:**
|
|
342
|
+
|
|
343
|
+
- Synchronous operations
|
|
344
|
+
- Supports multi-get/set operations
|
|
345
|
+
- Built-in TTL and cleanup
|
|
346
|
+
- Good for production single-instance apps
|
|
347
|
+
|
|
348
|
+
#### LRUStorage
|
|
349
|
+
|
|
350
|
+
Wrapper for [lru-cache](https://www.npmjs.com/package/lru-cache) - Least Recently Used cache with automatic eviction.
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
npm install @node-ts-cache/lru-storage
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { ExpirationStrategy } from '@node-ts-cache/core';
|
|
358
|
+
import LRUStorage from '@node-ts-cache/lru-storage';
|
|
359
|
+
|
|
360
|
+
const storage = new LRUStorage({
|
|
361
|
+
max: 500, // Maximum number of items
|
|
362
|
+
ttl: 300 // TTL in seconds
|
|
363
|
+
});
|
|
364
|
+
const strategy = new ExpirationStrategy(storage);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Characteristics:**
|
|
368
|
+
|
|
369
|
+
- Synchronous operations
|
|
370
|
+
- Automatic eviction when max size reached
|
|
371
|
+
- Memory-safe with bounded size
|
|
372
|
+
- Supports multi-get/set operations
|
|
373
|
+
|
|
374
|
+
**Note:** LRU cache has its own TTL (`ttl` in seconds). When using with `ExpirationStrategy`, both TTLs apply. Set LRU `ttl` higher than your strategy TTL or use `isCachedForever` in the strategy.
|
|
375
|
+
|
|
376
|
+
#### RedisStorage
|
|
377
|
+
|
|
378
|
+
Redis storage using the legacy `redis` package (v3.x). For new projects, consider using `RedisIOStorage` instead.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm install @node-ts-cache/redis-storage
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { ExpirationStrategy } from '@node-ts-cache/core';
|
|
386
|
+
import RedisStorage from '@node-ts-cache/redis-storage';
|
|
387
|
+
|
|
388
|
+
const storage = new RedisStorage({
|
|
389
|
+
host: 'localhost',
|
|
390
|
+
port: 6379,
|
|
391
|
+
password: 'optional'
|
|
392
|
+
});
|
|
393
|
+
const strategy = new ExpirationStrategy(storage);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Characteristics:**
|
|
397
|
+
|
|
398
|
+
- Asynchronous operations
|
|
399
|
+
- Uses legacy `redis` package with Bluebird promises
|
|
400
|
+
- Shared cache across multiple instances
|
|
401
|
+
- No compression support
|
|
402
|
+
|
|
403
|
+
#### RedisIOStorage
|
|
404
|
+
|
|
405
|
+
Modern Redis storage using [ioredis](https://github.com/redis/ioredis) with optional Snappy compression.
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
npm install @node-ts-cache/ioredis-storage
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { ExpirationStrategy } from '@node-ts-cache/core';
|
|
413
|
+
import RedisIOStorage from '@node-ts-cache/ioredis-storage';
|
|
414
|
+
import Redis from 'ioredis';
|
|
415
|
+
|
|
416
|
+
const redisClient = new Redis({
|
|
417
|
+
host: 'localhost',
|
|
418
|
+
port: 6379
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Basic usage
|
|
422
|
+
const storage = new RedisIOStorage(
|
|
423
|
+
() => redisClient,
|
|
424
|
+
{ maxAge: 3600 } // TTL in seconds
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// With compression (reduces bandwidth, increases CPU usage)
|
|
428
|
+
const compressedStorage = new RedisIOStorage(() => redisClient, { maxAge: 3600, compress: true });
|
|
429
|
+
|
|
430
|
+
// With error handler (non-blocking writes)
|
|
431
|
+
storage.onError(error => {
|
|
432
|
+
console.error('Redis error:', error);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const strategy = new ExpirationStrategy(storage);
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Characteristics:**
|
|
439
|
+
|
|
440
|
+
- Asynchronous operations
|
|
441
|
+
- Supports multi-get/set operations
|
|
442
|
+
- Optional Snappy compression
|
|
443
|
+
- Modern ioredis client
|
|
444
|
+
- Custom error handler support
|
|
445
|
+
- Can bypass ExpirationStrategy TTL (uses Redis native TTL)
|
|
446
|
+
|
|
447
|
+
**Constructor Options:**
|
|
448
|
+
| Option | Type | Default | Description |
|
|
449
|
+
|--------|------|---------|-------------|
|
|
450
|
+
| `maxAge` | `number` | `86400` | TTL in seconds (used by Redis SETEX) |
|
|
451
|
+
| `compress` | `boolean` | `false` | Enable Snappy compression |
|
|
452
|
+
|
|
453
|
+
#### LRUWithRedisStorage
|
|
454
|
+
|
|
455
|
+
Two-tier caching: fast local LRU cache with Redis fallback. Provides the best of both worlds.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
npm install @node-ts-cache/lru-redis-storage
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { ExpirationStrategy } from '@node-ts-cache/core';
|
|
463
|
+
import LRUWithRedisStorage from '@node-ts-cache/lru-redis-storage';
|
|
464
|
+
import Redis from 'ioredis';
|
|
465
|
+
|
|
466
|
+
const redisClient = new Redis();
|
|
467
|
+
|
|
468
|
+
const storage = new LRUWithRedisStorage(
|
|
469
|
+
{ max: 1000 }, // LRU options
|
|
470
|
+
() => redisClient // Redis client factory
|
|
471
|
+
);
|
|
472
|
+
const strategy = new ExpirationStrategy(storage);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Characteristics:**
|
|
476
|
+
|
|
477
|
+
- Asynchronous operations
|
|
478
|
+
- Local LRU for hot data
|
|
479
|
+
- Redis fallback for cache misses
|
|
480
|
+
- Reduces Redis round-trips
|
|
481
|
+
- Good for high-traffic applications
|
|
482
|
+
|
|
483
|
+
## Custom Key Strategies
|
|
484
|
+
|
|
485
|
+
By default, cache keys are generated as: `ClassName:methodName:JSON.stringify(args)`
|
|
486
|
+
|
|
487
|
+
You can implement custom key strategies for different needs:
|
|
488
|
+
|
|
489
|
+
### Synchronous Key Strategy
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
import { Cache, ExpirationStrategy, MemoryStorage, ISyncKeyStrategy } from '@node-ts-cache/core';
|
|
493
|
+
|
|
494
|
+
class CustomKeyStrategy implements ISyncKeyStrategy {
|
|
495
|
+
getKey(className: string, methodName: string, args: any[]): string | undefined {
|
|
496
|
+
// Return undefined to skip caching for this call
|
|
497
|
+
if (args[0] === 'skip') {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Custom key format
|
|
502
|
+
return `${className}::${methodName}::${args.join('-')}`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const strategy = new ExpirationStrategy(new MemoryStorage());
|
|
507
|
+
const keyStrategy = new CustomKeyStrategy();
|
|
508
|
+
|
|
509
|
+
class MyService {
|
|
510
|
+
@Cache(strategy, { ttl: 60 }, keyStrategy)
|
|
511
|
+
async getData(id: string): Promise<Data> {
|
|
512
|
+
return fetchData(id);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Asynchronous Key Strategy
|
|
518
|
+
|
|
519
|
+
For key generation that requires async operations (e.g., fetching user context):
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { Cache, ExpirationStrategy, MemoryStorage, IAsyncKeyStrategy } from '@node-ts-cache/core';
|
|
523
|
+
|
|
524
|
+
class AsyncKeyStrategy implements IAsyncKeyStrategy {
|
|
525
|
+
async getKey(className: string, methodName: string, args: any[]): Promise<string | undefined> {
|
|
526
|
+
// Async operation to build key
|
|
527
|
+
const userId = await getCurrentUserId();
|
|
528
|
+
return `${userId}:${className}:${methodName}:${JSON.stringify(args)}`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Interface Definitions
|
|
534
|
+
|
|
535
|
+
### Storage Interfaces
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
/**
|
|
539
|
+
* Cache entry structure stored in backends
|
|
540
|
+
*/
|
|
541
|
+
interface ICacheEntry {
|
|
542
|
+
content: any; // The cached value
|
|
543
|
+
meta: any; // Metadata (e.g., TTL, createdAt)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Asynchronous storage for single items
|
|
548
|
+
*/
|
|
549
|
+
interface IAsynchronousCacheType<C = ICacheEntry> {
|
|
550
|
+
/** Retrieve an item by key. Returns undefined if not found. */
|
|
551
|
+
getItem<T>(key: string): Promise<T | undefined>;
|
|
552
|
+
|
|
553
|
+
/** Store an item. Pass undefined as content to delete. */
|
|
554
|
+
setItem(key: string, content: C | undefined, options?: any): Promise<void>;
|
|
555
|
+
|
|
556
|
+
/** Clear all items from the cache. */
|
|
557
|
+
clear(): Promise<void>;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Synchronous storage for single items
|
|
562
|
+
*/
|
|
563
|
+
interface ISynchronousCacheType<C = ICacheEntry> {
|
|
564
|
+
/** Retrieve an item by key. Returns undefined if not found. */
|
|
565
|
+
getItem<T>(key: string): T | undefined;
|
|
566
|
+
|
|
567
|
+
/** Store an item. Pass undefined as content to delete. */
|
|
568
|
+
setItem(key: string, content: C | undefined, options?: any): void;
|
|
569
|
+
|
|
570
|
+
/** Clear all items from the cache. */
|
|
571
|
+
clear(): void;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Asynchronous storage with batch operations
|
|
576
|
+
*/
|
|
577
|
+
interface IMultiIAsynchronousCacheType<C = ICacheEntry> {
|
|
578
|
+
/** Retrieve multiple items by keys. */
|
|
579
|
+
getItems<T>(keys: string[]): Promise<{ [key: string]: T | undefined }>;
|
|
580
|
+
|
|
581
|
+
/** Store multiple items at once. */
|
|
582
|
+
setItems(values: { key: string; content: C | undefined }[], options?: any): Promise<void>;
|
|
583
|
+
|
|
584
|
+
/** Clear all items from the cache. */
|
|
585
|
+
clear(): Promise<void>;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Synchronous storage with batch operations
|
|
590
|
+
*/
|
|
591
|
+
interface IMultiSynchronousCacheType<C = ICacheEntry> {
|
|
592
|
+
/** Retrieve multiple items by keys. */
|
|
593
|
+
getItems<T>(keys: string[]): { [key: string]: T | undefined };
|
|
594
|
+
|
|
595
|
+
/** Store multiple items at once. */
|
|
596
|
+
setItems(values: { key: string; content: C | undefined }[], options?: any): void;
|
|
597
|
+
|
|
598
|
+
/** Clear all items from the cache. */
|
|
599
|
+
clear(): void;
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Key Strategy Interfaces
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
/**
|
|
607
|
+
* Synchronous key generation strategy
|
|
608
|
+
*/
|
|
609
|
+
interface ISyncKeyStrategy {
|
|
610
|
+
/**
|
|
611
|
+
* Generate a cache key from method context
|
|
612
|
+
* @param className - Name of the class containing the method
|
|
613
|
+
* @param methodName - Name of the cached method
|
|
614
|
+
* @param args - Arguments passed to the method
|
|
615
|
+
* @returns Cache key string, or undefined to skip caching
|
|
616
|
+
*/
|
|
617
|
+
getKey(className: string, methodName: string, args: any[]): string | undefined;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Asynchronous key generation strategy
|
|
622
|
+
*/
|
|
623
|
+
interface IAsyncKeyStrategy {
|
|
624
|
+
/**
|
|
625
|
+
* Generate a cache key from method context (can be async)
|
|
626
|
+
* @param className - Name of the class containing the method
|
|
627
|
+
* @param methodName - Name of the cached method
|
|
628
|
+
* @param args - Arguments passed to the method
|
|
629
|
+
* @returns Cache key string, or undefined to skip caching
|
|
630
|
+
*/
|
|
631
|
+
getKey(
|
|
632
|
+
className: string,
|
|
633
|
+
methodName: string,
|
|
634
|
+
args: any[]
|
|
635
|
+
): Promise<string | undefined> | string | undefined;
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### ExpirationStrategy Options
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
interface ExpirationOptions {
|
|
643
|
+
/** Time to live in seconds (default: 60) */
|
|
644
|
+
ttl?: number;
|
|
645
|
+
|
|
646
|
+
/** If true, delete on access after expiration. If false, delete via setTimeout (default: true) */
|
|
647
|
+
isLazy?: boolean;
|
|
648
|
+
|
|
649
|
+
/** If true, cache forever ignoring TTL (default: false) */
|
|
650
|
+
isCachedForever?: boolean;
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Advanced Usage
|
|
655
|
+
|
|
656
|
+
### Call Deduplication
|
|
657
|
+
|
|
658
|
+
The `@Cache` decorator automatically deduplicates concurrent calls with the same cache key. If multiple calls are made before the first one completes, they all receive the same result:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
class DataService {
|
|
662
|
+
@Cache(strategy, { ttl: 60 })
|
|
663
|
+
async fetchData(id: string): Promise<Data> {
|
|
664
|
+
console.log('Fetching...'); // Only logged once
|
|
665
|
+
return await slowApiCall(id);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const service = new DataService();
|
|
670
|
+
|
|
671
|
+
// All three calls share the same pending promise
|
|
672
|
+
const [a, b, c] = await Promise.all([
|
|
673
|
+
service.fetchData('123'),
|
|
674
|
+
service.fetchData('123'),
|
|
675
|
+
service.fetchData('123')
|
|
676
|
+
]);
|
|
677
|
+
// "Fetching..." is logged only once, all three get the same result
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Handling Undefined vs Null
|
|
681
|
+
|
|
682
|
+
The cache distinguishes between:
|
|
683
|
+
|
|
684
|
+
- `undefined`: No value found in cache, or value should not be cached
|
|
685
|
+
- `null`: Explicit null value that is cached
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
class UserService {
|
|
689
|
+
@Cache(strategy, { ttl: 60 })
|
|
690
|
+
async findUser(id: string): Promise<User | null> {
|
|
691
|
+
const user = await db.findUser(id);
|
|
692
|
+
// Return null for non-existent users to cache the "not found" result
|
|
693
|
+
// Return undefined would cause re-fetching on every call
|
|
694
|
+
return user ?? null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Error Handling
|
|
700
|
+
|
|
701
|
+
Cache errors are logged but don't break the application flow. If caching fails, the method executes normally:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// Cache read/write failures are logged as warnings:
|
|
705
|
+
// "@node-ts-cache/core: reading cache failed [key] [error]"
|
|
706
|
+
// "@node-ts-cache/core: writing result to cache failed [key] [error]"
|
|
707
|
+
|
|
708
|
+
// For RedisIOStorage, you can add a custom error handler:
|
|
709
|
+
storage.onError(error => {
|
|
710
|
+
metrics.incrementCacheError();
|
|
711
|
+
logger.error('Cache error', error);
|
|
712
|
+
});
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## Environment Variables
|
|
716
|
+
|
|
717
|
+
| Variable | Description |
|
|
718
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
719
|
+
| `DISABLE_CACHE_DECORATOR` | Set to any value to disable all `@Cache` decorators (useful for debugging) |
|
|
720
|
+
|
|
721
|
+
## Testing
|
|
722
|
+
|
|
723
|
+
```bash
|
|
724
|
+
# Run all tests
|
|
725
|
+
npm test
|
|
726
|
+
|
|
727
|
+
# Run tests in watch mode
|
|
728
|
+
npm run tdd
|
|
729
|
+
|
|
730
|
+
# Run tests with debugger
|
|
731
|
+
npm run tdd-debug-brk
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
## API Reference
|
|
735
|
+
|
|
736
|
+
### Exports
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
// Decorators
|
|
740
|
+
export { Cache } from './decorator/cache.decorator';
|
|
741
|
+
export { SyncCache } from './decorator/synccache.decorator';
|
|
742
|
+
export { MultiCache } from './decorator/multicache.decorator';
|
|
743
|
+
|
|
744
|
+
// Strategies
|
|
745
|
+
export { ExpirationStrategy } from './strategy/caching/expiration.strategy';
|
|
746
|
+
|
|
747
|
+
// Built-in Storages
|
|
748
|
+
export { MemoryStorage } from './storage/memory';
|
|
749
|
+
export { FsJsonStorage } from './storage/fs';
|
|
750
|
+
|
|
751
|
+
// Interfaces
|
|
752
|
+
export {
|
|
753
|
+
IAsynchronousCacheType,
|
|
754
|
+
ISynchronousCacheType,
|
|
755
|
+
IMultiIAsynchronousCacheType,
|
|
756
|
+
IMultiSynchronousCacheType
|
|
757
|
+
} from './types/cache.types';
|
|
758
|
+
export { ISyncKeyStrategy, IAsyncKeyStrategy } from './types/key.strategy.types';
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
## License
|
|
762
|
+
|
|
763
|
+
MIT License
|