@fluojs/cache-manager 1.0.0-beta.4 → 1.0.0-beta.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 +19 -7
- package/README.md +19 -7
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +2 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +1 -1
- package/dist/service.d.ts +14 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +78 -16
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
4
|
|
|
5
|
-
메모리(Memory)
|
|
5
|
+
메모리(Memory), Redis, custom store 어댑터를 지원하는 fluo 애플리케이션용 범용 캐시 관리 패키지입니다. 데코레이터 기반의 HTTP 응답 캐싱과 프로그래밍 방식의 애플리케이션 레벨 캐시 API를 모두 제공합니다.
|
|
6
6
|
|
|
7
7
|
## 목차
|
|
8
8
|
|
|
@@ -127,7 +127,7 @@ Redis reset 소유권은 기본값이 `fluo:cache:`인 `keyPrefix`로 제한됩
|
|
|
127
127
|
|
|
128
128
|
내장 HTTP 캐시 키 전략은 경로 부분을 route template metadata가 아니라 구체적인 요청 경로(`requestContext.request.path`)에서 계산합니다. 따라서 같은 `@Get('/:id')` 핸들러를 타더라도 `/users/1`과 `/users/2` 같은 요청은 항상 서로 다른 캐시 키로 분리됩니다.
|
|
129
129
|
|
|
130
|
-
기본적으로
|
|
130
|
+
기본적으로 익명 요청은 구체적인 요청 경로만 사용하고 쿼리 매개변수를 무시합니다. 인증된 요청은 principal scope가 있으면 이를 suffix로 덧붙이며, `principalScopeResolver`로 이 suffix를 커스터마이즈할 수 있습니다. 검색 조건 등에 따라 다른 응답을 캐싱하려면 `httpKeyStrategy: 'route+query'`를 활성화하세요. 내장 전략에서는 `full`도 동일하게 query-aware 키를 만듭니다. query-aware 키는 매개변수 이름과 반복 값 모두를 정규화하므로 `/products?tag=a&tag=b`와 `/products?tag=b&tag=a`는 같은 캐시 엔트리를 공유합니다.
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
133
|
CacheModule.forRoot({
|
|
@@ -136,9 +136,11 @@ CacheModule.forRoot({
|
|
|
136
136
|
})
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요.
|
|
140
|
+
|
|
139
141
|
### 캐시 소유권과 reset 범위
|
|
140
142
|
|
|
141
|
-
`CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
|
|
143
|
+
`CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 진행 중인 `remember(...)` bookkeeping을 제거하므로 reset 전에 시작된 loader가 reset 완료 후 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
|
|
142
144
|
|
|
143
145
|
```typescript
|
|
144
146
|
CacheModule.forRoot({
|
|
@@ -149,6 +151,10 @@ CacheModule.forRoot({
|
|
|
149
151
|
|
|
150
152
|
Redis cache prefix를 cache가 아닌 데이터와 공유하지 마세요. `del(key)`은 이 패키지가 해석한 정확한 캐시 키를 삭제하고, `reset()`은 위에서 설명한 store 소유 캐시 namespace만 삭제합니다.
|
|
151
153
|
|
|
154
|
+
애플리케이션이 종료될 때 `CacheService`는 `close()` 또는 `dispose()`를 노출하는 custom store로 shutdown을 전달합니다. store가 socket, pool, timer 또는 기타 외부 리소스를 소유한다면 이 optional hook 중 하나를 사용하세요.
|
|
155
|
+
|
|
156
|
+
`CacheStore` 계약을 구현한 custom store는 `store` 옵션에 직접 전달할 수 있습니다. in-process LRU store, Redis 외 원격 캐시, 또는 cache operation을 관찰해야 하는 테스트 더블에 적합합니다.
|
|
157
|
+
|
|
152
158
|
### 수동 모듈 조합
|
|
153
159
|
|
|
154
160
|
일반적인 애플리케이션 설정과 커스텀 `defineModule(...)` 조합에서는 `CacheModule.forRoot(...)`를 사용합니다.
|
|
@@ -180,21 +186,26 @@ defineModule(ManualCacheModule, {
|
|
|
180
186
|
## 공개 API 개요
|
|
181
187
|
|
|
182
188
|
### 모듈
|
|
183
|
-
- `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis), 기본 TTL, 키
|
|
189
|
+
- `CacheModule.forRoot(options)`: 캐시 저장소(memory/redis/custom), 기본 TTL, 키 전략, `isGlobal`, `principalScopeResolver`, `redis.scanCount` 같은 Redis 옵션을 설정합니다.
|
|
184
190
|
애플리케이션 모듈에서 사용하는 기본 패키지 진입점입니다.
|
|
185
191
|
|
|
186
192
|
|
|
187
193
|
### 서비스
|
|
188
|
-
- `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`)을 위한 기본 API입니다.
|
|
194
|
+
- `CacheService`: 수동 캐시 작업(`get`, `set`, `del`, `remember`, `reset`, `close`)을 위한 기본 API입니다.
|
|
189
195
|
|
|
190
196
|
### 데코레이터
|
|
191
197
|
- `@CacheTTL(seconds)`: 특정 핸들러의 TTL을 설정합니다.
|
|
192
|
-
- `@CacheKey(key)`: 특정 핸들러의
|
|
193
|
-
- `@CacheEvict(key)`: 성공적인
|
|
198
|
+
- `@CacheKey(key)`: 특정 핸들러의 custom cache key 또는 key factory를 설정합니다.
|
|
199
|
+
- `@CacheEvict(key)`: 성공적인 non-GET 핸들러가 완료된 뒤 하나 이상의 cache key를 삭제합니다.
|
|
194
200
|
|
|
195
201
|
### 인터셉터
|
|
196
202
|
- `CacheInterceptor`: 자동 GET 응답 캐싱 및 삭제 로직을 처리합니다.
|
|
197
203
|
|
|
204
|
+
### 저장소와 status helper
|
|
205
|
+
- `MemoryStore`, `RedisStore`: 내장 store 구현입니다.
|
|
206
|
+
- `CACHE_OPTIONS`, `CACHE_STORE`: 패키지 내부와 custom composition에서 사용하는 DI 토큰입니다.
|
|
207
|
+
- `createCacheManagerPlatformStatusSnapshot(...)`, `createCacheManagerPlatformDiagnosticIssues(...)`: 플랫폼 status와 diagnostic helper입니다.
|
|
208
|
+
|
|
198
209
|
## 관련 패키지
|
|
199
210
|
|
|
200
211
|
- `@fluojs/redis`: Redis 저장소 사용 시 필요합니다.
|
|
@@ -205,3 +216,4 @@ defineModule(ManualCacheModule, {
|
|
|
205
216
|
- `packages/cache-manager/src/module.test.ts`: 모듈 설정 및 프로바이더 테스트.
|
|
206
217
|
- `packages/cache-manager/src/interceptor.test.ts`: HTTP 캐싱 및 삭제 테스트.
|
|
207
218
|
- `packages/cache-manager/src/service.ts`: 코어 `CacheService` 구현.
|
|
219
|
+
- `packages/cache-manager/src/status.test.ts`: status 및 diagnostic helper 테스트.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
4
|
|
|
5
|
-
General-purpose cache manager for fluo with pluggable memory and
|
|
5
|
+
General-purpose cache manager for fluo with pluggable memory, Redis, and custom store adapters. Provides both decorator-driven HTTP response caching and a standalone cache API for application-level caching.
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
@@ -127,7 +127,7 @@ Redis reset ownership is scoped by `keyPrefix`, which defaults to `fluo:cache:`.
|
|
|
127
127
|
|
|
128
128
|
Built-in HTTP cache key strategies derive their path segment from the concrete request path (`requestContext.request.path`), not the route template metadata. That means requests such as `/users/1` and `/users/2` always resolve to different cache keys even when they hit the same `@Get('/:id')` handler.
|
|
129
129
|
|
|
130
|
-
By default, the
|
|
130
|
+
By default, anonymous requests use the concrete request path and ignore query parameters. Authenticated requests append a principal scope when one is available; use `principalScopeResolver` to customize that suffix. Enable `httpKeyStrategy: 'route+query'` (or `full`, which is equivalent for the built-in strategy set) to cache different responses for different search parameters. Query-aware keys canonicalize both parameter names and repeated values, so `/products?tag=a&tag=b` and `/products?tag=b&tag=a` share one cache entry.
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
133
|
CacheModule.forRoot({
|
|
@@ -136,9 +136,11 @@ CacheModule.forRoot({
|
|
|
136
136
|
})
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory.
|
|
140
|
+
|
|
139
141
|
### Cache Ownership and Reset Scope
|
|
140
142
|
|
|
141
|
-
`CacheService.reset()` clears entries owned by the configured store, not unrelated application state. 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.
|
|
143
|
+
`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.
|
|
142
144
|
|
|
143
145
|
```typescript
|
|
144
146
|
CacheModule.forRoot({
|
|
@@ -149,6 +151,10 @@ CacheModule.forRoot({
|
|
|
149
151
|
|
|
150
152
|
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.
|
|
151
153
|
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
152
158
|
### Manual Module Composition
|
|
153
159
|
|
|
154
160
|
Use `CacheModule.forRoot(...)` for normal application setup, including custom `defineModule(...)` composition.
|
|
@@ -180,21 +186,26 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
|
|
|
180
186
|
## Public API Overview
|
|
181
187
|
|
|
182
188
|
### Modules
|
|
183
|
-
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis), default TTL, and
|
|
189
|
+
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis/custom), default TTL, key strategies, `isGlobal`, `principalScopeResolver`, and Redis options such as `redis.scanCount`.
|
|
184
190
|
This is the primary package entrypoint for application modules.
|
|
185
191
|
|
|
186
192
|
|
|
187
193
|
### Services
|
|
188
|
-
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`).
|
|
194
|
+
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`, `close`).
|
|
189
195
|
|
|
190
196
|
### Decorators
|
|
191
197
|
- `@CacheTTL(seconds)`: Sets the TTL for a specific handler.
|
|
192
|
-
- `@CacheKey(key)`: Sets a custom cache key for a specific handler.
|
|
193
|
-
- `@CacheEvict(key)`: Clears
|
|
198
|
+
- `@CacheKey(key)`: Sets a custom cache key or key factory for a specific handler.
|
|
199
|
+
- `@CacheEvict(key)`: Clears one or more cache keys after a successful non-GET handler completes.
|
|
194
200
|
|
|
195
201
|
### Interceptors
|
|
196
202
|
- `CacheInterceptor`: Handles automatic GET response caching and eviction logic.
|
|
197
203
|
|
|
204
|
+
### Stores and status helpers
|
|
205
|
+
- `MemoryStore` and `RedisStore`: Built-in store implementations.
|
|
206
|
+
- `CACHE_OPTIONS` and `CACHE_STORE`: DI tokens for package internals and custom composition.
|
|
207
|
+
- `createCacheManagerPlatformStatusSnapshot(...)` and `createCacheManagerPlatformDiagnosticIssues(...)`: Platform status and diagnostic helpers.
|
|
208
|
+
|
|
198
209
|
## Related Packages
|
|
199
210
|
|
|
200
211
|
- `@fluojs/redis`: Required for Redis storage.
|
|
@@ -205,3 +216,4 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
|
|
|
205
216
|
- `packages/cache-manager/src/module.test.ts`: Module configuration and provider tests.
|
|
206
217
|
- `packages/cache-manager/src/interceptor.test.ts`: HTTP caching and eviction tests.
|
|
207
218
|
- `packages/cache-manager/src/service.ts`: Core `CacheService` implementation.
|
|
219
|
+
- `packages/cache-manager/src/status.test.ts`: Status and diagnostic helper tests.
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEnF,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,eAAoC,CAAC;AAKvE,KAAK,mBAAmB,GAAG,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACxD,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AAmCjG;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,sBAAsB,GAAG,yBAAyB,CAI/E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,yBAAyB,CAItE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,wBAAwB,GAAG,yBAAyB,CAIrF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,mBAAmB,GAAG,sBAAsB,GAAG,SAAS,CAEhG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAQhF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,mBAAmB,GAAG,wBAAwB,GAAG,SAAS,CAIpG"}
|
package/dist/decorators.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { ensureMetadataSymbol } from '@fluojs/core/internal';
|
|
1
2
|
/** Shared controller metadata key used to store per-route cache metadata records. */
|
|
2
3
|
export const cacheRouteMetadataKey = Symbol.for('fluo.standard.route');
|
|
3
4
|
const cacheKeyMetadataKey = Symbol.for('fluo.cache.key');
|
|
4
5
|
const cacheTtlMetadataKey = Symbol.for('fluo.cache.ttl');
|
|
5
6
|
const cacheEvictMetadataKey = Symbol.for('fluo.cache.evict');
|
|
7
|
+
ensureMetadataSymbol();
|
|
6
8
|
function getMetadataBag(metadata) {
|
|
7
9
|
return metadata;
|
|
8
10
|
}
|
|
@@ -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;
|
|
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;AAmL3J;;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;YA0BZ,eAAe;YA2Bf,gBAAgB;IAY9B,OAAO,CAAC,gBAAgB;YAYV,OAAO;YAQP,OAAO;YAOP,OAAO;CAMtB"}
|
package/dist/interceptor.js
CHANGED
|
@@ -31,7 +31,7 @@ function normalizeCacheMethod(method) {
|
|
|
31
31
|
function buildSortedQueryString(query) {
|
|
32
32
|
const entries = Object.entries(query).filter(([, value]) => value !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => {
|
|
33
33
|
if (Array.isArray(value)) {
|
|
34
|
-
return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(
|
|
34
|
+
return [...value].map(v => String(v)).sort((a, b) => a.localeCompare(b)).map(v => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join('&');
|
|
35
35
|
}
|
|
36
36
|
return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
|
|
37
37
|
});
|
package/dist/service.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export declare class CacheService {
|
|
|
7
7
|
private readonly options;
|
|
8
8
|
private readonly inflight;
|
|
9
9
|
private readonly pendingLoads;
|
|
10
|
+
private readonly pendingInvalidations;
|
|
10
11
|
private readonly invalidatedInflight;
|
|
12
|
+
private closed;
|
|
11
13
|
private resetVersion;
|
|
12
14
|
private beginPendingLoad;
|
|
13
15
|
private endPendingLoad;
|
|
@@ -50,5 +52,17 @@ export declare class CacheService {
|
|
|
50
52
|
* @returns A promise that resolves after the store reset completes.
|
|
51
53
|
*/
|
|
52
54
|
reset(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Close the configured store when it exposes an optional teardown hook.
|
|
57
|
+
*
|
|
58
|
+
* @returns A promise that resolves after store teardown completes.
|
|
59
|
+
*/
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Runtime shutdown hook that releases resource-owning stores during application close.
|
|
63
|
+
*
|
|
64
|
+
* @returns A promise that resolves after store teardown completes.
|
|
65
|
+
*/
|
|
66
|
+
onModuleDestroy(): Promise<void>;
|
|
53
67
|
}
|
|
54
68
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/service.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/service.js
CHANGED
|
@@ -16,18 +16,29 @@ class CacheService {
|
|
|
16
16
|
}
|
|
17
17
|
inflight = new Map();
|
|
18
18
|
pendingLoads = new Map();
|
|
19
|
+
pendingInvalidations = new Map();
|
|
19
20
|
invalidatedInflight = new Set();
|
|
21
|
+
closed = false;
|
|
20
22
|
resetVersion = 0;
|
|
21
|
-
beginPendingLoad(key) {
|
|
22
|
-
|
|
23
|
+
beginPendingLoad(key, generation) {
|
|
24
|
+
const generations = this.pendingLoads.get(key) ?? new Map();
|
|
25
|
+
generations.set(generation, (generations.get(generation) ?? 0) + 1);
|
|
26
|
+
this.pendingLoads.set(key, generations);
|
|
23
27
|
}
|
|
24
|
-
endPendingLoad(key) {
|
|
25
|
-
const
|
|
28
|
+
endPendingLoad(key, generation) {
|
|
29
|
+
const generations = this.pendingLoads.get(key);
|
|
30
|
+
if (!generations) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const remaining = (generations.get(generation) ?? 0) - 1;
|
|
26
34
|
if (remaining > 0) {
|
|
27
|
-
|
|
35
|
+
generations.set(generation, remaining);
|
|
28
36
|
return;
|
|
29
37
|
}
|
|
30
|
-
|
|
38
|
+
generations.delete(generation);
|
|
39
|
+
if (generations.size === 0) {
|
|
40
|
+
this.pendingLoads.delete(key);
|
|
41
|
+
}
|
|
31
42
|
}
|
|
32
43
|
constructor(store, options) {
|
|
33
44
|
this.store = store;
|
|
@@ -69,34 +80,45 @@ class CacheService {
|
|
|
69
80
|
* @returns The cached or freshly loaded value.
|
|
70
81
|
*/
|
|
71
82
|
async remember(key, loader, ttlSeconds) {
|
|
72
|
-
this.
|
|
83
|
+
const resetVersion = this.resetVersion;
|
|
84
|
+
this.beginPendingLoad(key, resetVersion);
|
|
73
85
|
try {
|
|
74
|
-
const resetVersion = this.resetVersion;
|
|
75
86
|
const cached = await this.get(key);
|
|
76
87
|
if (cached !== undefined) {
|
|
77
88
|
return cached;
|
|
78
89
|
}
|
|
79
90
|
const existing = this.inflight.get(key);
|
|
80
91
|
if (existing) {
|
|
81
|
-
return existing;
|
|
92
|
+
return existing.promise;
|
|
82
93
|
}
|
|
94
|
+
const entry = {
|
|
95
|
+
generation: resetVersion,
|
|
96
|
+
invalidated: this.pendingInvalidations.get(key) === resetVersion,
|
|
97
|
+
promise: Promise.resolve(undefined)
|
|
98
|
+
};
|
|
83
99
|
const promise = loader().then(async value => {
|
|
84
|
-
if (
|
|
100
|
+
if (entry.invalidated || this.resetVersion !== resetVersion) {
|
|
85
101
|
return value;
|
|
86
102
|
}
|
|
87
103
|
await this.set(key, value, ttlSeconds);
|
|
88
|
-
if (
|
|
104
|
+
if (entry.invalidated || this.resetVersion !== resetVersion) {
|
|
89
105
|
await this.store.del(key);
|
|
90
106
|
}
|
|
91
107
|
return value;
|
|
92
108
|
}).finally(() => {
|
|
93
|
-
this.inflight.
|
|
94
|
-
|
|
109
|
+
if (this.inflight.get(key) === entry) {
|
|
110
|
+
this.inflight.delete(key);
|
|
111
|
+
this.invalidatedInflight.delete(key);
|
|
112
|
+
}
|
|
113
|
+
if (this.pendingInvalidations.get(key) === entry.generation) {
|
|
114
|
+
this.pendingInvalidations.delete(key);
|
|
115
|
+
}
|
|
95
116
|
});
|
|
96
|
-
|
|
117
|
+
entry.promise = promise;
|
|
118
|
+
this.inflight.set(key, entry);
|
|
97
119
|
return promise;
|
|
98
120
|
} finally {
|
|
99
|
-
this.endPendingLoad(key);
|
|
121
|
+
this.endPendingLoad(key, resetVersion);
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
124
|
|
|
@@ -107,7 +129,12 @@ class CacheService {
|
|
|
107
129
|
* @returns A promise that resolves after the entry is removed.
|
|
108
130
|
*/
|
|
109
131
|
async del(key) {
|
|
110
|
-
|
|
132
|
+
const entry = this.inflight.get(key);
|
|
133
|
+
if (entry) {
|
|
134
|
+
entry.invalidated = true;
|
|
135
|
+
this.invalidatedInflight.add(key);
|
|
136
|
+
} else if (this.pendingLoads.has(key)) {
|
|
137
|
+
this.pendingInvalidations.set(key, this.resetVersion);
|
|
111
138
|
this.invalidatedInflight.add(key);
|
|
112
139
|
}
|
|
113
140
|
await this.store.del(key);
|
|
@@ -120,9 +147,44 @@ class CacheService {
|
|
|
120
147
|
*/
|
|
121
148
|
async reset() {
|
|
122
149
|
this.resetVersion += 1;
|
|
150
|
+
this.inflight.clear();
|
|
151
|
+
this.pendingLoads.clear();
|
|
152
|
+
this.pendingInvalidations.clear();
|
|
123
153
|
this.invalidatedInflight.clear();
|
|
124
154
|
await this.store.reset();
|
|
125
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Close the configured store when it exposes an optional teardown hook.
|
|
159
|
+
*
|
|
160
|
+
* @returns A promise that resolves after store teardown completes.
|
|
161
|
+
*/
|
|
162
|
+
async close() {
|
|
163
|
+
if (this.closed) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.closed = true;
|
|
167
|
+
this.inflight.clear();
|
|
168
|
+
this.pendingLoads.clear();
|
|
169
|
+
this.pendingInvalidations.clear();
|
|
170
|
+
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
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Runtime shutdown hook that releases resource-owning stores during application close.
|
|
182
|
+
*
|
|
183
|
+
* @returns A promise that resolves after store teardown completes.
|
|
184
|
+
*/
|
|
185
|
+
onModuleDestroy() {
|
|
186
|
+
return this.close();
|
|
187
|
+
}
|
|
126
188
|
static {
|
|
127
189
|
_initClass();
|
|
128
190
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface CacheStore {
|
|
|
8
8
|
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
9
9
|
del(key: string): Promise<void>;
|
|
10
10
|
reset(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Optional lifecycle hook for stores that own sockets, pools, timers, or other resources.
|
|
13
|
+
*/
|
|
14
|
+
close?(): Awaitable<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Optional lifecycle hook accepted as an alias for resource-owning stores that expose dispose semantics.
|
|
17
|
+
*/
|
|
18
|
+
dispose?(): Awaitable<void>;
|
|
11
19
|
}
|
|
12
20
|
/**
|
|
13
21
|
* Redis client subset required by `RedisStore`.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEnC;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC5C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"memory-store",
|
|
11
11
|
"decorator"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.0-beta.
|
|
13
|
+
"version": "1.0.0-beta.6",
|
|
14
14
|
"private": false,
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@fluojs/core": "^1.0.0-beta.
|
|
41
|
-
"@fluojs/di": "^1.0.0-beta.
|
|
42
|
-
"@fluojs/http": "^1.0.0-beta.
|
|
43
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
40
|
+
"@fluojs/core": "^1.0.0-beta.4",
|
|
41
|
+
"@fluojs/di": "^1.0.0-beta.6",
|
|
42
|
+
"@fluojs/http": "^1.0.0-beta.10",
|
|
43
|
+
"@fluojs/runtime": "^1.0.0-beta.11"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"ioredis": "^5.0.0",
|