@fluojs/cache-manager
Advanced tools
@@ -15,2 +15,3 @@ import { type CallHandler, type Interceptor, type InterceptorContext } from '@fluojs/http'; | ||
| private resolveEvictKeys; | ||
| private shouldCacheRoute; | ||
| private shouldCacheValue; | ||
@@ -17,0 +18,0 @@ private safeGet; |
@@ -1,1 +0,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"} | ||
| {"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;AAuL3J;;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"} |
+17
-2
@@ -84,2 +84,5 @@ let _initClass; | ||
| } | ||
| function isSuccessStatusCode(statusCode) { | ||
| return statusCode >= 200 && statusCode < 300; | ||
| } | ||
| async function resolveCacheKeyValue(metadata, context, strategy, resolver) { | ||
@@ -152,3 +155,4 @@ if (!metadata) { | ||
| const ttl = normalizeTtl(metadataBag ? getCacheTtlMetadata(metadataBag) : undefined, this.options.ttl); | ||
| if (ttl !== undefined) { | ||
| const cacheableRoute = this.shouldCacheRoute(context); | ||
| if (ttl !== undefined && cacheableRoute) { | ||
| const cached = await this.safeGet(key); | ||
@@ -160,3 +164,3 @@ if (cached !== undefined) { | ||
| const value = await next.handle(); | ||
| if (ttl !== undefined && this.shouldCacheValue(context, value)) { | ||
| if (ttl !== undefined && cacheableRoute && this.shouldCacheValue(context, value)) { | ||
| await this.safeSet(key, value, ttl); | ||
@@ -189,6 +193,17 @@ } | ||
| } | ||
| shouldCacheRoute(context) { | ||
| const route = context.handler.route; | ||
| if (route.redirect) { | ||
| return false; | ||
| } | ||
| return typeof route.successStatus !== 'number' || isSuccessStatusCode(route.successStatus); | ||
| } | ||
| shouldCacheValue(context, value) { | ||
| const statusCode = context.requestContext.response.statusCode; | ||
| if (value === undefined) { | ||
| return false; | ||
| } | ||
| if (typeof statusCode === 'number' && !isSuccessStatusCode(statusCode)) { | ||
| return false; | ||
| } | ||
| if (value instanceof SseResponse) { | ||
@@ -195,0 +210,0 @@ return false; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwCD;;GAEG;AACH,qBAAa,UAAW,YAAW,UAAU;IAMzC,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGhB,MAAM,EAAE,qBAAqB,EAC9C,OAAO,GAAE,iBAAsB;IAMjC,OAAO,CAAC,UAAU;IAIZ,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAiBrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5E,OAAO,CAAC,aAAa;IAMf,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAoC7B"} | ||
| {"version":3,"file":"redis-store.d.ts","sourceRoot":"","sources":["../../src/stores/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgDD;;GAEG;AACH,qBAAa,UAAW,YAAW,UAAU;IAMzC,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGhB,MAAM,EAAE,qBAAqB,EAC9C,OAAO,GAAE,iBAAsB;IAMjC,OAAO,CAAC,UAAU;IAIZ,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAqBrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5E,OAAO,CAAC,aAAa;IAMf,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAoC7B"} |
@@ -31,2 +31,8 @@ /** | ||
| } | ||
| function normalizePositiveTtlMilliseconds(ttlSeconds) { | ||
| return Math.max(1, Math.ceil(ttlSeconds * 1000)); | ||
| } | ||
| function normalizeRedisExpirySeconds(ttlSeconds) { | ||
| return Math.max(1, Math.ceil(ttlSeconds)); | ||
| } | ||
@@ -58,2 +64,5 @@ /** | ||
| } | ||
| if (decoded.expiresAt !== undefined && decoded.expiresAt <= Date.now()) { | ||
| return undefined; | ||
| } | ||
| return decoded.value; | ||
@@ -68,5 +77,5 @@ } | ||
| if (ttlSeconds > 0) { | ||
| const ttlMilliseconds = Math.max(1, Math.floor(ttlSeconds * 1000)); | ||
| const ttlMilliseconds = normalizePositiveTtlMilliseconds(ttlSeconds); | ||
| entry.expiresAt = now + ttlMilliseconds; | ||
| const ttlSecondsRounded = Math.max(1, Math.ceil(ttlMilliseconds / 1000)); | ||
| const ttlSecondsRounded = normalizeRedisExpirySeconds(ttlSeconds); | ||
| await this.client.set(redisKey, JSON.stringify(entry), 'EX', ttlSecondsRounded); | ||
@@ -73,0 +82,0 @@ this.trackOwnedKey(redisKey); |
+5
-5
@@ -13,3 +13,3 @@ { | ||
| ], | ||
| "version": "1.0.0-beta.7", | ||
| "version": "1.0.0-beta.8", | ||
| "private": false, | ||
@@ -41,10 +41,10 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@fluojs/core": "^1.0.0-beta.4", | ||
| "@fluojs/di": "^1.0.0-beta.6", | ||
| "@fluojs/core": "^1.0.0-beta.5", | ||
| "@fluojs/di": "^1.0.0-beta.7", | ||
| "@fluojs/http": "^1.0.0-beta.10", | ||
| "@fluojs/runtime": "^1.0.0-beta.11" | ||
| "@fluojs/runtime": "^1.0.0-beta.12" | ||
| }, | ||
| "peerDependencies": { | ||
| "ioredis": "^5.0.0", | ||
| "@fluojs/redis": "^1.0.0-beta.3" | ||
| "@fluojs/redis": "^1.0.0-beta.4" | ||
| }, | ||
@@ -51,0 +51,0 @@ "peerDependenciesMeta": { |
+6
-1
@@ -82,4 +82,5 @@ # @fluojs/cache-manager | ||
| @Inject(CacheService) | ||
| class UserService { | ||
| constructor(@Inject(CacheService) private readonly cache: CacheService) {} | ||
| constructor(private readonly cache: CacheService) {} | ||
@@ -125,2 +126,4 @@ async getProfile(userId: string) { | ||
| 양수 Redis TTL 값은 초 단위로 받으며 소수도 허용됩니다. Redis `EX`는 정수 초를 사용하므로 Redis 만료 시간은 다음 정수 초로 올림하지만, fluo는 저장된 엔트리 안에 밀리초 정밀도의 만료 timestamp도 기록하고 해당 timestamp에 도달하면 값을 만료된 것으로 처리합니다. Redis 만료를 의도적으로 사용하지 않으려면 `ttl: 0`을 사용하세요. | ||
| Redis reset 소유권은 기본값이 `fluo:cache:`인 `keyPrefix`로 제한됩니다. Redis 기반 저장소에서 `CacheService.reset()`은 해당 prefix 아래의 키만 삭제하므로, cache prefix 밖의 애플리케이션 소유 Redis 데이터는 유지됩니다. 의도적으로 빈 `keyPrefix`를 설정하면 reset은 `*`를 scan하지 않고 현재 `RedisStore` 인스턴스가 쓴 키로만 제한됩니다. 재시작 이후나 여러 프로세스에 걸친 캐시 엔트리까지 reset해야 한다면 비어 있지 않은 애플리케이션 전용 prefix를 사용하세요. | ||
@@ -143,2 +146,4 @@ | ||
| HTTP 인터셉터는 나중에 재사용할 수 있는 값이 있는 성공한, 아직 commit되지 않은 GET 핸들러 결과만 캐싱합니다. `undefined`, `SseResponse` 스트림, 이미 commit된 응답, 그리고 status code가 `2xx` 범위를 벗어난 응답은 건너뛰므로 redirect와 error 응답은 cache hit로 저장되지 않습니다. | ||
| ### 캐시 소유권과 reset 범위 | ||
@@ -145,0 +150,0 @@ |
+6
-1
@@ -82,4 +82,5 @@ # @fluojs/cache-manager | ||
| @Inject(CacheService) | ||
| class UserService { | ||
| constructor(@Inject(CacheService) private readonly cache: CacheService) {} | ||
| constructor(private readonly cache: CacheService) {} | ||
@@ -125,2 +126,4 @@ async getProfile(userId: string) { | ||
| 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. | ||
| Redis reset ownership is scoped by `keyPrefix`, which defaults to `fluo:cache:`. `CacheService.reset()` deletes only keys under that prefix for Redis-backed stores, so application-owned Redis data outside the cache prefix is preserved. 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. | ||
@@ -143,2 +146,4 @@ | ||
| The HTTP interceptor caches only successful, uncommitted GET handler results with a value that can be replayed later. It skips `undefined`, `SseResponse` streams, already committed responses, and responses whose status code is outside the `2xx` range, so redirects and error responses are not stored as cache hits. | ||
| ### Cache Ownership and Reset Scope | ||
@@ -145,0 +150,0 @@ |
89542
2.79%1421
1.79%225
2.27%Updated
Updated