@fluojs/cache-manager 1.0.0-beta.7 → 1.0.0-beta.8

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
@@ -79,8 +79,9 @@ class AppModule {}
79
79
  import { Inject } from '@fluojs/core';
80
80
  import { CacheService } from '@fluojs/cache-manager';
81
81
 
82
+ @Inject(CacheService)
82
83
  class UserService {
83
- constructor(@Inject(CacheService) private readonly cache: CacheService) {}
84
+ constructor(private readonly cache: CacheService) {}
84
85
 
85
86
  async getProfile(userId: string) {
86
87
  return this.cache.remember(`user:${userId}`, async () => {
@@ -121,6 +122,8 @@ CacheModule.forRoot({
121
122
 
122
123
  내장 `RedisStore`는 엔트리를 `JSON.stringify(...)`로 저장합니다. 따라서 캐시 값은 JSON 호환 형태여야 합니다. 일반 객체, 배열, 문자열, 숫자, 불리언, `null`은 안정적으로 round-trip 되지만, `Date`는 JSON 결과(예: ISO 문자열)로 돌아오고, 함수/`undefined`/`symbol`은 유지되지 않으며, `bigint`나 순환 그래프처럼 직렬화 불가능한 값은 캐싱 전에 정규화해야 합니다.
123
124
 
125
+ 양수 Redis TTL 값은 초 단위로 받으며 소수도 허용됩니다. Redis `EX`는 정수 초를 사용하므로 Redis 만료 시간은 다음 정수 초로 올림하지만, fluo는 저장된 엔트리 안에 밀리초 정밀도의 만료 timestamp도 기록하고 해당 timestamp에 도달하면 값을 만료된 것으로 처리합니다. Redis 만료를 의도적으로 사용하지 않으려면 `ttl: 0`을 사용하세요.
126
+
124
127
  Redis reset 소유권은 기본값이 `fluo:cache:`인 `keyPrefix`로 제한됩니다. Redis 기반 저장소에서 `CacheService.reset()`은 해당 prefix 아래의 키만 삭제하므로, cache prefix 밖의 애플리케이션 소유 Redis 데이터는 유지됩니다. 의도적으로 빈 `keyPrefix`를 설정하면 reset은 `*`를 scan하지 않고 현재 `RedisStore` 인스턴스가 쓴 키로만 제한됩니다. 재시작 이후나 여러 프로세스에 걸친 캐시 엔트리까지 reset해야 한다면 비어 있지 않은 애플리케이션 전용 prefix를 사용하세요.
125
128
 
126
129
  ### 쿼리 매개변수 기반 캐싱
@@ -138,6 +141,8 @@ CacheModule.forRoot({
138
141
 
139
142
  완전히 다른 키 전략이 필요하다면 `httpKeyStrategy`에 함수를 전달하거나, literal key 또는 key factory를 받는 `@CacheKey(...)`를 사용하세요.
140
143
 
144
+ HTTP 인터셉터는 나중에 재사용할 수 있는 값이 있는 성공한, 아직 commit되지 않은 GET 핸들러 결과만 캐싱합니다. `undefined`, `SseResponse` 스트림, 이미 commit된 응답, 그리고 status code가 `2xx` 범위를 벗어난 응답은 건너뛰므로 redirect와 error 응답은 cache hit로 저장되지 않습니다.
145
+
141
146
  ### 캐시 소유권과 reset 범위
142
147
 
143
148
  `CacheService.reset()`은 관련 없는 애플리케이션 상태가 아니라 설정된 store가 소유한 엔트리만 삭제합니다. 또한 진행 중인 `remember(...)` bookkeeping을 제거하므로 reset 전에 시작된 loader가 reset 완료 후 stale 엔트리를 다시 채우지 못합니다. 내장 메모리 저장소에서는 해당 store 인스턴스가 보유한 in-process 엔트리를 의미합니다. Redis에서는 설정된 `keyPrefix` namespace가 소유권 경계입니다. 공유 Redis 배포에서는 기본 `fluo:cache:`를 유지하거나 `myapp:cache:`처럼 전용 prefix를 선택하세요.
package/README.md CHANGED
@@ -79,8 +79,9 @@ Inject `CacheService` to manage cache programmatically.
79
79
  import { Inject } from '@fluojs/core';
80
80
  import { CacheService } from '@fluojs/cache-manager';
81
81
 
82
+ @Inject(CacheService)
82
83
  class UserService {
83
- constructor(@Inject(CacheService) private readonly cache: CacheService) {}
84
+ constructor(private readonly cache: CacheService) {}
84
85
 
85
86
  async getProfile(userId: string) {
86
87
  return this.cache.remember(`user:${userId}`, async () => {
@@ -121,6 +122,8 @@ CacheModule.forRoot({
121
122
 
122
123
  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.
123
124
 
125
+ 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.
126
+
124
127
  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.
125
128
 
126
129
  ### Query-Sensitive Caching
@@ -138,6 +141,8 @@ CacheModule.forRoot({
138
141
 
139
142
  For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory.
140
143
 
144
+ 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.
145
+
141
146
  ### Cache Ownership and Reset Scope
142
147
 
143
148
  `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.
@@ -12,6 +12,7 @@ export declare class CacheInterceptor implements Interceptor {
12
12
  private interceptGet;
13
13
  private evictAfterWrite;
14
14
  private resolveEvictKeys;
15
+ private shouldCacheRoute;
15
16
  private shouldCacheValue;
16
17
  private safeGet;
17
18
  private safeSet;
@@ -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;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"}
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;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"}
@@ -81,6 +81,9 @@ function normalizeEvictKeys(value) {
81
81
  }
82
82
  return [];
83
83
  }
84
+ function isSuccessStatusCode(statusCode) {
85
+ return statusCode >= 200 && statusCode < 300;
86
+ }
84
87
  async function resolveCacheKeyValue(metadata, context, strategy, resolver) {
85
88
  if (!metadata) {
86
89
  return defaultCacheKey(context, strategy, resolver);
@@ -148,14 +151,15 @@ class CacheInterceptor {
148
151
  const keyMetadata = metadataBag ? getCacheKeyMetadata(metadataBag) : undefined;
149
152
  const key = await resolveCacheKeyValue(keyMetadata, context, this.options.httpKeyStrategy, this.options.principalScopeResolver);
150
153
  const ttl = normalizeTtl(metadataBag ? getCacheTtlMetadata(metadataBag) : undefined, this.options.ttl);
151
- if (ttl !== undefined) {
154
+ const cacheableRoute = this.shouldCacheRoute(context);
155
+ if (ttl !== undefined && cacheableRoute) {
152
156
  const cached = await this.safeGet(key);
153
157
  if (cached !== undefined) {
154
158
  return cached;
155
159
  }
156
160
  }
157
161
  const value = await next.handle();
158
- if (ttl !== undefined && this.shouldCacheValue(context, value)) {
162
+ if (ttl !== undefined && cacheableRoute && this.shouldCacheValue(context, value)) {
159
163
  await this.safeSet(key, value, ttl);
160
164
  }
161
165
  return value;
@@ -183,10 +187,21 @@ class CacheInterceptor {
183
187
  }
184
188
  return normalizeEvictKeys(metadata);
185
189
  }
190
+ shouldCacheRoute(context) {
191
+ const route = context.handler.route;
192
+ if (route.redirect) {
193
+ return false;
194
+ }
195
+ return typeof route.successStatus !== 'number' || isSuccessStatusCode(route.successStatus);
196
+ }
186
197
  shouldCacheValue(context, value) {
198
+ const statusCode = context.requestContext.response.statusCode;
187
199
  if (value === undefined) {
188
200
  return false;
189
201
  }
202
+ if (typeof statusCode === 'number' && !isSuccessStatusCode(statusCode)) {
203
+ return false;
204
+ }
190
205
  if (value instanceof SseResponse) {
191
206
  return false;
192
207
  }
@@ -1 +1 @@
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"}
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;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"}
@@ -28,6 +28,12 @@ function normalizeScanResponse(result) {
28
28
  keys
29
29
  };
30
30
  }
31
+ function normalizePositiveTtlMilliseconds(ttlSeconds) {
32
+ return Math.max(1, Math.ceil(ttlSeconds * 1000));
33
+ }
34
+ function normalizeRedisExpirySeconds(ttlSeconds) {
35
+ return Math.max(1, Math.ceil(ttlSeconds));
36
+ }
31
37
 
32
38
  /**
33
39
  * Cache store implementation backed by a Redis-compatible client.
@@ -54,6 +60,9 @@ export class RedisStore {
54
60
  if (!decoded) {
55
61
  return undefined;
56
62
  }
63
+ if (decoded.expiresAt !== undefined && decoded.expiresAt <= Date.now()) {
64
+ return undefined;
65
+ }
57
66
  return decoded.value;
58
67
  }
59
68
  async set(key, value, ttlSeconds = 0) {
@@ -63,9 +72,9 @@ export class RedisStore {
63
72
  value
64
73
  };
65
74
  if (ttlSeconds > 0) {
66
- const ttlMilliseconds = Math.max(1, Math.floor(ttlSeconds * 1000));
75
+ const ttlMilliseconds = normalizePositiveTtlMilliseconds(ttlSeconds);
67
76
  entry.expiresAt = now + ttlMilliseconds;
68
- const ttlSecondsRounded = Math.max(1, Math.ceil(ttlMilliseconds / 1000));
77
+ const ttlSecondsRounded = normalizeRedisExpirySeconds(ttlSeconds);
69
78
  await this.client.set(redisKey, JSON.stringify(entry), 'EX', ttlSecondsRounded);
70
79
  this.trackOwnedKey(redisKey);
71
80
  return;
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "memory-store",
11
11
  "decorator"
12
12
  ],
13
- "version": "1.0.0-beta.7",
13
+ "version": "1.0.0-beta.8",
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.0-beta.4",
41
- "@fluojs/di": "^1.0.0-beta.6",
40
+ "@fluojs/core": "^1.0.0-beta.5",
41
+ "@fluojs/di": "^1.0.0-beta.7",
42
42
  "@fluojs/http": "^1.0.0-beta.10",
43
- "@fluojs/runtime": "^1.0.0-beta.11"
43
+ "@fluojs/runtime": "^1.0.0-beta.12"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "ioredis": "^5.0.0",
47
- "@fluojs/redis": "^1.0.0-beta.3"
47
+ "@fluojs/redis": "^1.0.0-beta.4"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "@fluojs/redis": {