@fluojs/cache-manager 1.0.4 → 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":"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,10 +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;
18
+ private readonly storeOperations;
14
19
  private beginPendingLoad;
15
20
  private endPendingLoad;
16
- constructor(store: CacheStore, options: NormalizedCacheModuleOptions);
21
+ private readonly operationObserver;
22
+ constructor(store: CacheStore, options: NormalizedCacheModuleOptions, clock?: MonotonicClock);
17
23
  /**
18
24
  * Read a cached value by key.
19
25
  *
@@ -21,6 +27,7 @@ export declare class CacheService {
21
27
  * @returns The cached value, or `undefined` when the key is missing or expired.
22
28
  */
23
29
  get<T = unknown>(key: string): Promise<T | undefined>;
30
+ private readFromStore;
24
31
  /**
25
32
  * Store a value in the configured cache store.
26
33
  *
@@ -28,8 +35,14 @@ export declare class CacheService {
28
35
  * @param value Value to cache.
29
36
  * @param ttlSeconds Optional per-call TTL override in seconds.
30
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.
31
43
  */
32
44
  set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void>;
45
+ private writeToStore;
33
46
  /**
34
47
  * Load a value through the cache, de-duplicating concurrent misses for the same key.
35
48
  *
@@ -39,6 +52,7 @@ export declare class CacheService {
39
52
  * @returns The cached or freshly loaded value.
40
53
  */
41
54
  remember<T = unknown>(key: string, loader: () => Promise<T>, ttlSeconds?: number): Promise<T>;
55
+ private rememberThroughStore;
42
56
  /**
43
57
  * Delete a single cache entry.
44
58
  *
@@ -46,18 +60,24 @@ export declare class CacheService {
46
60
  * @returns A promise that resolves after the entry is removed.
47
61
  */
48
62
  del(key: string): Promise<void>;
63
+ private invalidateKey;
49
64
  /**
50
65
  * Clear every cache entry owned by the configured store.
51
66
  *
52
67
  * @returns A promise that resolves after the store reset completes.
53
68
  */
54
69
  reset(): Promise<void>;
70
+ private resetStore;
55
71
  /**
56
72
  * Close the configured store when it exposes an optional teardown hook.
57
73
  *
74
+ * Concurrent and repeated calls share the first teardown completion and failure.
75
+ *
58
76
  * @returns A promise that resolves after store teardown completes.
59
77
  */
60
78
  close(): Promise<void>;
79
+ private closeStore;
80
+ private deleteFromStore;
61
81
  /**
62
82
  * Runtime shutdown hook that releases resource-owning stores during application close.
63
83
  *
@@ -65,4 +85,5 @@ export declare class CacheService {
65
85
  */
66
86
  onModuleDestroy(): Promise<void>;
67
87
  }
