@fluojs/cache-manager 1.0.4 → 1.0.6

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 CHANGED
@@ -26,14 +26,16 @@
26
26
  npm install @fluojs/cache-manager
27
27
  ```
28
28
 
29
- root `@fluojs/cache-manager` import는 memory-only 설치에서도 안전합니다. Redis peer는 Redis 저장소 경로를 명시적으로 선택할 때만 필요합니다.
29
+ root `@fluojs/cache-manager` import는 memory-only 설치에서도 안전합니다. Redis client는 Redis 저장소 경로를 명시적으로 선택할 때만 필요합니다.
30
30
 
31
- Redis 기반 캐싱을 사용하는 경우:
31
+ Lifecycle이 관리되는 `@fluojs/redis` client로 Redis 기반 캐싱을 사용하는 경우:
32
32
 
33
33
  ```bash
34
34
  npm install @fluojs/cache-manager @fluojs/redis ioredis
35
35
  ```
36
36
 
37
+ 대신 애플리케이션이 소유하는 compatible client를 `redis.client`로 직접 전달할 수 있습니다. 이 경로에는 `@fluojs/redis`가 필요하지 않습니다. 필수 `get`, `set`, `del`, tuple-returning `scan` operation을 제공하는 client package를 설치하고, 해당 client는 애플리케이션 lifecycle에서 닫으세요.
38
+
37
39
  ## 사용 시점
38
40
 
39
41
  - 비용이 많이 드는 데이터베이스 쿼리나 외부 API 응답을 캐싱하고 싶을 때 사용합니다.
@@ -96,16 +98,30 @@ class UserService {
96
98
 
97
99
  ### Redis 저장소 사용
98
100
 
99
- Redis를 사용하려면 `@fluojs/redis`가 설정되어 있어야 하며, `store` 옵션을 `'redis'`로 설정합니다.
101
+ `store: 'redis'`를 설정한 지원되는 client 통합 경로 중 하나를 선택합니다.
102
+
103
+ 1. 기본 또는 named raw client를 `@fluojs/redis`로 등록하고 cache module이 DI를 통해 해석하도록 합니다.
104
+ 2. 애플리케이션이 소유하는 `RedisCompatibleClient`를 `redis.client`로 직접 전달합니다.
100
105
 
101
106
  memory-only 소비자는 `@fluojs/redis`나 `ioredis`를 설치하지 않아도 `@fluojs/cache-manager`를 계속 import할 수 있습니다. 이 optional peer들은 Redis 저장소 경로를 선택할 때만 해석됩니다.
102
107
 
103
108
  ```typescript
