@fluojs/cache-manager 1.0.6 → 2.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/README.ko.md +150 -7
- package/README.md +151 -8
- package/dist/decorators.js +2 -2
- package/dist/deferred-eviction.d.ts +13 -0
- package/dist/deferred-eviction.d.ts.map +1 -0
- package/dist/deferred-eviction.js +85 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +9 -49
- package/dist/module.d.ts +26 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +55 -9
- package/dist/operation-observer.d.ts +16 -0
- package/dist/operation-observer.d.ts.map +1 -0
- package/dist/operation-observer.js +61 -0
- package/dist/service.d.ts +21 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +58 -24
- package/dist/status.js +1 -1
- package/dist/store-operation-scheduler.d.ts +28 -0
- package/dist/store-operation-scheduler.d.ts.map +1 -0
- package/dist/store-operation-scheduler.js +49 -0
- package/dist/stores/memory-store.d.ts.map +1 -1
- package/dist/stores/memory-store.js +5 -1
- package/dist/stores/redis-store.d.ts.map +1 -1
- package/dist/stores/redis-store.js +12 -7
- package/dist/ttl-jitter.d.ts +19 -0
- package/dist/ttl-jitter.d.ts.map +1 -0
- package/dist/ttl-jitter.js +72 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/README.ko.md
CHANGED
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
- [애플리케이션 레벨 캐싱](#애플리케이션-레벨-캐싱)
|
|
14
14
|
- [공통 패턴](#공통-패턴)
|
|
15
15
|
- [Redis 저장소 사용](#redis-저장소-사용)
|
|
16
|
+
- [TTL 지터](#ttl-지터)
|
|
16
17
|
- [쿼리 매개변수 기반 캐싱](#쿼리-매개변수-기반-캐싱)
|
|
17
18
|
- [캐시 소유권과 reset 범위](#캐시-소유권과-reset-범위)
|
|
19
|
+
- [캐시 작업 관찰](#캐시-작업-관찰)
|
|
20
|
+
- [비동기 설정](#비동기-설정)
|
|
18
21
|
- [수동 모듈 조합](#수동-모듈-조합)
|
|
22
|
+
- [NestJS 캐시 마이그레이션](#nestjs-캐시-마이그레이션)
|
|
19
23
|
- [공개 API 개요](#공개-api-개요)
|
|
20
24
|
- [관련 패키지](#관련-패키지)
|
|
21
25
|
- [예제 소스](#예제-소스)
|
|
@@ -26,6 +30,8 @@
|
|
|
26
30
|
npm install @fluojs/cache-manager
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
`@fluojs/cache-manager`는 Node.js `>=24.0.0 <27`을 지원하며 `engines.node`로 정확히 이 범위를 선언합니다. 이 package-owned 지원 계약에 따라 Node 24 미만과 Node 27 이상은 제외됩니다. 이전 1.x 릴리스는 `engines.node >=20.0.0`을 광고했지만, 이는 실제 dependency floor와 일치한 적이 없습니다.
|
|
34
|
+
|
|
29
35
|
root `@fluojs/cache-manager` import는 memory-only 설치에서도 안전합니다. Redis client는 Redis 저장소 경로를 명시적으로 선택할 때만 필요합니다.
|
|
30
36
|
|
|
31
37
|
Lifecycle이 관리되는 `@fluojs/redis` client로 Redis 기반 캐싱을 사용하는 경우:
|
|
@@ -159,8 +165,30 @@ class AppModule {}
|
|
|
159
165
|
내장 `RedisStore`는 엔트리를 `JSON.stringify(...)`로 저장합니다. 따라서 캐시 값은 JSON 호환 형태여야 합니다. 일반 객체, 배열, 문자열, 숫자, 불리언, `null`은 안정적으로 round-trip 되지만, `Date`는 JSON 결과(예: ISO 문자열)로 돌아오고, 함수/`undefined`/`symbol`은 유지되지 않으며, `bigint`나 순환 그래프처럼 직렬화 불가능한 값은 캐싱 전에 정규화해야 합니다.
|
|
160
166
|
|
|
161
167
|
양수 Redis TTL 값은 초 단위로 받으며 소수도 허용됩니다. Redis `EX`는 정수 초를 사용하므로 Redis 만료 시간은 다음 정수 초로 올림하지만, fluo는 저장된 엔트리 안에 밀리초 정밀도의 만료 timestamp도 기록하고 해당 timestamp에 도달하면 값을 만료된 것으로 처리합니다. Redis 만료를 의도적으로 사용하지 않으려면 `ttl: 0`을 사용하세요.
|
|
168
|
+
예외적으로 큰 유한 TTL 값은 두 내장 store 모두에서 가장 큰 안전한 JavaScript 만료 timestamp로 제한되므로 Redis JSON metadata는 유한하게 유지되고 memory 경로와 일치합니다.
|
|
169
|
+
|
|
170
|
+
Redis reset 소유권은 기본값이 `fluo:cache:`이며 내장 `RedisStore` namespace로 전달되는 top-level `keyPrefix` 옵션으로 제한됩니다. Redis 기반 저장소에서 `CacheService.reset()`은 해당 prefix 아래의 키만 삭제하므로, cache prefix 밖의 애플리케이션 소유 Redis 데이터는 유지됩니다. 비어 있지 않은 prefix의 Redis glob metacharacter(`*`, `?`, `[`, `]`, `\`)는 `SCAN` 전에 escape되므로 설정한 prefix가 reset 소유권을 넓히지 않고 literal namespace로 유지됩니다. 의도적으로 빈 `keyPrefix`를 설정하면 reset은 `*`를 scan하지 않고 현재 `RedisStore` 인스턴스가 쓴 키로만 제한됩니다. 재시작 이후나 여러 프로세스에 걸친 캐시 엔트리까지 reset해야 한다면 비어 있지 않은 애플리케이션 전용 prefix를 사용하세요.
|
|
171
|
+
|
|
172
|
+
### TTL 지터
|
|
173
|
+
|
|
174
|
+
함께 기록된 인기 키는 같은 시점에 만료되어 origin 부하를 동기화할 수 있습니다. `ttlJitter`를 사용하면 양수 TTL 지터를 중앙에서 opt-in할 수 있습니다. `CacheService`는 memory, Redis 또는 custom store에 쓰기를 넘기기 전에 유효 TTL을 한 번 계산합니다.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
CacheModule.forRoot({
|
|
178
|
+
store: 'redis',
|
|
179
|
+
ttl: 600,
|
|
180
|
+
ttlJitter: {
|
|
181
|
+
ratio: 0.1,
|
|
182
|
+
mode: 'symmetric',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ratio`는 `0`보다 크고 `1` 이하여야 합니다. 기본 `symmetric` mode는 `ttl ± (ttl * ratio)` 범위에서 값을 뽑고, `shorten`은 TTL을 줄이기만 하며 `lengthen`은 늘리기만 합니다. `CacheService.set(...)` 또는 `remember(...)`의 per-call TTL override가 있으면 module 기본값 대신 해당 값에 지터를 적용합니다. `ttl: 0`은 계속 만료 없음 쓰기이며, 음수 또는 유한하지 않은 TTL 값은 여전히 쓰기를 건너뜁니다.
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
`ttlJitter`를 생략하거나 `undefined`로 설정한 경우에만 지터가 비활성화됩니다. `null`, primitive, array 및 invalid option field는 module 등록 중 거부됩니다. Optional `random` 함수는 deterministic test seam이며 `[0, 1]` 범위의 유한한 값을 반환해야 합니다. Invalid sample은 coercion하지 않고 write를 거부합니다. Production code에서는 일반적으로 기본 `Math.random`을 유지하세요.
|
|
190
|
+
|
|
191
|
+
지터가 적용된 모든 양수 TTL은 선택한 방향 범위 안에서 양수이자 유한한 값으로 유지됩니다. 완전히 단축된 TTL은 no-expiry sentinel이 되지 않고 JavaScript의 가장 작은 양수 유한값을 사용하며, 표현 가능한 범위를 넘는 증가 결과는 `Number.MAX_VALUE`에서 포화됩니다. TTL 지터는 만료 시점을 분산할 뿐입니다. Distributed locking, refresh-ahead caching 또는 cross-instance stampede coordination이 아닙니다.
|
|
164
192
|
|
|
165
193
|
### 쿼리 매개변수 기반 캐싱
|
|
166
194
|
|
|
@@ -175,7 +203,7 @@ CacheModule.forRoot({
|
|
|
175
203
|
})
|
|
176
204
|
```
|
|
177
205
|
|
|
178
|
-
완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요. 요청을 인식하는 cache key를 만들 때 지원되는 확장 경로는 이러한 function-based hook이며, cache key 생성만 바꾸기 위해 `CacheInterceptor`를 subclass하지 않습니다.
|
|
206
|
+
완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요. 빈 literal `@CacheKey('')`도 명시적인 key로 유지되며, decorator metadata가 없을 때만 설정된 `httpKeyStrategy`를 선택합니다. 요청을 인식하는 cache key를 만들 때 지원되는 확장 경로는 이러한 function-based hook이며, cache key 생성만 바꾸기 위해 `CacheInterceptor`를 subclass하지 않습니다.
|
|
179
207
|
|
|
180
208
|
```typescript
|
|
181
209
|
CacheModule.forRoot({
|
|
@@ -205,6 +233,8 @@ HTTP 인터셉터는 나중에 재사용할 수 있는 값이 있는 성공한,
|
|
|
205
233
|
|
|
206
234
|
### 캐시 소유권과 reset 범위
|
|
207
235
|
|
|
236
|
+
일반적인 `get(...)`, `set(...)`, `del(...)` 호출은 설정된 store에 대해 동시에 실행되므로, 한 키의 느린 store 호출이 관련 없는 키를 지연시키지 않습니다.
|
|
237
|
+
|
|
208
238
|
`CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 reset 경계에서 store read/write를 직렬화하고 진행 중인 `remember(...)` loader를 무효화하므로, reset 전에 시작된 loader가 reset 완료 후 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
|
|
209
239
|
|
|
210
240
|
```typescript
|
|
@@ -216,10 +246,89 @@ CacheModule.forRoot({
|
|
|
216
246
|
|
|
217
247
|
Redis cache prefix를 cache가 아닌 데이터와 공유하지 마세요. `del(key)`은 이 패키지가 해석한 정확한 캐시 키를 삭제하고, `reset()`은 위에서 설명한 store 소유 캐시 namespace만 삭제합니다.
|
|
218
248
|
|
|
219
|
-
애플리케이션이 종료될 때 `CacheService`는 새 store read/write를 중단하고 이미 시작된 store 작업을 기다린 뒤, `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
|
|
249
|
+
애플리케이션이 종료될 때 `CacheService`는 새 store read/write를 중단하고 이미 시작된 store 작업을 기다린 뒤, `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. 동시에 또는 반복해서 호출된 `close()`와 lifecycle hook은 첫 teardown의 완료 및 실패를 공유하므로, store teardown은 한 번만 실행되고 모든 호출자가 같은 shutdown 경계를 관찰합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
|
|
220
250
|
|
|
221
251
|
`CacheStore` 계약을 구현한 custom store는 `store` 옵션에 직접 전달할 수 있습니다. in-process LRU store, Redis 외 원격 캐시, 또는 cache operation을 관찰해야 하는 테스트 더블에 적합합니다.
|
|
222
252
|
|
|
253
|
+
### 캐시 작업 관찰
|
|
254
|
+
|
|
255
|
+
플랫폼 status helper는 캐시 가용성만 보고합니다. hit rate, latency, error outcome을 측정하려면 `CacheModule.forRoot(...)`에 opt-in `observer`를 전달하세요. 이 observer는 `@fluojs/metrics`와 독립적이므로, 애플리케이션이 이미 사용하는 metrics backend에 자유롭게 연결할 수 있습니다.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { CacheModule, type CacheObservation } from '@fluojs/cache-manager';
|
|
259
|
+
|
|
260
|
+
CacheModule.forRoot({
|
|
261
|
+
store: 'memory',
|
|
262
|
+
observer: {
|
|
263
|
+
onCacheOperation(observation: CacheObservation) {
|
|
264
|
+
cacheOperationCounter.inc({
|
|
265
|
+
operation: observation.operation,
|
|
266
|
+
outcome: observation.outcome,
|
|
267
|
+
});
|
|
268
|
+
cacheOperationLatency.observe(observation.durationMs);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
이 계약은 의도적으로 좁게 정의되어 있습니다.
|
|
275
|
+
|
|
276
|
+
- **프라이버시**: observation은 `operation`, `outcome`, `durationMs`만 전달합니다. cache key, 캐시된 값, loader 결과, error 객체는 observer로 전달되지 않으므로 계측이 애플리케이션 데이터를 유출할 수 없습니다.
|
|
277
|
+
- **operation taxonomy**: `operation`은 `get`, `set`, `del`, `remember`, `reset`, `close` 중 하나입니다. `remember`는 호출당 한 번 보고되며, 내부 read는 별도의 `get`으로 보고되지 않습니다.
|
|
278
|
+
- **outcome**: `CacheObservation`은 discriminated union입니다. read 작업(`get`, `remember`)은 `hit`, `miss`, `error`만 보고할 수 있고, write, invalidation, lifecycle 작업은 `success`, `error`만 보고할 수 있습니다. 같은 key의 in-flight load에 합류한 `remember` 호출은 캐시된 값을 읽지 않았으므로 `miss`를 보고합니다.
|
|
279
|
+
- **timing**: `durationMs`는 런타임의 monotonic `performance.now()` clock을 사용하여 store queue 직렬화를 포함한 전체 `CacheService` 작업 시간을 측정합니다.
|
|
280
|
+
- **실패 격리**: observer 오류는 삼켜집니다. throw된 error나 rejected promise는 caller가 받는 값을 바꾸지 않고 unhandled rejection으로도 노출되지 않습니다. observer 작업은 cache 작업이 await하지 않습니다.
|
|
281
|
+
- **HTTP fail-soft 상호작용**: `CacheInterceptor`는 여전히 store 실패를 삼켜서 캐시 문제가 정상 핸들러를 실패시키지 않도록 합니다. observer는 그 실패를 `error` observation으로 확인하므로, 요청 처리를 유지하면서 저하된 캐시를 알림하는 지원 경로가 됩니다.
|
|
282
|
+
|
|
283
|
+
`observer`를 설정하지 않으면 캐시는 관찰 작업 없이 기존 코드 경로 그대로 동작합니다.
|
|
284
|
+
Lifecycle diagnostic은 shutdown이 실제로 사용하는 teardown 소유자를 그대로 보고합니다. `createCacheManagerPlatformStatusSnapshot(...)`은 모든 non-memory store를 같게 취급하지 않고 lifecycle 책임에서 소유권을 해석합니다.
|
|
285
|
+
|
|
286
|
+
- 내장 메모리 store는 프레임워크가 in-process로 생성하고 보유하므로 `framework` 소유입니다.
|
|
287
|
+
- Custom store는 `CacheService.close()`가 optional `close()` 또는 `dispose()` hook으로 teardown을 전달할 책임을 가지므로 기본적으로 `framework` 소유입니다.
|
|
288
|
+
- Redis store는 client를 닫지 않는 `CacheService`에 대해 `external`입니다. Cache module이 `@fluojs/redis`를 통해 client를 해석하면 해당 integration이 lifecycle을 소유하고, `redis.client`로 client를 직접 전달하면 애플리케이션이 lifecycle을 소유합니다.
|
|
289
|
+
|
|
290
|
+
명시적인 `storeOwnershipMode`는 store 기본값보다 우선합니다. 애플리케이션이 custom store의 lifecycle 책임을 의도적으로 유지하는 경우 `external`로 설정하세요.
|
|
291
|
+
|
|
292
|
+
### 비동기 설정
|
|
293
|
+
|
|
294
|
+
최종 store, TTL, `keyPrefix`, key strategy를 DI나 비동기 bootstrap 작업에서 결정해야 한다면 `CacheModule.forRootAsync(...)`를 사용합니다. 의존성 토큰을 `inject`에 나열하고 `useFactory`에서 일반 `CacheModuleOptions`를 반환하면, module이 `CacheModule.forRoot(...)`와 동일한 기본값으로 그 결과를 정규화합니다.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Module } from '@fluojs/core';
|
|
298
|
+
import { CacheModule } from '@fluojs/cache-manager';
|
|
299
|
+
|
|
300
|
+
import { CacheSettingsService } from './cache-settings.service';
|
|
301
|
+
|
|
302
|
+
@Module({
|
|
303
|
+
imports: [
|
|
304
|
+
CacheModule.forRootAsync({
|
|
305
|
+
inject: [CacheSettingsService],
|
|
306
|
+
useFactory: async (settings: CacheSettingsService) => ({
|
|
307
|
+
store: 'redis',
|
|
308
|
+
ttl: await settings.resolveTtlSeconds(),
|
|
309
|
+
keyPrefix: settings.keyPrefix,
|
|
310
|
+
redis: { clientName: 'cache' },
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
],
|
|
314
|
+
})
|
|
315
|
+
class AppModule {}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Inject한 토큰은 cache module을 생성하는 container에 보여야 합니다. Cache options provider가 resolve되기 전에 bootstrap runtime provider로 제공하거나 globally visible한 imported module에서 export하세요. Import하는 parent module에만 local인 provider나 일반 sibling/parent export는 async cache module에 보이지 않습니다. Factory는 cache provider가 처음 resolve될 때 등록마다 한 번 실행되며, factory가 reject되면 부분적으로 설정된 cache를 등록하지 않고 bootstrap이 실패합니다.
|
|
319
|
+
|
|
320
|
+
모듈 가시성은 등록 호출이 소유합니다. 전역으로 노출하려면 `CacheModule.forRootAsync({ global: true, ... })`처럼 전달하세요. `useFactory`는 `global` property를 포함한 준비된 `CacheModuleOptions` 값을 반환할 수 있으며, module metadata는 factory 실행 전에 확정되므로 반환된 `global`은 무시됩니다.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
CacheModule.forRootAsync({
|
|
324
|
+
global: true,
|
|
325
|
+
inject: [CacheSettingsService],
|
|
326
|
+
useFactory: (settings: CacheSettingsService) => ({ store: settings.store }),
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
비동기 경로도 `forRoot(...)`와 동일한 store 선택을 지원합니다. `'memory'`, DI로 해석하거나 직접 전달한 client를 사용하는 `'redis'`, 그리고 모든 custom `CacheStore` instance를 사용할 수 있습니다.
|
|
331
|
+
|
|
223
332
|
### 수동 모듈 조합
|
|
224
333
|
|
|
225
334
|
일반적인 애플리케이션 설정과 커스텀 `defineModule(...)` 조합에서는 `CacheModule.forRoot(...)`를 사용합니다.
|
|
@@ -236,6 +345,33 @@ defineModule(ManualCacheModule, {
|
|
|
236
345
|
});
|
|
237
346
|
```
|
|
238
347
|
|
|
348
|
+
### NestJS 캐시 마이그레이션
|
|
349
|
+
|
|
350
|
+
`@nestjs/cache-manager`와 `@fluojs/cache-manager`는 cache 개념이 일부 겹치지만 option 이름, 단위, 기본값, 소유권이 모두 그대로 유지되지는 않습니다. 아래 항목을 각각 변환하고, 전체 마이그레이션 계약은 [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.ko.md)을 참고하세요.
|
|
351
|
+
|
|
352
|
+
| NestJS option 또는 decorator | fluo 대응 | 변환 규칙 |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| 설치된 underlying `cache-manager` generation이 millisecond를 사용하는 경우의 `ttl` | 초 단위 `ttl` | 설치된 underlying `cache-manager` dependency/version을 확인하세요. 해당 generation이 TTL을 millisecond로 정의할 때에만 1000으로 나눕니다. `ttl`을 생략하면 memory 경로는 `300`초를, `redis` 및 custom-store 경로는 `0`을 적용합니다. |
|
|
355
|
+
| `ttl: 0` | `ttl: 0` | "캐싱하지 않음"이 아니라 만료 없음을 뜻합니다. 음수이거나 유한하지 않은 값은 잘못된 값으로 처리되어 `CacheService.set(...)`은 쓰기를 건너뛰고 `CacheInterceptor`는 해당 handler의 cache 읽기와 쓰기를 모두 건너뜁니다. |
|
|
356
|
+
| `@CacheTTL(...)` | `@CacheTTL(ttlSeconds: number)` | 정적 숫자 하나만 받습니다. 요청마다 달라지는 lifetime은 `CacheService.set(key, value, ttlSeconds)`로 옮기세요. |
|
|
357
|
+
| 암묵적 query 민감 key | `httpKeyStrategy` | 기본값은 path만 사용하는 `'route'`입니다. 응답이 query parameter에 따라 달라지면 `'route+query'`(또는 `'full'`), function strategy, `@CacheKey(...)` 중 하나를 선택하세요. |
|
|
358
|
+
| `isGlobal: true` | `global: true` | NestJS `isGlobal`과 fluo `global`은 모두 기본값이 `false`이므로, 명시적으로 opt-in하거나 cache provider를 resolve하는 모든 module에 import하지 않으면 두 cache module 모두 module-local로 유지됩니다. |
|
|
359
|
+
| `cache-manager-redis-store` 같은 NestJS store adapter | `store: 'redis'` 또는 `CacheStore` 객체 | NestJS adapter는 `CacheStore` 계약을 만족하지 않습니다. 내장 Redis 경로를 쓰거나 callback/options 완료를 Promise로 변환하고, `ttlSeconds`를 legacy TTL 초 단위로 매핑하며, `reset()`이 cache namespace만 비우도록 adapter를 감싸세요. `reset()`을 whole-database `flushDb`로 무분별하게 전달하면 안 됩니다. |
|
|
360
|
+
| adapter가 소유하던 client teardown | store의 `close()` / `dispose()` | 애플리케이션 shutdown은 이 optional hook에만 teardown을 전달합니다. `redis.client`로 전달한 raw client는 애플리케이션 소유로 남아 애플리케이션 lifecycle에서 닫아야 합니다. |
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
CacheModule.forRoot({
|
|
364
|
+
// 설치된 underlying cache-manager generation이 milliseconds를 사용할 때
|
|
365
|
+
// NestJS `ttl: 60_000`은 60초가 됩니다.
|
|
366
|
+
ttl: 60,
|
|
367
|
+
// NestJS `isGlobal: true` becomes `global: true`.
|
|
368
|
+
global: true,
|
|
369
|
+
// Opt in explicitly when responses vary by query parameters.
|
|
370
|
+
httpKeyStrategy: 'route+query',
|
|
371
|
+
store: 'redis',
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
239
375
|
### 메모리 저장소 운영 한계
|
|
240
376
|
|
|
241
377
|
내장 메모리 저장소는 단일 프로세스의 bounded cache 용도로 설계되어 있습니다.
|
|
@@ -263,20 +399,26 @@ class ProductController {
|
|
|
263
399
|
}
|
|
264
400
|
```
|
|
265
401
|
|
|
266
|
-
이렇게 지원되는 HTTP 경로에서는
|
|
402
|
+
이렇게 지원되는 HTTP 경로에서는 framework response writer가 성공적으로 settle되고 response가 commit 완료를 보고할 때까지 cache eviction을 지연합니다. Writer가 reject되거나 commit 확인 없이 settle되거나, disconnect 또는 shutdown으로 commit 전에 request가 abort되면 지연 eviction을 취소하여 이전 cached read 결과를 유지합니다. `response.send(...)`를 호출하지 않고 commit하는 adapter 경로도 bounded 5초 fallback을 유지합니다. 이 fallback은 deadline에 `response.committed`가 이미 commit을 확인한 경우에만 eviction을 실행하고, 확인되지 않은 response는 취소하므로 경과 시간만으로 이후의 실패한 commit보다 먼저 cache를 삭제하지 않습니다. Fallback timer는 Node.js에서 unref되고 response writer가 settle되면 clear되므로 pending fallback work가 process shutdown을 계속 붙잡지 않습니다. 또한 지연 eviction 실패는 interceptor 내부에 containment되어 cache key factory나 cache store 삭제 오류가 response 이후 unhandled promise rejection으로 노출되지 않습니다.
|
|
267
403
|
|
|
268
404
|
## 공개 API 개요
|
|
269
405
|
|
|
270
406
|
### 모듈
|
|
271
|
-
- `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis/custom), 기본 TTL, 키 전략, `global`, `principalScopeResolver`, Redis namespace `keyPrefix`, `redis.scanCount` 같은 Redis 옵션을 설정합니다.
|
|
407
|
+
- `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis/custom), 기본 TTL, opt-in `ttlJitter`, 키 전략, `global`, `principalScopeResolver`, Redis namespace `keyPrefix`, `redis.scanCount` 같은 Redis 옵션을 설정합니다.
|
|
272
408
|
애플리케이션 모듈에서 사용하는 기본 패키지 진입점입니다.
|
|
409
|
+
- `CacheModule.forRootAsync({ inject, useFactory, global? })`: cache 설정을 DI나 비동기 bootstrap 작업에서 만드는 애플리케이션을 위해 동일한 옵션을 injected factory로 해석합니다. `global`은 이 등록 호출이 소유하며, factory가 reject되면 bootstrap이 실패합니다.
|
|
273
410
|
|
|
274
411
|
### 공개 타입
|
|
275
|
-
- `CacheModuleOptions`: `CacheModule.forRoot(...)`가 받는 애플리케이션-facing
|
|
412
|
+
- `CacheModuleOptions`: `CacheModule.forRoot(...)`가 받는 애플리케이션-facing 설정이며 optional `ttlJitter`와 `observer`를 포함합니다.
|
|
413
|
+
- `CacheTtlJitterOptions`, `CacheTtlJitterMode`: Opt-in 양수 TTL 지터의 범위, 방향, deterministic randomness seam을 정의합니다.
|
|
414
|
+
- `NormalizedCacheTtlJitterOptions`: 기본값이 적용된 정규화 TTL 지터 설정입니다.
|
|
415
|
+
- `CacheObserver`: 단일 `onCacheOperation(observation)` 메서드를 가지는 opt-in 관찰 hook입니다.
|
|
416
|
+
- `CacheObservation`: 각 operation category를 유효한 outcome과 결합하고 `durationMs`를 전달하는 privacy-safe discriminated union입니다.
|
|
417
|
+
- `CacheAsyncModuleOptions`: `CacheModule.forRootAsync(...)`가 받는 injected-factory 설정입니다. `useFactory`는 `CacheModuleOptions`를 반환하며, module visibility는 등록 수준의 `global`만 따릅니다.
|
|
276
418
|
- `NormalizedCacheModuleOptions`: 기본값이 적용된 정규화 설정 모양과 일치하는 compatibility-only type export입니다. 애플리케이션 코드에서는 `CacheModuleOptions`를 우선 사용하세요. 이 타입은 이전에 배포된 declaration surface를 참조한 소비자가 계속 컴파일되도록 공개 상태를 유지합니다.
|
|
277
419
|
|
|
278
420
|
### 서비스
|
|
279
|
-
- `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`, `close`)을 위한 기본 API입니다. 애플리케이션 shutdown은 같은 `close()` 경로를 호출하며, 이 경로는 `close()` 또는 `dispose()`를 노출하는 custom store로 teardown을
|
|
421
|
+
- `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`, `close`)을 위한 기본 API입니다. 애플리케이션 shutdown은 같은 `close()` 경로를 호출하며, 이 경로는 `close()` 또는 `dispose()`를 노출하는 custom store로 teardown을 전달하고 동시에 또는 반복해서 호출한 caller가 첫 teardown 완료를 공유하도록 합니다.
|
|
280
422
|
|
|
281
423
|
### 데코레이터
|
|
282
424
|
- `@CacheTTL(seconds)`: 특정 핸들러의 TTL을 설정합니다.
|
|
@@ -303,3 +445,4 @@ class ProductController {
|
|
|
303
445
|
- `packages/cache-manager/src/interceptor.test.ts`: HTTP 캐싱 및 삭제 테스트.
|
|
304
446
|
- `packages/cache-manager/src/service.ts`: 코어 `CacheService` 구현.
|
|
305
447
|
- `packages/cache-manager/src/status.test.ts`: status 및 diagnostic helper 테스트.
|
|
448
|
+
- `packages/cache-manager/src/cache-observer.test.ts`: 캐시 관찰 계약 테스트.
|
package/README.md
CHANGED
|
@@ -13,9 +13,13 @@ General-purpose cache manager for fluo with pluggable memory, Redis, and custom
|
|
|
13
13
|
- [Application-Level Caching](#application-level-caching)
|
|
14
14
|
- [Common Patterns](#common-patterns)
|
|
15
15
|
- [Redis Storage](#redis-storage)
|
|
16
|
+
- [TTL Jitter](#ttl-jitter)
|
|
16
17
|
- [Query-Sensitive Caching](#query-sensitive-caching)
|
|
17
18
|
- [Cache Ownership and Reset Scope](#cache-ownership-and-reset-scope)
|
|
19
|
+
- [Observing Cache Operations](#observing-cache-operations)
|
|
20
|
+
- [Async Configuration](#async-configuration)
|
|
18
21
|
- [Manual Module Composition](#manual-module-composition)
|
|
22
|
+
- [NestJS Cache Migration](#nestjs-cache-migration)
|
|
19
23
|
- [Public API Overview](#public-api-overview)
|
|
20
24
|
- [Related Packages](#related-packages)
|
|
21
25
|
- [Example Sources](#example-sources)
|
|
@@ -26,6 +30,8 @@ General-purpose cache manager for fluo with pluggable memory, Redis, and custom
|
|
|
26
30
|
npm install @fluojs/cache-manager
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
`@fluojs/cache-manager` supports Node.js `>=24.0.0 <27` and declares that exact range through `engines.node`. That package-owned support contract means Node versions below 24 and Node 27+ are excluded. Earlier 1.x releases advertised `engines.node >=20.0.0`, which never matched the effective dependency floor.
|
|
34
|
+
|
|
29
35
|
The root `@fluojs/cache-manager` import stays safe for memory-only installs. You only need a Redis client when you explicitly select the Redis-backed store path.
|
|
30
36
|
|
|
31
37
|
For Redis-backed caching with a lifecycle-managed `@fluojs/redis` client:
|
|
@@ -159,8 +165,30 @@ class AppModule {}
|
|
|
159
165
|
The built-in `RedisStore` persists entries with `JSON.stringify(...)`. Cache values therefore need to be JSON-compatible: plain objects, arrays, strings, numbers, booleans, and `null` round-trip cleanly, while values such as `Date` come back as JSON output (for example ISO strings), functions/`undefined`/symbols do not survive, and non-serializable values like `bigint` or cyclic graphs should be normalized before caching.
|
|
160
166
|
|
|
161
167
|
Positive Redis TTL values are accepted in seconds and may be fractional. Redis expiry is rounded up to the next whole second because Redis `EX` uses integer seconds, while fluo also records the millisecond-precision expiry timestamp in the stored entry and treats the value as expired once that timestamp is reached. Use `ttl: 0` when you intentionally want no Redis expiry.
|
|
168
|
+
Exceptionally large finite TTL values are capped at the largest safe JavaScript expiry timestamp by both built-in stores, so Redis JSON metadata remains finite and aligns with the memory path.
|
|
169
|
+
|
|
170
|
+
Redis reset ownership is scoped by the top-level `keyPrefix` option, which defaults to `fluo:cache:` and is passed through to the built-in `RedisStore` namespace. `CacheService.reset()` deletes only keys under that prefix for Redis-backed stores, so application-owned Redis data outside the cache prefix is preserved. Redis glob metacharacters in a non-empty prefix (`*`, `?`, `[`, `]`, and `\`) are escaped before `SCAN`, so the configured prefix remains a literal namespace instead of broadening reset ownership. If you intentionally configure an empty `keyPrefix`, reset is limited to keys written by the current `RedisStore` instance instead of scanning `*`; use a non-empty, application-specific prefix when you need reset to cover cache entries across restarts or multiple processes.
|
|
171
|
+
|
|
172
|
+
### TTL Jitter
|
|
173
|
+
|
|
174
|
+
Popular keys written together can otherwise expire together and synchronize origin load. Opt in to centralized positive-TTL jitter with `ttlJitter`; `CacheService` calculates the effective TTL once before handing the write to memory, Redis, or a custom store.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
CacheModule.forRoot({
|
|
178
|
+
store: 'redis',
|
|
179
|
+
ttl: 600,
|
|
180
|
+
ttlJitter: {
|
|
181
|
+
ratio: 0.1,
|
|
182
|
+
mode: 'symmetric',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ratio` must be greater than `0` and at most `1`. The default `symmetric` mode samples within `ttl ± (ttl * ratio)`; `shorten` only subtracts from the TTL and `lengthen` only adds to it. A `CacheService.set(...)` or `remember(...)` per-call TTL override is jittered instead of the module default. `ttl: 0` remains a no-expiry write, and negative or non-finite TTL values still skip the write.
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
Jitter is disabled only when `ttlJitter` is omitted or `undefined`; `null`, primitives, arrays, and invalid option fields are rejected during module registration. The optional `random` function is a deterministic test seam and must return a finite value in `[0, 1]`; an invalid sample rejects the write instead of being coerced. Production code should normally keep the default `Math.random`.
|
|
190
|
+
|
|
191
|
+
Every jittered positive TTL remains positive and finite within its selected direction. A fully shortened TTL uses JavaScript's smallest positive finite value rather than becoming the no-expiry sentinel, while an upward result beyond the representable range saturates at `Number.MAX_VALUE`. TTL jitter spreads expiry times only. It is not distributed locking, refresh-ahead caching, or cross-instance stampede coordination.
|
|
164
192
|
|
|
165
193
|
### Query-Sensitive Caching
|
|
166
194
|
|
|
@@ -175,7 +203,7 @@ CacheModule.forRoot({
|
|
|
175
203
|
})
|
|
176
204
|
```
|
|
177
205
|
|
|
178
|
-
For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory. These function-based hooks are the supported extension path for request-aware keys; do not subclass `CacheInterceptor` just to replace cache-key generation.
|
|
206
|
+
For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory. An empty literal `@CacheKey('')` remains an explicit key; only absent decorator metadata selects the configured `httpKeyStrategy`. These function-based hooks are the supported extension path for request-aware keys; do not subclass `CacheInterceptor` just to replace cache-key generation.
|
|
179
207
|
|
|
180
208
|
```typescript
|
|
181
209
|
CacheModule.forRoot({
|
|
@@ -205,6 +233,8 @@ The HTTP interceptor caches only successful, uncommitted GET handler results wit
|
|
|
205
233
|
|
|
206
234
|
### Cache Ownership and Reset Scope
|
|
207
235
|
|
|
236
|
+
Ordinary `get(...)`, `set(...)`, and `del(...)` calls run concurrently against the configured store, so a slow store call for one key does not delay unrelated keys.
|
|
237
|
+
|
|
208
238
|
`CacheService.reset()` clears entries owned by the configured store, not unrelated application state. It also serializes store reads/writes across the reset boundary and invalidates in-flight `remember(...)` loaders so loaders that started before the reset cannot repopulate stale entries after the reset completes. For the built-in memory store that means the in-process entries held by that store instance. For Redis, ownership is the configured `keyPrefix` namespace; keep the default `fluo:cache:` or choose a dedicated prefix such as `myapp:cache:` for shared Redis deployments.
|
|
209
239
|
|
|
210
240
|
```typescript
|
|
@@ -216,10 +246,89 @@ CacheModule.forRoot({
|
|
|
216
246
|
|
|
217
247
|
Avoid sharing a Redis cache prefix with non-cache data. `del(key)` removes the exact cache key resolved by this package, while `reset()` removes only the store-owned cache namespace described above.
|
|
218
248
|
|
|
219
|
-
When the application closes, `CacheService` stops new store reads/writes, waits for already-started store operations, and then forwards shutdown to custom stores that expose `close()` or `dispose()`. Use one of those optional hooks when a store owns sockets, pools, timers, or other external resources.
|
|
249
|
+
When the application closes, `CacheService` stops new store reads/writes, waits for already-started store operations, and then forwards shutdown to custom stores that expose `close()` or `dispose()`. Concurrent and repeated `close()` or lifecycle-hook calls share that first teardown completion and failure, so every caller observes the same shutdown boundary while store teardown runs once. Use one of those optional hooks when a store owns sockets, pools, timers, or other external resources.
|
|
220
250
|
|
|
221
251
|
Custom stores can be passed directly through `store` when they implement the `CacheStore` contract. This is the right option for in-process LRU stores, remote caches other than Redis, or test doubles that need to observe cache operations.
|
|
222
252
|
|
|
253
|
+
### Observing Cache Operations
|
|
254
|
+
|
|
255
|
+
The platform status helpers report cache availability only. To measure hit rate, latency, and error outcomes, pass an opt-in `observer` to `CacheModule.forRoot(...)`. The observer is independent of `@fluojs/metrics`; wire it to whichever metrics backend the application already uses.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { CacheModule, type CacheObservation } from '@fluojs/cache-manager';
|
|
259
|
+
|
|
260
|
+
CacheModule.forRoot({
|
|
261
|
+
store: 'memory',
|
|
262
|
+
observer: {
|
|
263
|
+
onCacheOperation(observation: CacheObservation) {
|
|
264
|
+
cacheOperationCounter.inc({
|
|
265
|
+
operation: observation.operation,
|
|
266
|
+
outcome: observation.outcome,
|
|
267
|
+
});
|
|
268
|
+
cacheOperationLatency.observe(observation.durationMs);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The contract is intentionally narrow:
|
|
275
|
+
|
|
276
|
+
- **Privacy**: an observation carries only `operation`, `outcome`, and `durationMs`. Cache keys, cached values, loader results, and error objects are never passed to the observer, so instrumentation cannot leak application data.
|
|
277
|
+
- **Operation taxonomy**: `operation` is one of `get`, `set`, `del`, `remember`, `reset`, or `close`. `remember` is reported once per call; its internal read is not reported as a separate `get`.
|
|
278
|
+
- **Outcomes**: `CacheObservation` is a discriminated union: read operations (`get`, `remember`) can report only `hit`, `miss`, or `error`, while write, invalidation, and lifecycle operations can report only `success` or `error`. A `remember` call that joins an in-flight load for the same key reports `miss`, because that call did not read a cached value.
|
|
279
|
+
- **Timing**: `durationMs` measures the full `CacheService` operation, including store-queue serialization, with the runtime's monotonic `performance.now()` clock.
|
|
280
|
+
- **Failure containment**: observer errors are swallowed. A thrown error or a rejected promise never changes the value the caller receives and never surfaces as an unhandled rejection. Observer work is not awaited by the cache operation.
|
|
281
|
+
- **HTTP fail-soft interaction**: `CacheInterceptor` still swallows store failures so cache problems cannot fail an otherwise successful handler. The observer sees those failures as `error` observations, which is the supported way to alert on a degraded cache while keeping requests served.
|
|
282
|
+
|
|
283
|
+
When no `observer` is configured, the cache runs its original code path with no observation work.
|
|
284
|
+
Lifecycle diagnostics report the same teardown owner that shutdown actually uses. `createCacheManagerPlatformStatusSnapshot(...)` resolves ownership from lifecycle responsibility rather than treating every non-memory store alike:
|
|
285
|
+
|
|
286
|
+
- The built-in memory store is `framework`-owned because the framework creates and holds it in-process.
|
|
287
|
+
- A custom store is `framework`-owned by default because `CacheService.close()` owns teardown dispatch to its optional `close()` or `dispose()` hook.
|
|
288
|
+
- The Redis store is `external` to `CacheService`, which never closes the client. When the cache module resolves a client through `@fluojs/redis`, that integration owns its lifecycle; when `redis.client` supplies a client directly, the application owns its lifecycle.
|
|
289
|
+
|
|
290
|
+
An explicit `storeOwnershipMode` still wins over the store default. Set it to `external` when the application intentionally retains lifecycle responsibility for a custom store.
|
|
291
|
+
|
|
292
|
+
### Async Configuration
|
|
293
|
+
|
|
294
|
+
Use `CacheModule.forRootAsync(...)` when the final store, TTL, `keyPrefix`, or key strategy must come from DI or asynchronous bootstrap work. List the dependency tokens in `inject`, return ordinary `CacheModuleOptions` from `useFactory`, and the module normalizes that result with the same defaults as `CacheModule.forRoot(...)`.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Module } from '@fluojs/core';
|
|
298
|
+
import { CacheModule } from '@fluojs/cache-manager';
|
|
299
|
+
|
|
300
|
+
import { CacheSettingsService } from './cache-settings.service';
|
|
301
|
+
|
|
302
|
+
@Module({
|
|
303
|
+
imports: [
|
|
304
|
+
CacheModule.forRootAsync({
|
|
305
|
+
inject: [CacheSettingsService],
|
|
306
|
+
useFactory: async (settings: CacheSettingsService) => ({
|
|
307
|
+
store: 'redis',
|
|
308
|
+
ttl: await settings.resolveTtlSeconds(),
|
|
309
|
+
keyPrefix: settings.keyPrefix,
|
|
310
|
+
redis: { clientName: 'cache' },
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
],
|
|
314
|
+
})
|
|
315
|
+
class AppModule {}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Injected tokens must be visible to the container that instantiates the cache module. Provide them as bootstrap runtime providers or export them from a globally visible imported module before the cache options provider resolves. A provider local only to the importing parent module, or an ordinary sibling/parent export, is not visible to the async cache module. The factory runs once per registration when cache providers are first resolved, and a rejected factory fails bootstrap instead of registering a partially configured cache.
|
|
319
|
+
|
|
320
|
+
Module visibility stays on the registration call: pass `global: true` to `CacheModule.forRootAsync({ global: true, ... })`. `useFactory` may return a prepared `CacheModuleOptions` value, including its `global` property; any returned `global` is ignored because module metadata is fixed before the factory runs.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
CacheModule.forRootAsync({
|
|
324
|
+
global: true,
|
|
325
|
+
inject: [CacheSettingsService],
|
|
326
|
+
useFactory: (settings: CacheSettingsService) => ({ store: settings.store }),
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The async path supports the same store selection as `forRoot(...)`: `'memory'`, `'redis'` with a DI-resolved or directly supplied client, and any custom `CacheStore` instance.
|
|
331
|
+
|
|
223
332
|
### Manual Module Composition
|
|
224
333
|
|
|
225
334
|
Use `CacheModule.forRoot(...)` for normal application setup, including custom `defineModule(...)` composition.
|
|
@@ -236,6 +345,33 @@ defineModule(ManualCacheModule, {
|
|
|
236
345
|
});
|
|
237
346
|
```
|
|
238
347
|
|
|
348
|
+
### NestJS Cache Migration
|
|
349
|
+
|
|
350
|
+
`@nestjs/cache-manager` and `@fluojs/cache-manager` expose overlapping cache concepts, but their option names, units, defaults, and ownership do not all carry over. Convert each of the following, and see [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.md) for the full migration contract.
|
|
351
|
+
|
|
352
|
+
| NestJS option or decorator | fluo equivalent | Conversion rule |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `ttl` when the installed underlying `cache-manager` generation uses milliseconds | `ttl` in seconds | Inspect the installed underlying `cache-manager` dependency/version. Divide by 1000 only when that generation defines TTLs in milliseconds. Omitting `ttl` applies `300` seconds on the memory path and `0` for the `redis` and custom-store paths. |
|
|
355
|
+
| `ttl: 0` | `ttl: 0` | Means no expiry, not "do not cache". Negative or non-finite values are invalid: `CacheService.set(...)` drops the write, and `CacheInterceptor` skips both the cache read and write for that handler. |
|
|
356
|
+
| `@CacheTTL(...)` | `@CacheTTL(ttlSeconds: number)` | Accepts one static number only. Move per-request lifetimes to `CacheService.set(key, value, ttlSeconds)`. |
|
|
357
|
+
| implicit query-sensitive keys | `httpKeyStrategy` | Defaults to path-only `'route'`. Select `'route+query'` (or `'full'`), a function strategy, or `@CacheKey(...)` when a response varies by query parameters. |
|
|
358
|
+
| `isGlobal: true` | `global: true` | Both NestJS `isGlobal` and fluo `global` default to `false`, so both cache modules are module-local unless you opt in or import the module everywhere it is resolved. |
|
|
359
|
+
| NestJS store adapters such as `cache-manager-redis-store` | `store: 'redis'` or a `CacheStore` object | NestJS adapters do not satisfy the `CacheStore` contract; use the built-in Redis path or wrap the adapter so callback/options completion becomes a Promise, `ttlSeconds` maps to the legacy TTL in seconds, and `reset()` clears only the cache namespace. Never forward `reset()` blindly to a whole-database `flushDb`. |
|
|
360
|
+
| adapter-owned client teardown | `close()` / `dispose()` on the store | Application shutdown forwards teardown only to those optional hooks. A raw client passed through `redis.client` stays application-owned and must be closed from the application lifecycle. |
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
CacheModule.forRoot({
|
|
364
|
+
// If the installed underlying cache-manager generation uses milliseconds,
|
|
365
|
+
// NestJS `ttl: 60_000` becomes 60 seconds.
|
|
366
|
+
ttl: 60,
|
|
367
|
+
// NestJS `isGlobal: true` becomes `global: true`.
|
|
368
|
+
global: true,
|
|
369
|
+
// Opt in explicitly when responses vary by query parameters.
|
|
370
|
+
httpKeyStrategy: 'route+query',
|
|
371
|
+
store: 'redis',
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
239
375
|
### Memory Store Operational Limits
|
|
240
376
|
|
|
241
377
|
The built-in memory store is designed for single-process, bounded caching:
|
|
@@ -263,20 +399,26 @@ class ProductController {
|
|
|
263
399
|
}
|
|
264
400
|
```
|
|
265
401
|
|
|
266
|
-
On that supported HTTP path, eviction is deferred until
|
|
402
|
+
On that supported HTTP path, eviction is deferred until a framework response writer settles successfully and the response reports that it committed. If a writer rejects, settles without a confirmed commit, or the request aborts before commit because of disconnect or shutdown, deferred eviction is cancelled so the previous cached read result remains available. Adapter paths that commit without invoking `response.send(...)` retain the bounded five-second fallback: it evicts only when `response.committed` already confirms commit at the deadline. An unconfirmed response is cancelled instead, so elapsed time alone cannot evict before a later failed commit. The fallback timer is unreferenced on Node.js and cleared when a response writer settles, so pending fallback work does not keep process shutdown alive. Deferred eviction failures stay contained inside the interceptor, so cache-key factories or cache-store deletes cannot surface as post-response unhandled promise rejections.
|
|
267
403
|
|
|
268
404
|
## Public API Overview
|
|
269
405
|
|
|
270
406
|
### Modules
|
|
271
|
-
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis/custom), default TTL, key strategies, `global`, `principalScopeResolver`, the Redis namespace `keyPrefix`, and Redis options such as `redis.scanCount`.
|
|
407
|
+
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis/custom), default TTL, opt-in `ttlJitter`, key strategies, `global`, `principalScopeResolver`, the Redis namespace `keyPrefix`, and Redis options such as `redis.scanCount`.
|
|
272
408
|
This is the primary package entrypoint for application modules.
|
|
409
|
+
- `CacheModule.forRootAsync({ inject, useFactory, global? })`: Resolves the same options through an injected factory for applications that build cache configuration from DI or asynchronous bootstrap work. `global` belongs to this registration call, and a rejected factory fails bootstrap.
|
|
273
410
|
|
|
274
411
|
### Public types
|
|
275
|
-
- `CacheModuleOptions`: Application-facing configuration accepted by `CacheModule.forRoot(...)`.
|
|
276
|
-
- `
|
|
412
|
+
- `CacheModuleOptions`: Application-facing configuration accepted by `CacheModule.forRoot(...)`, including optional `ttlJitter` and `observer`.
|
|
413
|
+
- `CacheTtlJitterOptions` and `CacheTtlJitterMode`: Opt-in positive-TTL jitter bounds, direction, and deterministic randomness seam.
|
|
414
|
+
- `NormalizedCacheTtlJitterOptions`: Normalized TTL jitter configuration after defaults are applied.
|
|
415
|
+
- `CacheObserver`: Opt-in observation hook with a single `onCacheOperation(observation)` method.
|
|
416
|
+
- `CacheObservation`: Privacy-safe discriminated union coupling each operation category to its valid outcomes and carrying `durationMs`.
|
|
417
|
+
- `CacheAsyncModuleOptions`: Injected-factory configuration accepted by `CacheModule.forRootAsync(...)`. `useFactory` returns `CacheModuleOptions`; registration-level `global` alone controls module visibility.
|
|
418
|
+
- `NormalizedCacheModuleOptions`: Compatibility-only type export matching the normalized module configuration shape after defaults are applied. Prefer `CacheModuleOptions` for application code; this type remains public so consumers that referenced the previously shipped declaration surface can keep compiling.
|
|
277
419
|
|
|
278
420
|
### Services
|
|
279
|
-
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`, `close`). Application shutdown calls the same `close()` path, which forwards teardown to custom stores exposing `close()` or `dispose()
|
|
421
|
+
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`, `close`). Application shutdown calls the same `close()` path, which forwards teardown to custom stores exposing `close()` or `dispose()` and shares the first teardown completion across concurrent or repeated callers.
|
|
280
422
|
|
|
281
423
|
### Decorators
|
|
282
424
|
- `@CacheTTL(seconds)`: Sets the TTL for a specific handler.
|
|
@@ -303,3 +445,4 @@ On that supported HTTP path, eviction is deferred until the response successfull
|
|
|
303
445
|
- `packages/cache-manager/src/interceptor.test.ts`: HTTP caching and eviction tests.
|
|
304
446
|
- `packages/cache-manager/src/service.ts`: Core `CacheService` implementation.
|
|
305
447
|
- `packages/cache-manager/src/status.test.ts`: Status and diagnostic helper tests.
|
|
448
|
+
- `packages/cache-manager/src/cache-observer.test.ts`: Cache observation contract tests.
|
package/dist/decorators.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureRequestPipelineMetadataSymbol } from '@fluojs/core/request-pipeline';
|
|
2
2
|
/** Shared controller metadata key used to store per-route cache metadata records. */
|
|
3
3
|
export const cacheRouteMetadataKey = Symbol.for('fluo.standard.route');
|
|
4
4
|
const cacheKeyMetadataKey = Symbol.for('fluo.cache.key');
|
|
5
5
|
const cacheTtlMetadataKey = Symbol.for('fluo.cache.ttl');
|
|
6
6
|
const cacheEvictMetadataKey = Symbol.for('fluo.cache.evict');
|
|
7
|
-
|
|
7
|
+
ensureRequestPipelineMetadataSymbol();
|
|
8
8
|
function getMetadataBag(metadata) {
|
|
9
9
|
return metadata;
|
|
10
10
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InterceptorContext } from '@fluojs/http';
|
|
2
|
+
type RequestAbortState = Pick<InterceptorContext['requestContext']['request'], 'isAborted' | 'signal'>;
|
|
3
|
+
/**
|
|
4
|
+
* Defers cache eviction until a response writer or bounded fallback confirms commit.
|
|
5
|
+
*
|
|
6
|
+
* @param response Active framework response whose writers and committed flag own cleanup.
|
|
7
|
+
* @param request Request cancellation surfaces used to discard eviction during shutdown or disconnect.
|
|
8
|
+
* @param evict Cache eviction work to run after a confirmed successful commit.
|
|
9
|
+
* @returns A cancellation function that restores the response writers and clears the fallback timer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installDeferredEviction(response: InterceptorContext['requestContext']['response'], request: RequestAbortState, evict: () => Promise<void>): () => void;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=deferred-eviction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred-eviction.d.ts","sourceRoot":"","sources":["../src/deferred-eviction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIvD,KAAK,iBAAiB,GAAG,IAAI,CAC3B,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,SAAS,CAAC,EAC/C,WAAW,GAAG,QAAQ,CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,UAAU,CAAC,EAC1D,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACzB,MAAM,IAAI,CAwFZ"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const EVICTION_FALLBACK_TIMEOUT_MS = 5_000;
|
|
2
|
+
/**
|
|
3
|
+
* Defers cache eviction until a response writer or bounded fallback confirms commit.
|
|
4
|
+
*
|
|
5
|
+
* @param response Active framework response whose writers and committed flag own cleanup.
|
|
6
|
+
* @param request Request cancellation surfaces used to discard eviction during shutdown or disconnect.
|
|
7
|
+
* @param evict Cache eviction work to run after a confirmed successful commit.
|
|
8
|
+
* @returns A cancellation function that restores the response writers and clears the fallback timer.
|
|
9
|
+
*/
|
|
10
|
+
export function installDeferredEviction(response, request, evict) {
|
|
11
|
+
const originalSend = response.send;
|
|
12
|
+
const originalSimpleJsonSend = Reflect.get(response, 'sendSimpleJson');
|
|
13
|
+
const signal = request.signal;
|
|
14
|
+
let restored = false;
|
|
15
|
+
let completed = false;
|
|
16
|
+
let abortListenerInstalled = false;
|
|
17
|
+
let responseWriteStarted = false;
|
|
18
|
+
const requestAborted = () => signal?.aborted === true || request.isAborted?.() === true;
|
|
19
|
+
const runEviction = () => {
|
|
20
|
+
if (completed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
completed = true;
|
|
24
|
+
void evict().catch(() => {});
|
|
25
|
+
};
|
|
26
|
+
const restore = () => {
|
|
27
|
+
if (restored) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (abortListenerInstalled) {
|
|
31
|
+
signal?.removeEventListener('abort', cancel);
|
|
32
|
+
}
|
|
33
|
+
clearTimeout(fallbackTimer);
|
|
34
|
+
response.send = originalSend;
|
|
35
|
+
if (typeof originalSimpleJsonSend === 'function') {
|
|
36
|
+
Reflect.set(response, 'sendSimpleJson', originalSimpleJsonSend);
|
|
37
|
+
}
|
|
38
|
+
restored = true;
|
|
39
|
+
};
|
|
40
|
+
const cancel = () => {
|
|
41
|
+
completed = true;
|
|
42
|
+
restore();
|
|
43
|
+
};
|
|
44
|
+
const runResponseWrite = async write => {
|
|
45
|
+
responseWriteStarted = true;
|
|
46
|
+
try {
|
|
47
|
+
await write();
|
|
48
|
+
if (requestAborted()) {
|
|
49
|
+
cancel();
|
|
50
|
+
} else if (response.committed) {
|
|
51
|
+
runEviction();
|
|
52
|
+
} else {
|
|
53
|
+
cancel();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
cancel();
|
|
57
|
+
throw error;
|
|
58
|
+
} finally {
|
|
59
|
+
restore();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
response.send = body => runResponseWrite(() => originalSend.call(response, body));
|
|
63
|
+
if (typeof originalSimpleJsonSend === 'function') {
|
|
64
|
+
Reflect.set(response, 'sendSimpleJson', body => {
|
|
65
|
+
return runResponseWrite(() => Reflect.apply(originalSimpleJsonSend, response, [body]));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const fallbackTimer = setTimeout(() => {
|
|
69
|
+
if (!responseWriteStarted && !requestAborted() && response.committed) {
|
|
70
|
+
runEviction();
|
|
71
|
+
}
|
|
72
|
+
restore();
|
|
73
|
+
}, EVICTION_FALLBACK_TIMEOUT_MS);
|
|
74
|
+
fallbackTimer.unref?.();
|
|
75
|
+
if (signal) {
|
|
76
|
+
signal.addEventListener('abort', cancel, {
|
|
77
|
+
once: true
|
|
78
|
+
});
|
|
79
|
+
abortListenerInstalled = true;
|
|
80
|
+
}
|
|
81
|
+
if (requestAborted()) {
|
|
82
|
+
cancel();
|
|
83
|
+
}
|
|
84
|
+
return cancel;
|
|
85
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { CacheEvict, CacheKey,
|
|
1
|
+
export { CacheEvict, CacheKey, CacheTTL, cacheRouteMetadataKey, getCacheEvictMetadata, getCacheKeyMetadata, getCacheTtlMetadata, } from './decorators.js';
|
|
2
2
|
export { CacheInterceptor } from './interceptor.js';
|
|
3
3
|
export { CacheModule } from './module.js';
|
|
4
4
|
export { CacheService } from './service.js';
|
|
5
|
+
export type { CacheManagerPlatformStatusSnapshot, CacheManagerStatusAdapterInput, CacheManagerStoreKind, CacheManagerStoreOwnershipMode, } from './status.js';
|
|
5
6
|
export { createCacheManagerPlatformDiagnosticIssues, createCacheManagerPlatformStatusSnapshot, } from './status.js';
|
|
6
7
|
export { MemoryStore } from './stores/memory-store.js';
|
|
7
8
|
export { RedisStore, type RedisStoreOptions } from './stores/redis-store.js';
|
|
8
9
|
export { CACHE_OPTIONS, CACHE_STORE } from './tokens.js';
|
|
9
|
-
export type { CacheEvictDecoratorValue, CacheEvictFactory, CacheKeyDecoratorValue, CacheKeyFactory, CacheKeyStrategy, CacheModuleOptions, CacheStore, NormalizedCacheModuleOptions, PrincipalScopeResolver, RedisCacheOptions, RedisCompatibleClient, } from './types.js';
|
|
10
|
-
export type { CacheManagerPlatformStatusSnapshot, CacheManagerStatusAdapterInput, CacheManagerStoreKind, CacheManagerStoreOwnershipMode, } from './status.js';
|
|
10
|
+
export type { CacheAsyncModuleOptions, CacheEvictDecoratorValue, CacheEvictFactory, CacheKeyDecoratorValue, CacheKeyFactory, CacheKeyStrategy, CacheModuleOptions, CacheObservation, CacheObserver, CacheStore, CacheTtlJitterMode, CacheTtlJitterOptions, NormalizedCacheModuleOptions, NormalizedCacheTtlJitterOptions, PrincipalScopeResolver, RedisCacheOptions, RedisCompatibleClient, } from './types.js';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|