88
+ export {};
68
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;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":"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,7 +27,9 @@ class CacheService {
19
27
  pendingInvalidations = new Map();
20
28
  invalidatedInflight = new Set();
21
29
  closed = false;
30
+ closePromise;
22
31
  resetVersion = 0;
32
+ storeOperations = new StoreOperationScheduler();
23
33
  beginPendingLoad(key, generation) {
24
34
  const generations = this.pendingLoads.get(key) ?? new Map();
25
35
  generations.set(generation, (generations.get(generation) ?? 0) + 1);
@@ -40,9 +50,11 @@ class CacheService {
40
50
  this.pendingLoads.delete(key);
41
51
  }
42
52
  }
43
- constructor(store, options) {
53
+ operationObserver;
54
+ constructor(store, options, clock = systemCacheClock) {
44
55
  this.store = store;
45
56
  this.options = options;
57
+ this.operationObserver = new CacheOperationObserver(options.observer, clock);
46
58
  }
47
59
 
48
60
  /**
@@ -52,7 +64,18 @@ class CacheService {
52
64
  * @returns The cached value, or `undefined` when the key is missing or expired.
53
65
  */
54
66
  get(key) {
55
- return Promise.resolve(this.store.get(key));
67
+ return this.operationObserver.observeRead('get', () => this.readFromStore(key), value => value === undefined ? 'miss' : 'hit');
68
+ }
69
+ readFromStore(key) {
70
+ if (this.closed) {
71
+ return Promise.resolve(undefined);
72
+ }
73
+ return this.storeOperations.run(() => {
74
+ if (this.closed) {
75
+ return undefined;
76
+ }
77
+ return this.store.get(key);
78
+ });
56
79
  }
57
80
 
58
81
  /**
@@ -62,13 +85,27 @@ class CacheService {
62
85
  * @param value Value to cache.
63
86
  * @param ttlSeconds Optional per-call TTL override in seconds.
64
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.
65
93
  */
66
94
  async set(key, value, ttlSeconds) {
95
+ await this.operationObserver.observeWrite('set', () => this.writeToStore(key, value, ttlSeconds));
96
+ }
97
+ async writeToStore(key, value, ttlSeconds) {
67
98
  const resolvedTtl = ttlSeconds ?? this.options.ttl;
68
- if (!Number.isFinite(resolvedTtl) || resolvedTtl < 0) {
99
+ if (this.closed || !Number.isFinite(resolvedTtl) || resolvedTtl < 0) {
69
100
  return;
70
101
  }
71
- await this.store.set(key, value, resolvedTtl);
102
+ const effectiveTtl = applyCacheTtlJitter(resolvedTtl, this.options.ttlJitter);
103
+ await this.storeOperations.run(async () => {
104
+ if (this.closed) {
105
+ return;
106
+ }
107
+ await this.store.set(key, value, effectiveTtl);
108
+ });
72
109
  }
73
110
 
74
111
  /**
@@ -80,16 +117,29 @@ class CacheService {
80
117
  * @returns The cached or freshly loaded value.
81
118
  */
82
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) {
124
+ if (this.closed) {
125
+ return [await loader(), 'miss'];
126
+ }
83
127
  const resetVersion = this.resetVersion;
84
128
  this.beginPendingLoad(key, resetVersion);
85
129
  try {
86
- const cached = await this.get(key);
130
+ const cached = await this.readFromStore(key);
87
131
  if (cached !== undefined) {
88
- return cached;
132
+ return [cached, 'hit'];
133
+ }
134
+ if (this.closed || this.resetVersion !== resetVersion) {
135
+ return [await loader(), 'miss'];
89
136
  }
90
137
  const existing = this.inflight.get(key);
138
+ if (existing && existing.generation === resetVersion) {
139
+ return [await existing.promise, 'miss'];
140
+ }
91
141
  if (existing) {
92
- return existing.promise;
142
+ this.inflight.delete(key);
93
143
  }
94
144
  const entry = {
95
145
  generation: resetVersion,
@@ -97,12 +147,12 @@ class CacheService {
97
147
  promise: Promise.resolve(undefined)
98
148
  };
99
149
  const promise = loader().then(async value => {
100
- if (entry.invalidated || this.resetVersion !== resetVersion) {
150
+ if (this.closed || entry.invalidated || this.resetVersion !== resetVersion) {
101
151
  return value;
102
152
  }
103
- await this.set(key, value, ttlSeconds);
104
- if (entry.invalidated || this.resetVersion !== resetVersion) {
105
- await this.store.del(key);
153
+ await this.writeToStore(key, value, ttlSeconds);
154
+ if (!this.closed && (entry.invalidated || this.resetVersion !== resetVersion)) {
155
+ await this.deleteFromStore(key);
106
156
  }
107
157
  return value;
108
158
  }).finally(() => {
@@ -116,7 +166,7 @@ class CacheService {
116
166
  });
117
167
  entry.promise = promise;
118
168
  this.inflight.set(key, entry);
119
- return promise;
169
+ return [await promise, 'miss'];
120
170
  } finally {
121
171
  this.endPendingLoad(key, resetVersion);
122
172
  }
@@ -129,6 +179,12 @@ class CacheService {
129
179
  * @returns A promise that resolves after the entry is removed.
130
180
  */
131
181
  async del(key) {
182
+ await this.operationObserver.observeWrite('del', () => this.invalidateKey(key));
183
+ }
184
+ async invalidateKey(key) {
185
+ if (this.closed) {
186
+ return;
187
+ }
132
188
  const entry = this.inflight.get(key);
133
189
  if (entry) {
134
190
  entry.invalidated = true;
@@ -137,7 +193,7 @@ class CacheService {
137
193
  this.pendingInvalidations.set(key, this.resetVersion);
138
194
  this.invalidatedInflight.add(key);
139
195
  }
140
- await this.store.del(key);
196
+ await this.deleteFromStore(key);
141
197
  }
142
198
 
143
199
  /**
@@ -146,35 +202,63 @@ class CacheService {
146
202
  * @returns A promise that resolves after the store reset completes.
147
203
  */
148
204
  async reset() {
205
+ await this.operationObserver.observeWrite('reset', () => this.resetStore());
206
+ }
207
+ async resetStore() {
208
+ if (this.closed) {
209
+ return;
210
+ }
149
211
  this.resetVersion += 1;
150
212
  this.inflight.clear();
151
213
  this.pendingLoads.clear();
152
214
  this.pendingInvalidations.clear();
153
215
  this.invalidatedInflight.clear();
154
- await this.store.reset();
216
+ await this.storeOperations.runExclusive(async () => {
217
+ if (this.closed) {
218
+ return;
219
+ }
220
+ await this.store.reset();
221
+ });
155
222
  }
156
223
 
157
224
  /**
158
225
  * Close the configured store when it exposes an optional teardown hook.
159
226
  *
227
+ * Concurrent and repeated calls share the first teardown completion and failure.
228
+ *
160
229
  * @returns A promise that resolves after store teardown completes.
161
230
  */
162
- async close() {
163
- if (this.closed) {
164
- return;
231
+ close() {
232
+ if (this.closePromise) {
233
+ return this.closePromise;
165
234
  }
235
+ this.closePromise = this.operationObserver.observeWrite('close', () => this.closeStore());
236
+ return this.closePromise;
237
+ }
238
+ closeStore() {
166
239
  this.closed = true;
240
+ this.resetVersion += 1;
167
241
  this.inflight.clear();
168
242
  this.pendingLoads.clear();
169
243
  this.pendingInvalidations.clear();
170
244
  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
- }
245
+ return this.storeOperations.runExclusive(async () => {
246
+ if (this.store.close) {
247
+ await this.store.close();
248
+ return;
249
+ }
250
+ if (this.store.dispose) {
251
+ await this.store.dispose();
252
+ }
253
+ });
254
+ }
255
+ async deleteFromStore(key) {
256
+ await this.storeOperations.run(async () => {
257
+ if (this.closed) {
258
+ return;
259
+ }
260
+ await this.store.del(key);
261
+ });
178
262
  }
179
263
 
180
264
  /**
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) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Ordering policy for cache store operations.
3
+ *
4
+ * Ordinary operations run concurrently with each other, so a slow operation for one key
5
+ * cannot block unrelated keys. Exclusive operations, such as reset and store teardown,
6
+ * close admission for later operations, drain already-started operations, and then run alone.
7
+ */
8
+ export declare class StoreOperationScheduler {
9
+ private exclusiveTail;
10
+ private readonly activeOperations;
11
+ /**
12
+ * Run an ordinary store operation once any pending exclusive operation has completed.
13
+ *
14
+ * @param operation Store operation to run concurrently with other ordinary operations.
15
+ * @returns The operation result, including its rejection.
16
+ */
17
+ run<T>(operation: () => Promise<T> | T): Promise<T>;
18
+ /**
19
+ * Run a store operation exclusively after every already-started operation has settled.
20
+ *
21
+ * Operations submitted after this call wait for the exclusive operation to complete.
22
+ *
23
+ * @param operation Store operation that requires exclusive store access.
24
+ * @returns The operation result, including its rejection.
25
+ */
26
+ runExclusive(operation: () => Promise<void>): Promise<void>;
27
+ }
28
+ //# sourceMappingURL=store-operation-scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-operation-scheduler.d.ts","sourceRoot":"","sources":["../src/store-operation-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA4B;IAE7D;;;;;OAKG;IACH,GAAG,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAmBnD;;;;;;;OAOG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAgB5D"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Ordering policy for cache store operations.
3
+ *
4
+ * Ordinary operations run concurrently with each other, so a slow operation for one key
5
+ * cannot block unrelated keys. Exclusive operations, such as reset and store teardown,
6
+ * close admission for later operations, drain already-started operations, and then run alone.
7
+ */
8
+ export class StoreOperationScheduler {
9
+ exclusiveTail = Promise.resolve();
10
+ activeOperations = new Set();
11
+
12
+ /**
13
+ * Run an ordinary store operation once any pending exclusive operation has completed.
14
+ *
15
+ * @param operation Store operation to run concurrently with other ordinary operations.
16
+ * @returns The operation result, including its rejection.
17
+ */
18
+ run(operation) {
19
+ const start = () => {
20
+ const result = Promise.resolve(operation());
21
+ const settled = result.then(() => undefined, () => undefined);
22
+ this.activeOperations.add(settled);
23
+ void settled.then(() => {
24
+ this.activeOperations.delete(settled);
25
+ });
26
+ return result;
27
+ };
28
+ return this.exclusiveTail.then(start, start);
29
+ }
30
+
31
+ /**
32
+ * Run a store operation exclusively after every already-started operation has settled.
33
+ *
34
+ * Operations submitted after this call wait for the exclusive operation to complete.
35
+ *
36
+ * @param operation Store operation that requires exclusive store access.
37
+ * @returns The operation result, including its rejection.
38
+ */
39
+ runExclusive(operation) {
40
+ const result = this.exclusiveTail.then(async () => {
41
+ while (this.activeOperations.size > 0) {
42
+ await Promise.all(this.activeOperations);
43
+ }
44
+ await operation();
45
+ });
46
+ this.exclusiveTail = result.then(() => undefined, () => undefined);
47
+ return result;
48
+ }
49
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/stores/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAyC9C;;GAEG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,WAAW,CAAK;IAElB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAsBrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBtE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/stores/memory-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA4C9C;;GAEG;AACH,qBAAa,WAAY,YAAW,UAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,WAAW,CAAK;IAElB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAsBrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BtE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
@@ -23,6 +23,9 @@ function enforceEntryLimit(entries) {
23
23
  entries.delete(oldestKey);
24
24
  }
25
25
  }
26
+ function normalizePositiveTtlMilliseconds(ttlSeconds, maximumTtlMilliseconds) {
27
+ return Math.min(maximumTtlMilliseconds, Math.max(1, Math.floor(ttlSeconds * 1000)));
28
+ }
26
29
 
27
30
  /**
28
31
  * Represents the memory store.
@@ -55,7 +58,8 @@ export class MemoryStore {
55
58
  value: cloneCacheValue(value)
56
59
  };
57
60
  if (ttlSeconds > 0) {
58
- const ttlMilliseconds = Math.max(1, Math.floor(ttlSeconds * 1000));
61
+ const maximumTtlMilliseconds = Number.MAX_SAFE_INTEGER - now;
62
+ const ttlMilliseconds = normalizePositiveTtlMilliseconds(ttlSeconds, maximumTtlMilliseconds);
59
63
  entry.expiresAt = now + ttlMilliseconds;
60
64
  }
61
65
  this.entries.delete(key);
@@ -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;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"}
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;AAoDD;;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;IAsB5E,OAAO,CAAC,aAAa;IAMf,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAoC7B"}