@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,QAAQ,EACR,qBAAqB,EACrB,QAAQ,EACR,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,0CAA0C,EAC1C,wCAAwC,GACzC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EACV,wBAAwB,EACxB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,kCAAkC,EAClC,8BAA8B,EAC9B,qBAAqB,EACrB,8BAA8B,GAC/B,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EACV,kCAAkC,EAClC,8BAA8B,EAC9B,qBAAqB,EACrB,8BAA8B,GAC/B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,0CAA0C,EAC1C,wCAAwC,GACzC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EACV,uBAAuB,EACvB,wBAAwB,EACxB,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACrB,4BAA4B,EAC5B,+BAA+B,EAC/B,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { CacheEvict, CacheKey, cacheRouteMetadataKey, CacheTTL, getCacheEvictMetadata, getCacheKeyMetadata, getCacheTtlMetadata } from './decorators.js';
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';
@@ -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;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"}
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;AAIxG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAsE,4BAA4B,EAA0B,MAAM,YAAY,CAAC;AA2I3J;;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;YAoCf,gBAAgB;IAY9B,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,gBAAgB;YAkBV,OAAO;YAQP,OAAO;YAOP,OAAO;CAMtB"}
@@ -5,16 +5,17 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
7
  import { Inject } from '@fluojs/core';
8
- import { getStandardMetadataBag } from '@fluojs/core/internal';
8
+ import { getRequestPipelineMetadataBag } from '@fluojs/core/request-pipeline';
9
9
  import { SseResponse } from '@fluojs/http';
10
10
  import { cacheRouteMetadataKey, getCacheEvictMetadata, getCacheKeyMetadata, getCacheTtlMetadata } from './decorators.js';
11
+ import { installDeferredEviction } from './deferred-eviction.js';
11
12
  import { CacheService } from './service.js';
12
13
  import { CACHE_OPTIONS } from './tokens.js';
13
14
  function isMetadataBag(value) {
14
15
  return typeof value === 'object' && value !== null;
15
16
  }