104
- CacheModule.forRoot({
105
- store: 'redis',
106
- ttl: 600,
107
- keyPrefix: 'myapp:cache:',
109
+ import { Module } from '@fluojs/core';
110
+ import { CacheModule } from '@fluojs/cache-manager';
111
+ import { RedisModule } from '@fluojs/redis';
112
+
113
+ @Module({
114
+ imports: [
115
+ RedisModule.forRoot({ name: 'cache', host: 'localhost', port: 6379 }),
116
+ CacheModule.forRoot({
117
+ store: 'redis',
118
+ ttl: 600,
119
+ keyPrefix: 'myapp:cache:',
120
+ redis: { clientName: 'cache' },
121
+ }),
122
+ ],
108
123
  })
124
+ class AppModule {}
109
125
  ```
110
126
 
111
127
  여러 Redis 클라이언트를 등록했다면 `redis.clientName`으로 사용할 `@fluojs/redis` 연결을 지정할 수 있습니다.
@@ -119,7 +135,26 @@ CacheModule.forRoot({
119
135
  })
120
136
  ```
121
137
 
122
- `redis.client`는 여전히 가장 높은 우선순위의 명시적 override입니다. DI 기반 선택을 완전히 우회해야 때만 사용하세요.
138
+ `redis.client`는 가장 높은 우선순위의 override이며 DI 기반 client 선택을 완전히 우회합니다. Export된 `RedisCompatibleClient` 계약을 만족하는 모든 client를 받을 수 있고, 이 경로에서는 `@fluojs/redis`를 load하거나 요구하지 않습니다. 직접 전달한 client의 connection startup과 shutdown은 애플리케이션이 소유합니다.
139
+
140
+ ```typescript
141
+ import Redis from 'ioredis';
142
+ import { Module } from '@fluojs/core';
143
+ import { CacheModule } from '@fluojs/cache-manager';
144
+
145
+ const cacheClient = new Redis({ host: 'localhost', port: 6379 });
146
+
147
+ @Module({
148
+ imports: [
149
+ CacheModule.forRoot({
150
+ store: 'redis',
151
+ keyPrefix: 'myapp:cache:',
152
+ redis: { client: cacheClient },
153
+ }),
154
+ ],
155
+ })
156
+ class AppModule {}
157
+ ```
123
158
 
124
159
  내장 `RedisStore`는 엔트리를 `JSON.stringify(...)`로 저장합니다. 따라서 캐시 값은 JSON 호환 형태여야 합니다. 일반 객체, 배열, 문자열, 숫자, 불리언, `null`은 안정적으로 round-trip 되지만, `Date`는 JSON 결과(예: ISO 문자열)로 돌아오고, 함수/`undefined`/`symbol`은 유지되지 않으며, `bigint`나 순환 그래프처럼 직렬화 불가능한 값은 캐싱 전에 정규화해야 합니다.
125
160
 
@@ -170,7 +205,7 @@ HTTP 인터셉터는 나중에 재사용할 수 있는 값이 있는 성공한,
170
205
 
171
206
  ### 캐시 소유권과 reset 범위
172
207
 
173
- `CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 진행 중인 `remember(...)` bookkeeping을 제거하므로 reset 전에 시작된 loader가 reset 완료 후 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
208
+ `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를 선택하세요.
174
209
 
175
210
  ```typescript
176
211
  CacheModule.forRoot({
@@ -181,7 +216,7 @@ CacheModule.forRoot({
181
216
 
182
217
  Redis cache prefix를 cache가 아닌 데이터와 공유하지 마세요. `del(key)`은 이 패키지가 해석한 정확한 캐시 키를 삭제하고, `reset()`은 위에서 설명한 store 소유 캐시 namespace만 삭제합니다.
183
218
 
184
- 애플리케이션이 종료될 때 `CacheService`는 `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
219
+ 애플리케이션이 종료될 때 `CacheService`는 새 store read/write를 중단하고 이미 시작된 store 작업을 기다린 뒤, `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
185
220
 
186
221
  `CacheStore` 계약을 구현한 custom store는 `store` 옵션에 직접 전달할 수 있습니다. in-process LRU store, Redis 외 원격 캐시, 또는 cache operation을 관찰해야 하는 테스트 더블에 적합합니다.
187
222
 
@@ -211,7 +246,24 @@ defineModule(ManualCacheModule, {
211
246
 
212
247
  ### 지연 삭제 시점
213
248
 
214
- `@CacheEvict(...)`가 붙은 non-GET 핸들러는 응답이 성공적으로 commit된 뒤에 캐시를 삭제합니다. `response.send(...)`가 reject되면 지연 eviction을 취소하여 실패한 commit이전 캐시된 읽기 결과를 삭제하지 않도록 합니다. 어댑터 경로가 `response.send(...)`를 호출하지 않더라도, 인터셉터는 bounded fallback timer를 통해 성공한 쓰기 이후 stale 엔트리가 무기한 남지 않도록 보장합니다. 또한 지연 eviction 실패는 인터셉터 내부에 containment되어 cache key factory나 cache store 삭제 오류가 응답 이후 unhandled promise rejection으로 노출되지 않습니다.
249
+ `@CacheEvict(...)`는 범용 service-method decorator가 아니라 HTTP route metadata입니다. `CacheInterceptor`가 non-GET controller handler를 감싸 실행될 때만 metadata를 소비합니다. Service method나 HTTP interceptor pipeline 밖의 호출에서는 `CacheService`를 주입하고 `del(...)`을 명시적으로 호출하세요.
250
+
251
+ ```typescript
252
+ import { CacheEvict, CacheInterceptor } from '@fluojs/cache-manager';
253
+ import { Controller, Post, UseInterceptors } from '@fluojs/http';
254
+
255
+ @Controller('/products')
256
+ @UseInterceptors(CacheInterceptor)
257
+ class ProductController {
258
+ @Post('/refresh')
259
+ @CacheEvict('/products')
260
+ refresh() {
261
+ return { refreshed: true };
262
+ }
263
+ }
264
+ ```
265
+
266
+ 이렇게 지원되는 HTTP 경로에서는 응답이 성공적으로 commit된 뒤에 캐시를 삭제합니다. `response.send(...)`가 reject되면 지연 eviction을 취소하여 실패한 commit이 이전 캐시된 읽기 결과를 삭제하지 않도록 합니다. 어댑터 경로가 `response.send(...)`를 호출하지 않더라도, 인터셉터는 bounded fallback timer를 통해 성공한 쓰기 이후 stale 엔트리가 무기한 남지 않도록 보장합니다. fallback timer는 response commit 경로가 호출되지 않았을 때만 eviction을 수행하며, `response.send(...)`가 여전히 pending 상태인 동안에는 send 경로가 eviction(성공 시) 또는 취소(실패 시)를 소유하므로 fallback timer가 pending send 하에서 eviction을 실행하지 않습니다. 또한 지연 eviction 실패는 인터셉터 내부에 containment되어 cache key factory나 cache store 삭제 오류가 응답 이후 unhandled promise rejection으로 노출되지 않습니다.
215
267
 
216
268
  ## 공개 API 개요
217
269
 
@@ -229,11 +281,11 @@ defineModule(ManualCacheModule, {
229
281
  ### 데코레이터
230
282
  - `@CacheTTL(seconds)`: 특정 핸들러의 TTL을 설정합니다.
231
283
  - `@CacheKey(key)`: 특정 핸들러의 custom cache key 또는 key factory를 설정합니다.
232
- - `@CacheEvict(key)`: 성공적인 non-GET 핸들러가 완료된 뒤 하나 이상의 cache key삭제합니다.
284
+ - `@CacheEvict(key)`: 성공적인 non-GET controller handler가 완료된 뒤 `CacheInterceptor`가 소비하는 HTTP route metadata저장합니다. 임의의 service call을 intercept하지 않습니다.
233
285
  - `cacheRouteMetadataKey`, `getCacheKeyMetadata(...)`, `getCacheTtlMetadata(...)`, `getCacheEvictMetadata(...)`: 캐시 데코레이터 metadata key를 다시 구현하지 않고 cache decorator metadata를 검사해야 하는 first-party interceptor 통합, 진단, 고급 tooling을 위해 공개된 low-level metadata helper입니다.
234
286
 
235
287
  ### 인터셉터
236
- - `CacheInterceptor`: 자동 GET 응답 캐싱 삭제 로직을 처리합니다.
288
+ - `CacheInterceptor`: 자동 GET 응답 캐싱을 처리하고 non-GET HTTP handler에서 `@CacheEvict(...)` metadata를 소비합니다.
237
289
 
238
290
  ### 저장소와 status helper
239
291
  - `MemoryStore`, `RedisStore`: 내장 store 구현입니다.
@@ -242,7 +294,7 @@ defineModule(ManualCacheModule, {
242
294
 
243
295
  ## 관련 패키지
244
296
 
245
- - `@fluojs/redis`: Redis 저장소 사용 필요합니다.
297
+ - `@fluojs/redis`: Lifecycle-managed Redis client를 위한 optional 통합입니다. `redis.client`로 애플리케이션 소유 `RedisCompatibleClient`를 직접 전달하면 필요하지 않습니다.
246
298
  - `@fluojs/http`: HTTP 인터셉터 및 데코레이터 사용 시 필요합니다.
247
299
 
248
300
  ## 예제 소스
package/README.md CHANGED
@@ -26,14 +26,16 @@ General-purpose cache manager for fluo with pluggable memory, Redis, and custom
26
26
  npm install @fluojs/cache-manager
27
27
  ```
28
28
 
29
- The root `@fluojs/cache-manager` import stays safe for memory-only installs. You only need Redis peers when you explicitly select the Redis-backed store path.
29
+ 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
30
 
31
- For Redis-backed caching:
31
+ For Redis-backed caching with a lifecycle-managed `@fluojs/redis` client:
32
32
 
33
33
  ```bash
34
34
  npm install @fluojs/cache-manager @fluojs/redis ioredis
35
35
  ```
36
36
 
37
+ You can instead pass an application-owned compatible client through `redis.client`. That path does not require `@fluojs/redis`; install whichever client package provides the required `get`, `set`, `del`, and tuple-returning `scan` operations, and close that client from the application lifecycle.
38
+
37
39
  ## When to Use
38
40
 
39
41
  - When you want to cache expensive database queries or external API responses.
@@ -96,16 +98,30 @@ class UserService {
96
98
 
97
99
  ### Redis Storage
98
100
 
99
- To use Redis, ensure `@fluojs/redis` is configured and set the store to `'redis'`.
101
+ Set `store: 'redis'`, then choose one of two supported client integration paths:
102
+
103
+ 1. Register a default or named raw client with `@fluojs/redis` and let the cache module resolve it through DI.
104
+ 2. Pass an application-owned `RedisCompatibleClient` directly through `redis.client`.
100
105
 
101
106
  Memory-only consumers can keep importing from `@fluojs/cache-manager` without installing `@fluojs/redis` or `ioredis`; those optional peers are resolved only when the Redis store path is selected.
102
107
 
103
108
  ```typescript
104
- CacheModule.forRoot({
105
- store: 'redis',
106
- ttl: 600,
107
- keyPrefix: 'myapp:cache:',
109
+ import { Module } from '@fluojs/core';
110
+ import { CacheModule } from '@fluojs/cache-manager';
111
+ import { RedisModule } from '@fluojs/redis';
112
+
113
+ @Module({
114
+ imports: [
115
+ RedisModule.forRoot({ name: 'cache', host: 'localhost', port: 6379 }),
116
+ CacheModule.forRoot({
117
+ store: 'redis',
118
+ ttl: 600,
119
+ keyPrefix: 'myapp:cache:',
120
+ redis: { clientName: 'cache' },
121
+ }),
122
+ ],
108
123
  })
124
+ class AppModule {}
109
125
  ```
110
126
 
111
127
  If you registered multiple Redis clients, set `redis.clientName` to target a named `@fluojs/redis` connection.
@@ -119,7 +135,26 @@ CacheModule.forRoot({
119
135
  })
120
136
  ```
121
137
 
122
- `redis.client` remains the highest-precedence override. Use it only when you need to bypass DI-based client selection entirely.
138
+ `redis.client` is the highest-precedence override and bypasses DI-based client selection entirely. It accepts any client that satisfies the exported `RedisCompatibleClient` contract; `@fluojs/redis` is not loaded or required on this path. The application owns connection startup and shutdown for a directly supplied client.
139
+
140
+ ```typescript
141
+ import Redis from 'ioredis';
142
+ import { Module } from '@fluojs/core';
143
+ import { CacheModule } from '@fluojs/cache-manager';
144
+
145
+ const cacheClient = new Redis({ host: 'localhost', port: 6379 });
146
+
147
+ @Module({
148
+ imports: [
149
+ CacheModule.forRoot({
150
+ store: 'redis',
151
+ keyPrefix: 'myapp:cache:',
152
+ redis: { client: cacheClient },
153
+ }),
154
+ ],
155
+ })
156
+ class AppModule {}
157
+ ```
123
158
 
124
159
  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.
125
160
 
@@ -170,7 +205,7 @@ The HTTP interceptor caches only successful, uncommitted GET handler results wit
170
205
 
171
206
  ### Cache Ownership and Reset Scope
172
207
 
173
- `CacheService.reset()` clears entries owned by the configured store, not unrelated application state. It also drops in-flight `remember(...)` bookkeeping 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.
208
+ `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.
174
209
 
175
210
  ```typescript
176
211
  CacheModule.forRoot({
@@ -181,7 +216,7 @@ CacheModule.forRoot({
181
216
 
182
217
  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.
183
218
 
184
- When the application closes, `CacheService` 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.
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.
185
220
 
186
221
  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.
187
222
 
@@ -211,7 +246,24 @@ The built-in memory store is designed for single-process, bounded caching:
211
246
 
212
247
  ### Deferred eviction timing
213
248
 
214
- For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred until the response successfully commits. If `response.send(...)` rejects, the deferred eviction is cancelled so a failed commit does not drop the previous cached read result. If an adapter path never calls `response.send(...)`, the interceptor still runs a bounded fallback timer so successful writes do not leave stale entries behind indefinitely. Deferred eviction failures stay contained inside the interceptor, so cache-key factories or cache-store deletes cannot surface as post-response unhandled promise rejections.
249
+ `@CacheEvict(...)` is HTTP route metadata, not a general service-method decorator. `CacheInterceptor` consumes it only when that interceptor runs around a non-GET controller handler. For service methods and other calls outside the HTTP interceptor pipeline, inject `CacheService` and call `del(...)` explicitly.
250
+
251
+ ```typescript
252
+ import { CacheEvict, CacheInterceptor } from '@fluojs/cache-manager';
253
+ import { Controller, Post, UseInterceptors } from '@fluojs/http';
254
+
255
+ @Controller('/products')
256
+ @UseInterceptors(CacheInterceptor)
257
+ class ProductController {
258
+ @Post('/refresh')
259
+ @CacheEvict('/products')
260
+ refresh() {
261
+ return { refreshed: true };
262
+ }
263
+ }
264
+ ```
265
+
266
+ On that supported HTTP path, eviction is deferred until the response successfully commits. If `response.send(...)` rejects, the deferred eviction is cancelled so a failed commit does not drop the previous cached read result. If an adapter path never calls `response.send(...)`, the interceptor still runs a bounded fallback timer so successful writes do not leave stale entries behind indefinitely. The fallback timer evicts only when no response commit path was invoked; while `response.send(...)` is still pending, the send path owns eviction (on success) or cancellation (on failure), so the fallback timer does not evict under a pending send. Deferred eviction failures stay contained inside the interceptor, so cache-key factories or cache-store deletes cannot surface as post-response unhandled promise rejections.
215
267
 
216
268
  ## Public API Overview
217
269
 
@@ -229,11 +281,11 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
229
281
  ### Decorators
230
282
  - `@CacheTTL(seconds)`: Sets the TTL for a specific handler.
231
283
  - `@CacheKey(key)`: Sets a custom cache key or key factory for a specific handler.
232
- - `@CacheEvict(key)`: Clears one or more cache keys after a successful non-GET handler completes.
284
+ - `@CacheEvict(key)`: Stores HTTP route metadata that `CacheInterceptor` consumes after a successful non-GET controller handler completes; it does not intercept arbitrary service calls.
233
285
  - `cacheRouteMetadataKey`, `getCacheKeyMetadata(...)`, `getCacheTtlMetadata(...)`, and `getCacheEvictMetadata(...)`: Low-level metadata helpers exported for first-party interceptor integration, diagnostics, and advanced tooling that needs to inspect cache decorator metadata without reimplementing the metadata keys.
234
286
 
235
287
  ### Interceptors
236
- - `CacheInterceptor`: Handles automatic GET response caching and eviction logic.
288
+ - `CacheInterceptor`: Handles automatic GET response caching and consumes `@CacheEvict(...)` metadata for non-GET HTTP handlers.
237
289
 
238
290
  ### Stores and status helpers
239
291
  - `MemoryStore` and `RedisStore`: Built-in store implementations.
@@ -242,7 +294,7 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
242
294
 
243
295
  ## Related Packages
244
296
 
245
- - `@fluojs/redis`: Required for Redis storage.
297
+ - `@fluojs/redis`: Optional lifecycle-managed Redis client integration. It is not required when `redis.client` supplies an application-owned `RedisCompatibleClient` directly.
246
298
  - `@fluojs/http`: Required for HTTP interceptors and decorators.
247
299
 
248
300
  ## Example Sources
@@ -1 +1 @@
1
- {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAe,MAAM,cAAc,CAAC;AAGxG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAsE,4BAA4B,EAA0B,MAAM,YAAY,CAAC;AA6L3J;;GAEG;AACH,qBACa,gBAAiB,YAAW,WAAW;IAEhD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B;IAGlD,SAAS,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;YAanE,YAAY;YA2BZ,eAAe;YA2Bf,gBAAgB;IAY9B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;YAkBV,OAAO;YAQP,OAAO;YAOP,OAAO;CAMtB"}
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAe,MAAM,cAAc,CAAC;AAGxG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAsE,4BAA4B,EAA0B,MAAM,YAAY,CAAC;AAsM3J;;GAEG;AACH,qBACa,gBAAiB,YAAW,WAAW;IAEhD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B;IAGlD,SAAS,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;YAanE,YAAY;YA2BZ,eAAe;YA2Bf,gBAAgB;IAY9B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;YAkBV,OAAO;YAQP,OAAO;YAOP,OAAO;CAMtB"}
@@ -98,6 +98,7 @@ function installDeferredEviction(response, evict) {
98
98
  const originalSend = response.send.bind(response);
99
99
  let restored = false;
100
100
  let completed = false;
101
+ let sendInvoked = false;
101
102
  const runEviction = () => {
102
103
  if (completed) {
103
104
  return;
@@ -114,10 +115,17 @@ function installDeferredEviction(response, evict) {
114
115
  restored = true;
115
116
  };
116
117
  const fallbackTimer = setTimeout(() => {
117
- runEviction();
118
+ // Run fallback eviction only when no response commit path was invoked.
119
+ // If response.send(...) is still pending or already completed, the send
120
+ // path owns eviction (on success) or cancellation (on failure), so the
121
+ // fallback timer must not evict under a pending send.
122
+ if (!sendInvoked) {
123
+ runEviction();
124
+ }
118
125
  restore();
119
126
  }, EVICTION_FALLBACK_TIMEOUT_MS);
120
127
  response.send = async body => {
128
+ sendInvoked = true;
121
129
  try {
122
130
  await originalSend(body);
123
131
  runEviction();
package/dist/service.d.ts CHANGED
@@ -11,6 +11,8 @@ export declare class CacheService {
11
11
  private readonly invalidatedInflight;
12
12
  private closed;
13
13
  private resetVersion;
14
+ private storeOperationTail;
15
+ private runStoreOperation;
14
16
  private beginPendingLoad;
15
17
  private endPendingLoad;
16
18
  constructor(store: CacheStore, options: NormalizedCacheModuleOptions);
@@ -58,6 +60,7 @@ export declare class CacheService {
58
60
  * @returns A promise that resolves after store teardown completes.
59
61
  */
60
62
  close(): Promise<void>;
63
+ private deleteFromStore;
61
64
  /**
62
65
  * Runtime shutdown hook that releases resource-owning stores during application close.
63
66
  *
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAQ3E;;GAEG;AACH,qBACa,YAAY;IAqCrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IArC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0C;IACvE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA6B;IAClE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAK;IAEzB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,cAAc;gBAsBH,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,4BAA4B;IAGxD;;;;;OAKG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAIrD;;;;;;;OAOG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjF;;;;;;;OAOG;IACG,QAAQ,CAAC,CAAC,GAAG,OAAO,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,CAAC,CAAC;IAsDb;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrC;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB5B;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAGjC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAQ3E;;GAEG;AACH,qBACa,YAAY;IAiDrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAjD1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0C;IACvE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA6B;IAClE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,kBAAkB,CAAoC;YAEhD,iBAAiB;IAW/B,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,cAAc;gBAsBH,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,4BAA4B;IAGxD;;;;;OAKG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAcrD;;;;;;;OAOG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjF;;;;;;;OAOG;IACG,QAAQ,CAAC,CAAC,GAAG,OAAO,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,CAAC,CAAC;IAkEb;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBrC;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAwBd,eAAe;IAU7B;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAGjC"}
package/dist/service.js CHANGED
@@ -20,6 +20,12 @@ class CacheService {
20
20
  invalidatedInflight = new Set();
21
21
  closed = false;
22
22
  resetVersion = 0;
23
+ storeOperationTail = Promise.resolve();
24
+ async runStoreOperation(operation) {
25
+ const result = this.storeOperationTail.then(operation, operation);
26
+ this.storeOperationTail = result.then(() => undefined, () => undefined);
27
+ return result;
28
+ }
23
29
  beginPendingLoad(key, generation) {
24
30
  const generations = this.pendingLoads.get(key) ?? new Map();
25
31
  generations.set(generation, (generations.get(generation) ?? 0) + 1);
@@ -52,7 +58,15 @@ class CacheService {
52
58
  * @returns The cached value, or `undefined` when the key is missing or expired.
53
59
  */
54
60
  get(key) {
55
- return Promise.resolve(this.store.get(key));
61
+ if (this.closed) {
62
+ return Promise.resolve(undefined);
63
+ }
64
+ return this.runStoreOperation(() => {
65
+ if (this.closed) {
66
+ return undefined;
67
+ }
68
+ return this.store.get(key);
69
+ });
56
70
  }
57
71
 
58
72
  /**
@@ -65,10 +79,15 @@ class CacheService {
65
79
  */
66
80
  async set(key, value, ttlSeconds) {
67
81
  const resolvedTtl = ttlSeconds ?? this.options.ttl;
68
- if (!Number.isFinite(resolvedTtl) || resolvedTtl < 0) {
82
+ if (this.closed || !Number.isFinite(resolvedTtl) || resolvedTtl < 0) {
69
83
  return;
70
84
  }
71
- await this.store.set(key, value, resolvedTtl);
85
+ await this.runStoreOperation(async () => {
86
+ if (this.closed) {
87
+ return;
88
+ }
89
+ await this.store.set(key, value, resolvedTtl);
90
+ });
72
91
  }
73
92
 
74
93
  /**
@@ -80,6 +99,9 @@ class CacheService {
80
99
  * @returns The cached or freshly loaded value.
81
100
  */
82
101
  async remember(key, loader, ttlSeconds) {
102
+ if (this.closed) {
103
+ return loader();
104
+ }
83
105
  const resetVersion = this.resetVersion;
84
106
  this.beginPendingLoad(key, resetVersion);
85
107
  try {
@@ -87,22 +109,28 @@ class CacheService {
87
109
  if (cached !== undefined) {
88
110
  return cached;
89
111
  }
112
+ if (this.closed || this.resetVersion !== resetVersion) {
113
+ return loader();
114
+ }
90
115
  const existing = this.inflight.get(key);
91
- if (existing) {
116
+ if (existing && existing.generation === resetVersion) {
92
117
  return existing.promise;
93
118
  }
119
+ if (existing) {
120
+ this.inflight.delete(key);
121
+ }
94
122
  const entry = {
95
123
  generation: resetVersion,
96
124
  invalidated: this.pendingInvalidations.get(key) === resetVersion,
97
125
  promise: Promise.resolve(undefined)
98
126
  };
99
127
  const promise = loader().then(async value => {
100
- if (entry.invalidated || this.resetVersion !== resetVersion) {
128
+ if (this.closed || entry.invalidated || this.resetVersion !== resetVersion) {
101
129
  return value;
102
130
  }
103
131
  await this.set(key, value, ttlSeconds);
104
- if (entry.invalidated || this.resetVersion !== resetVersion) {
105
- await this.store.del(key);
132
+ if (!this.closed && (entry.invalidated || this.resetVersion !== resetVersion)) {
133
+ await this.deleteFromStore(key);
106
134
  }
107
135
  return value;
108
136
  }).finally(() => {
@@ -129,6 +157,9 @@ class CacheService {
129
157
  * @returns A promise that resolves after the entry is removed.
130
158
  */
131
159
  async del(key) {
160
+ if (this.closed) {
161
+ return;
162
+ }
132
163
  const entry = this.inflight.get(key);
133
164
  if (entry) {
134
165
  entry.invalidated = true;
@@ -137,7 +168,7 @@ class CacheService {
137
168
  this.pendingInvalidations.set(key, this.resetVersion);
138
169
  this.invalidatedInflight.add(key);
139
170
  }
140
- await this.store.del(key);
171
+ await this.deleteFromStore(key);
141
172
  }
142
173
 
143
174
  /**
@@ -146,12 +177,20 @@ class CacheService {
146
177
  * @returns A promise that resolves after the store reset completes.
147
178
  */
148
179
  async reset() {
180
+ if (this.closed) {
181
+ return;
182
+ }
149
183
  this.resetVersion += 1;
150
184
  this.inflight.clear();
151
185
  this.pendingLoads.clear();
152
186
  this.pendingInvalidations.clear();
153
187
  this.invalidatedInflight.clear();
154
- await this.store.reset();
188
+ await this.runStoreOperation(async () => {
189
+ if (this.closed) {
190
+ return;
191
+ }
192
+ await this.store.reset();
193
+ });
155
194
  }
156
195
 
157
196
  /**
@@ -164,17 +203,28 @@ class CacheService {
164
203
  return;
165
204
  }
166
205
  this.closed = true;
206
+ this.resetVersion += 1;
167
207
  this.inflight.clear();
168
208
  this.pendingLoads.clear();
169
209
  this.pendingInvalidations.clear();
170
210
  this.invalidatedInflight.clear();
171
- if (this.store.close) {
172
- await this.store.close();
173
- return;
174
- }
175
- if (this.store.dispose) {
176
- await this.store.dispose();
177
- }
211
+ await this.runStoreOperation(async () => {
212
+ if (this.store.close) {
213
+ await this.store.close();
214
+ return;
215
+ }
216
+ if (this.store.dispose) {
217
+ await this.store.dispose();
218
+ }
219
+ });
220
+ }
221
+ async deleteFromStore(key) {
222
+ await this.runStoreOperation(async () => {
223
+ if (this.closed) {
224
+ return;
225
+ }
226
+ await this.store.del(key);
227
+ });
178
228
  }
179
229
 
180
230
  /**
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "memory-store",
11
11
  "decorator"
12
12
  ],
13
- "version": "1.0.4",
13
+ "version": "1.0.6",
14
14
  "private": false,
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -37,14 +37,14 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@fluojs/core": "^1.0.3",
41
- "@fluojs/di": "^1.1.0",
42
- "@fluojs/http": "^1.1.2",
43
- "@fluojs/runtime": "^1.1.8"
40
+ "@fluojs/core": "^1.1.0",
41
+ "@fluojs/di": "^2.0.0",
42
+ "@fluojs/http": "^2.0.1",
43
+ "@fluojs/runtime": "^2.0.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "ioredis": "^5.0.0",
47
- "@fluojs/redis": "^1.0.2"
47
+ "@fluojs/redis": "^1.1.0"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "@fluojs/redis": {