16
17
  function getMethodMetadataBag(controllerToken, methodName) {
17
- const classBag = getStandardMetadataBag(controllerToken);
18
+ const classBag = getRequestPipelineMetadataBag(controllerToken);
18
19
  if (!isMetadataBag(classBag)) {
19
20
  return undefined;
20
21
  }
@@ -85,7 +86,7 @@ function isSuccessStatusCode(statusCode) {
85
86
  return statusCode >= 200 && statusCode < 300;
86
87
  }
87
88
  async function resolveCacheKeyValue(metadata, context, strategy, resolver) {
88
- if (!metadata) {
89
+ if (metadata === undefined) {
89
90
  return defaultCacheKey(context, strategy, resolver);
90
91
  }
91
92
  if (typeof metadata === 'string') {
@@ -93,51 +94,6 @@ async function resolveCacheKeyValue(metadata, context, strategy, resolver) {
93
94
  }
94
95
  return metadata(context);
95
96
  }
96
- const EVICTION_FALLBACK_TIMEOUT_MS = 5_000;
97
- function installDeferredEviction(response, evict) {
98
- const originalSend = response.send.bind(response);
99
- let restored = false;
100
- let completed = false;
101
- let sendInvoked = false;
102
- const runEviction = () => {
103
- if (completed) {
104
- return;
105
- }
106
- completed = true;
107
- void evict().catch(() => {});
108
- };
109
- const restore = () => {
110
- if (restored) {
111
- return;
112
- }
113
- clearTimeout(fallbackTimer);
114
- response.send = originalSend;
115
- restored = true;
116
- };
117
- const fallbackTimer = setTimeout(() => {
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
- }
125
- restore();
126
- }, EVICTION_FALLBACK_TIMEOUT_MS);
127
- response.send = async body => {
128
- sendInvoked = true;
129
- try {
130
- await originalSend(body);
131
- runEviction();
132
- } catch (error) {
133
- completed = true;
134
- throw error;
135
- } finally {
136
- restore();
137
- }
138
- };
139
- return restore;
140
- }
141
97
 
142
98
  /**
143
99
  * Caches GET responses and evicts related entries after successful write operations.
@@ -190,10 +146,14 @@ class CacheInterceptor {
190
146
  }));
191
147
  };
192
148
  if (context.requestContext.response.committed) {
149
+ const request = context.requestContext.request;
150
+ if (request.signal?.aborted === true || request.isAborted?.() === true) {
151
+ return;
152
+ }
193
153
  await runEviction();
194
154
  return;
195
155
  }
196
- installDeferredEviction(context.requestContext.response, runEviction);
156
+ installDeferredEviction(context.requestContext.response, context.requestContext.request, runEviction);
197
157
  }
198
158
  async resolveEvictKeys(metadata, context, value) {
199
159
  if (typeof metadata === 'function') {
package/dist/module.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ModuleType } from '@fluojs/runtime';
2
- import type { CacheModuleOptions } from './types.js';
2
+ import type { CacheAsyncModuleOptions, CacheModuleOptions } from './types.js';
3
3
  /**
4
4
  * Runtime module entrypoint for cache-manager services and interceptor wiring.
5
5
  *
@@ -28,5 +28,30 @@ export declare class CacheModule {
28
28
  * ```
29
29
  */
30
30
  static forRoot(options?: CacheModuleOptions): ModuleType;
31
+ /**
32
+ * Register cache providers from an injected async factory.
33
+ *
34
+ * @remarks
35
+ * The factory runs once per module registration through the application container,
36
+ * and its resolved options are normalized with the same defaults as
37
+ * {@link CacheModule.forRoot}. Module visibility comes from the `global` option on this
38
+ * call because module metadata is fixed before the factory runs; a `global` value in the
39
+ * factory result is ignored. A rejected factory fails bootstrap.
40
+ *
41
+ * @param options Injected factory registration options.
42
+ * @returns A runtime module exporting `CacheService` and `CacheInterceptor`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * CacheModule.forRootAsync({
47
+ * inject: [ConfigService],
48
+ * useFactory: (config) => ({
49
+ * store: 'redis',
50
+ * ttl: config.cacheTtlSeconds,
51
+ * }),
52
+ * });
53
+ * ```
54
+ */
55
+ static forRootAsync(options: CacheAsyncModuleOptions): ModuleType;
31
56
  }
32
57
  //# sourceMappingURL=module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQhE,OAAO,KAAK,EAAE,kBAAkB,EAAuD,MAAM,YAAY,CAAC;AAqL1G;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU;CAU7D"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAShE,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EAGnB,MAAM,YAAY,CAAC;AAkLpB;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU;IAc5D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU;CAwBlE"}
package/dist/module.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { defineModule } from '@fluojs/runtime';
2
2
  import { RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
3
3
  import { CacheInterceptor } from './interceptor.js';
4
+ import { CacheService } from './service.js';
4
5
  import { MemoryStore } from './stores/memory-store.js';
5
6
  import { RedisStore } from './stores/redis-store.js';
6
- import { CacheService } from './service.js';
7
7
  import { CACHE_OPTIONS, CACHE_STORE } from './tokens.js';
8
+ import { normalizeCacheTtlJitterOptions } from './ttl-jitter.js';
8
9
  const DEFAULT_MEMORY_STORE_TTL_SECONDS = 300;
9
10
  const REDIS_PEER_MODULE_SPECIFIER = '@fluojs/redis';
10
11
  const loadOptionalModule = async specifier => import(specifier);
@@ -46,8 +47,10 @@ function normalizeCacheModuleOptions(options = {}) {
46
47
  redis: options.redis,
47
48
  store,
48
49
  ttl: options.ttl ?? (store === 'memory' ? DEFAULT_MEMORY_STORE_TTL_SECONDS : 0),
50
+ ttlJitter: normalizeCacheTtlJitterOptions(options.ttlJitter),
49
51
  httpKeyStrategy: options.httpKeyStrategy ?? 'route',
50
- principalScopeResolver: options.principalScopeResolver
52
+ principalScopeResolver: options.principalScopeResolver,
53
+ observer: options.observer
51
54
  };
52
55
  }
53
56
  function isNormalizedCacheModuleOptions(value) {
@@ -99,12 +102,8 @@ async function createStore(options, container) {
99
102
  }
100
103
  return new MemoryStore();
101
104
  }
102
- function createCacheModuleProviders(options = {}) {
103
- const normalized = normalizeCacheModuleOptions(options);
104
- return [{
105
- provide: CACHE_OPTIONS,
106
- useValue: normalized
107
- }, {
105
+ function createCacheRuntimeProviders(optionsProvider) {
106
+ return [optionsProvider, {
108
107
  inject: [CACHE_OPTIONS, RUNTIME_CONTAINER],
109
108
  provide: CACHE_STORE,
110
109
  useFactory: (...deps) => {
@@ -160,7 +159,54 @@ export class CacheModule {
160
159
  return defineModule(CacheRootModule, {
161
160
  exports: [CacheService, CacheInterceptor],
162
161
  global: normalized.global,
163
- providers: createCacheModuleProviders(options)
162
+ providers: createCacheRuntimeProviders({
163
+ provide: CACHE_OPTIONS,
164
+ useValue: normalized
165
+ })
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Register cache providers from an injected async factory.
171
+ *
172
+ * @remarks
173
+ * The factory runs once per module registration through the application container,
174
+ * and its resolved options are normalized with the same defaults as
175
+ * {@link CacheModule.forRoot}. Module visibility comes from the `global` option on this
176
+ * call because module metadata is fixed before the factory runs; a `global` value in the
177
+ * factory result is ignored. A rejected factory fails bootstrap.
178
+ *
179
+ * @param options Injected factory registration options.
180
+ * @returns A runtime module exporting `CacheService` and `CacheInterceptor`.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * CacheModule.forRootAsync({
185
+ * inject: [ConfigService],
186
+ * useFactory: (config) => ({
187
+ * store: 'redis',
188
+ * ttl: config.cacheTtlSeconds,
189
+ * }),
190
+ * });
191
+ * ```
192
+ */
193
+ static forRootAsync(options) {
194
+ class CacheRootAsyncModule extends CacheModule {}
195
+ return defineModule(CacheRootAsyncModule, {
196
+ exports: [CacheService, CacheInterceptor],
197
+ global: options.global ?? false,
198
+ providers: createCacheRuntimeProviders({
199
+ inject: options.inject,
200
+ provide: CACHE_OPTIONS,
201
+ scope: 'singleton',
202
+ useFactory: (...deps) => {
203
+ const factoryOptions = Reflect.apply(options.useFactory, options, deps);
204
+ return Promise.resolve(factoryOptions).then(resolved => normalizeCacheModuleOptions({
205
+ ...resolved,
206
+ global: options.global ?? false
207
+ }));
208
+ }
209
+ })
164
210
  });
165
211
  }
166
212
  }
@@ -0,0 +1,16 @@
1
+ import type { CacheObserver } from './types.js';
2
+ interface MonotonicClock {
3
+ now(): number;
4
+ }
5
+ /**
6
+ * Internal operation wrapper that times cache work and contains observer failures.
7
+ */
8
+ export declare class CacheOperationObserver {
9
+ private readonly observer;
10
+ private readonly clock;
11
+ constructor(observer: CacheObserver | undefined, clock: MonotonicClock);
12
+ observeRead<T>(operation: 'get' | 'remember', run: () => Promise<T>, classify: (value: T) => 'hit' | 'miss'): Promise<T>;
13
+ observeWrite<T>(operation: 'set' | 'del' | 'reset' | 'close', run: () => Promise<T>): Promise<T>;
14
+ }
15
+ export {};
16
+ //# sourceMappingURL=operation-observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-observer.d.ts","sourceRoot":"","sources":["../src/operation-observer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,aAAa,EAAE,MAAM,YAAY,CAAC;AAElE,UAAU,cAAc;IACtB,GAAG,IAAI,MAAM,CAAC;CACf;AAUD;;GAEG;AACH,qBAAa,sBAAsB;IAE/B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,KAAK,EAAE,cAAc;IAGlC,WAAW,CAAC,CAAC,EACjB,SAAS,EAAE,KAAK,GAAG,UAAU,EAC7B,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,GAAG,MAAM,GACrC,OAAO,CAAC,CAAC,CAAC;IAyBP,YAAY,CAAC,CAAC,EAClB,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,EAC5C,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC;CAwBd"}
@@ -0,0 +1,61 @@
1
+ function reportObservation(observer, observation) {
2
+ try {
3
+ void Promise.resolve(observer.onCacheOperation(observation)).catch(() => undefined);
4
+ } catch {
5
+ return;
6
+ }
7
+ }
8
+
9
+ /**
10
+ * Internal operation wrapper that times cache work and contains observer failures.
11
+ */
12
+ export class CacheOperationObserver {
13
+ constructor(observer, clock) {
14
+ this.observer = observer;
15
+ this.clock = clock;
16
+ }
17
+ async observeRead(operation, run, classify) {
18
+ if (!this.observer) {
19
+ return run();
20
+ }
21
+ const startedAt = this.clock.now();
22
+ try {
23
+ const value = await run();
24
+ reportObservation(this.observer, {
25
+ durationMs: this.clock.now() - startedAt,
26
+ operation,
27
+ outcome: classify(value)
28
+ });
29
+ return value;
30
+ } catch (error) {
31
+ reportObservation(this.observer, {
32
+ durationMs: this.clock.now() - startedAt,
33
+ operation,
34
+ outcome: 'error'
35
+ });
36
+ throw error;
37
+ }
38
+ }
39
+ async observeWrite(operation, run) {
40
+ if (!this.observer) {
41
+ return run();
42
+ }
43
+ const startedAt = this.clock.now();
44
+ try {
45
+ const value = await run();
46
+ reportObservation(this.observer, {
47
+ durationMs: this.clock.now() - startedAt,
48
+ operation,
49
+ outcome: 'success'
50
+ });
51
+ return value;
52
+ } catch (error) {
53
+ reportObservation(this.observer, {
54
+ durationMs: this.clock.now() - startedAt,
55
+ operation,
56
+ outcome: 'error'
57
+ });
58
+ throw error;
59
+ }
60
+ }
61
+ }
package/dist/service.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { CacheStore, NormalizedCacheModuleOptions } from './types.js';
2
+ interface MonotonicClock {
3
+ now(): number;
4
+ }
2
5
  /**
3
6
  * Application-level cache facade used for direct cache reads, writes, and read-through loading.
4
7
  */
@@ -10,12 +13,13 @@ export declare class CacheService {
10
13
  private readonly pendingInvalidations;
11
14
  private readonly invalidatedInflight;
12
15
  private closed;
16
+ private closePromise;
13
17
  private resetVersion;
14
- private storeOperationTail;
15
- private runStoreOperation;
18
+ private readonly storeOperations;
16
19
  private beginPendingLoad;
17
20
  private endPendingLoad;
18
- constructor(store: CacheStore, options: NormalizedCacheModuleOptions);
21
+ private readonly operationObserver;
22
+ constructor(store: CacheStore, options: NormalizedCacheModuleOptions, clock?: MonotonicClock);
19
23
  /**
20
24
  * Read a cached value by key.
21
25
  *
@@ -23,6 +27,7 @@ export declare class CacheService {
23
27
  * @returns The cached value, or `undefined` when the key is missing or expired.
24
28
  */
25
29
  get<T = unknown>(key: string): Promise<T | undefined>;
30
+ private readFromStore;
26
31
  /**
27
32
  * Store a value in the configured cache store.
28
33
  *
@@ -30,8 +35,14 @@ export declare class CacheService {
30
35
  * @param value Value to cache.
31
36
  * @param ttlSeconds Optional per-call TTL override in seconds.
32
37
  * @returns A promise that resolves after the write completes.
38
+ *
39
+ * @remarks
40
+ * When `ttlJitter` is configured, a positive resolved TTL is jittered once here, before store handoff,
41
+ * so every store observes the same effective expiry. `ttl: 0` stays a no-expiry write and invalid TTL
42
+ * values still skip the write entirely.
33
43
  */
34
44
  set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void>;
45
+ private writeToStore;
35
46
  /**
36
47
  * Load a value through the cache, de-duplicating concurrent misses for the same key.
37
48
  *
@@ -41,6 +52,7 @@ export declare class CacheService {
41
52
  * @returns The cached or freshly loaded value.
42
53
  */
43
54
  remember<T = unknown>(key: string, loader: () => Promise<T>, ttlSeconds?: number): Promise<T>;
55
+ private rememberThroughStore;
44
56
  /**
45
57
  * Delete a single cache entry.
46
58
  *
@@ -48,18 +60,23 @@ export declare class CacheService {
48
60
  * @returns A promise that resolves after the entry is removed.
49
61
  */
50
62
  del(key: string): Promise<void>;
63
+ private invalidateKey;
51
64
  /**
52
65
  * Clear every cache entry owned by the configured store.
53
66
  *
54
67
  * @returns A promise that resolves after the store reset completes.
55
68
  */
56
69
  reset(): Promise<void>;
70
+ private resetStore;
57
71
  /**
58
72
  * Close the configured store when it exposes an optional teardown hook.
59
73
  *
74
+ * Concurrent and repeated calls share the first teardown completion and failure.
75
+ *
60
76
  * @returns A promise that resolves after store teardown completes.
61
77
  */
62
78
  close(): Promise<void>;
79
+ private closeStore;
63
80
  private deleteFromStore;
64
81
  /**
65
82
  * Runtime shutdown hook that releases resource-owning stores during application close.
@@ -68,4 +85,5 @@ export declare class CacheService {
68
85
  */
69
86
  onModuleDestroy(): Promise<void>;
70
87
  }
88
+ export {};
71
89
  //# sourceMappingURL=service.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAS3E,UAAU,cAAc;IACtB,GAAG,IAAI,MAAM,CAAC;CACf;AAID;;GAEG;AACH,qBACa,YAAY;IAyCrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAzC1B,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,CAA4B;IAChD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiC;IAEjE,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyB;gBAGxC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,4BAA4B,EACtD,KAAK,GAAE,cAAiC;IAK1C;;;;;OAKG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAQrD,OAAO,CAAC,aAAa;IAcrB;;;;;;;;;;;;OAYG;IACG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAInE,YAAY;IAkB1B;;;;;;;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;YAUC,oBAAoB;IAsElC;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAIvB,aAAa;IAkB3B;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,UAAU;IAmBxB;;;;;;OAMG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,OAAO,CAAC,UAAU;YAoBJ,eAAe;IAU7B;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAGjC"}
package/dist/service.js CHANGED
@@ -5,11 +5,19 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
7
  import { Inject } from '@fluojs/core';
8
+ import { CacheOperationObserver } from './operation-observer.js';
9
+ import { StoreOperationScheduler } from './store-operation-scheduler.js';
8
10
  import { CACHE_OPTIONS, CACHE_STORE } from './tokens.js';
9
- let _CacheService;
11
+ import { applyCacheTtlJitter } from './ttl-jitter.js';
12
+
13
+ // allow: SIZE_OK — cache lifecycle and in-flight invalidation form one indivisible state machine.
14
+
15
+ const systemCacheClock = globalThis.performance;
16
+
10
17
  /**
11
18
  * Application-level cache facade used for direct cache reads, writes, and read-through loading.
12
19
  */
20
+ let _CacheService;
13
21
  class CacheService {
14
22
  static {
15
23
  [_CacheService, _initClass] = _applyDecs(this, [Inject(CACHE_STORE, CACHE_OPTIONS)], []).c;
@@ -19,13 +27,9 @@ class CacheService {
19
27
  pendingInvalidations = new Map();
20
28
  invalidatedInflight = new Set();
21
29
  closed = false;
30
+ closePromise;
22
31
  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
- }
32
+ storeOperations = new StoreOperationScheduler();
29
33
  beginPendingLoad(key, generation) {
30
34
  const generations = this.pendingLoads.get(key) ?? new Map();
31
35
  generations.set(generation, (generations.get(generation) ?? 0) + 1);
@@ -46,9 +50,11 @@ class CacheService {
46
50
  this.pendingLoads.delete(key);
47
51
  }
48
52
  }
49
- constructor(store, options) {
53
+ operationObserver;
54
+ constructor(store, options, clock = systemCacheClock) {
50
55
  this.store = store;
51
56
  this.options = options;
57
+ this.operationObserver = new CacheOperationObserver(options.observer, clock);
52
58
  }
53
59
 
54
60
  /**
@@ -58,10 +64,13 @@ class CacheService {
58
64
  * @returns The cached value, or `undefined` when the key is missing or expired.
59
65
  */
60
66
  get(key) {
67
+ return this.operationObserver.observeRead('get', () => this.readFromStore(key), value => value === undefined ? 'miss' : 'hit');
68
+ }
69
+ readFromStore(key) {
61
70
  if (this.closed) {
62
71
  return Promise.resolve(undefined);
63
72
  }
64
- return this.runStoreOperation(() => {
73
+ return this.storeOperations.run(() => {
65
74
  if (this.closed) {
66
75
  return undefined;
67
76
  }
@@ -76,17 +85,26 @@ class CacheService {
76
85
  * @param value Value to cache.
77
86
  * @param ttlSeconds Optional per-call TTL override in seconds.
78
87
  * @returns A promise that resolves after the write completes.
88
+ *
89
+ * @remarks
90
+ * When `ttlJitter` is configured, a positive resolved TTL is jittered once here, before store handoff,
91
+ * so every store observes the same effective expiry. `ttl: 0` stays a no-expiry write and invalid TTL
92
+ * values still skip the write entirely.
79
93
  */
80
94
  async set(key, value, ttlSeconds) {
95
+ await this.operationObserver.observeWrite('set', () => this.writeToStore(key, value, ttlSeconds));
96
+ }
97
+ async writeToStore(key, value, ttlSeconds) {
81
98
  const resolvedTtl = ttlSeconds ?? this.options.ttl;
82
99
  if (this.closed || !Number.isFinite(resolvedTtl) || resolvedTtl < 0) {
83
100
  return;
84
101
  }
85
- await this.runStoreOperation(async () => {
102
+ const effectiveTtl = applyCacheTtlJitter(resolvedTtl, this.options.ttlJitter);
103
+ await this.storeOperations.run(async () => {
86
104
  if (this.closed) {
87
105
  return;
88
106
  }
89
- await this.store.set(key, value, resolvedTtl);
107
+ await this.store.set(key, value, effectiveTtl);
90
108
  });
91
109
  }
92
110
 
@@ -99,22 +117,26 @@ class CacheService {
99
117
  * @returns The cached or freshly loaded value.
100
118
  */
101
119
  async remember(key, loader, ttlSeconds) {
120
+ const [value] = await this.operationObserver.observeRead('remember', () => this.rememberThroughStore(key, loader, ttlSeconds), ([, outcome]) => outcome);
121
+ return value;
122
+ }
123
+ async rememberThroughStore(key, loader, ttlSeconds) {
102
124
  if (this.closed) {
103
- return loader();
125
+ return [await loader(), 'miss'];
104
126
  }
105
127
  const resetVersion = this.resetVersion;
106
128
  this.beginPendingLoad(key, resetVersion);
107
129
  try {
108
- const cached = await this.get(key);
130
+ const cached = await this.readFromStore(key);
109
131
  if (cached !== undefined) {
110
- return cached;
132
+ return [cached, 'hit'];
111
133
  }
112
134
  if (this.closed || this.resetVersion !== resetVersion) {
113
- return loader();
135
+ return [await loader(), 'miss'];
114
136
  }
115
137
  const existing = this.inflight.get(key);
116
138
  if (existing && existing.generation === resetVersion) {
117
- return existing.promise;
139
+ return [await existing.promise, 'miss'];
118
140
  }
119
141
  if (existing) {
120
142
  this.inflight.delete(key);
@@ -128,7 +150,7 @@ class CacheService {
128
150
  if (this.closed || entry.invalidated || this.resetVersion !== resetVersion) {
129
151
  return value;
130
152
  }
131
- await this.set(key, value, ttlSeconds);
153
+ await this.writeToStore(key, value, ttlSeconds);
132
154
  if (!this.closed && (entry.invalidated || this.resetVersion !== resetVersion)) {
133
155
  await this.deleteFromStore(key);
134
156
  }
@@ -144,7 +166,7 @@ class CacheService {
144
166
  });
145
167
  entry.promise = promise;
146
168
  this.inflight.set(key, entry);
147
- return promise;
169
+ return [await promise, 'miss'];
148
170
  } finally {
149
171
  this.endPendingLoad(key, resetVersion);
150
172
  }
@@ -157,6 +179,9 @@ class CacheService {
157
179
  * @returns A promise that resolves after the entry is removed.
158
180
  */
159
181
  async del(key) {
182
+ await this.operationObserver.observeWrite('del', () => this.invalidateKey(key));
183
+ }
184
+ async invalidateKey(key) {
160
185
  if (this.closed) {
161
186
  return;
162
187
  }
@@ -177,6 +202,9 @@ class CacheService {
177
202
  * @returns A promise that resolves after the store reset completes.
178
203
  */
179
204
  async reset() {
205
+ await this.operationObserver.observeWrite('reset', () => this.resetStore());
206
+ }
207
+ async resetStore() {
180
208
  if (this.closed) {
181
209
  return;
182
210
  }
@@ -185,7 +213,7 @@ class CacheService {
185
213
  this.pendingLoads.clear();
186
214
  this.pendingInvalidations.clear();
187
215
  this.invalidatedInflight.clear();
188
- await this.runStoreOperation(async () => {
216
+ await this.storeOperations.runExclusive(async () => {
189
217
  if (this.closed) {
190
218
  return;
191
219
  }
@@ -196,19 +224,25 @@ class CacheService {
196
224
  /**
197
225
  * Close the configured store when it exposes an optional teardown hook.
198
226
  *
227
+ * Concurrent and repeated calls share the first teardown completion and failure.
228
+ *
199
229
  * @returns A promise that resolves after store teardown completes.
200
230
  */
201
- async close() {
202
- if (this.closed) {
203
- return;
231
+ close() {
232
+ if (this.closePromise) {
233
+ return this.closePromise;
204
234
  }
235
+ this.closePromise = this.operationObserver.observeWrite('close', () => this.closeStore());
236
+ return this.closePromise;
237
+ }
238
+ closeStore() {
205
239
  this.closed = true;
206
240
  this.resetVersion += 1;
207
241
  this.inflight.clear();
208
242
  this.pendingLoads.clear();
209
243
  this.pendingInvalidations.clear();
210
244
  this.invalidatedInflight.clear();
211
- await this.runStoreOperation(async () => {
245
+ return this.storeOperations.runExclusive(async () => {
212
246
  if (this.store.close) {
213
247
  await this.store.close();
214
248
  return;
@@ -219,7 +253,7 @@ class CacheService {
219
253
  });
220
254
  }
221
255
  async deleteFromStore(key) {
222
- await this.runStoreOperation(async () => {
256
+ await this.storeOperations.run(async () => {
223
257
  if (this.closed) {
224
258
  return;
225
259
  }
package/dist/status.js CHANGED
@@ -18,7 +18,7 @@ function resolveStoreOwnershipMode(input) {
18
18
  if (input.storeOwnershipMode) {
19
19
  return input.storeOwnershipMode;
20
20
  }
21
- return input.storeKind === 'memory' ? 'framework' : 'external';
21
+ return input.storeKind === 'redis' ? 'external' : 'framework';
22
22
  }
23
23
  function isBackingStoreReady(input) {
24
24
  if (input.backingStoreReady !== undefined) {