@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.
- package/README.ko.md +214 -19
- package/README.md +215 -20
- package/dist/decorators.js +2 -2
- package/dist/deferred-eviction.d.ts +13 -0
- package/dist/deferred-eviction.d.ts.map +1 -0
- package/dist/deferred-eviction.js +85 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +9 -41
- package/dist/module.d.ts +26 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +55 -9
- package/dist/operation-observer.d.ts +16 -0
- package/dist/operation-observer.d.ts.map +1 -0
- package/dist/operation-observer.js +61 -0
- package/dist/service.d.ts +22 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +109 -25
- package/dist/status.js +1 -1
- package/dist/store-operation-scheduler.d.ts +28 -0
- package/dist/store-operation-scheduler.d.ts.map +1 -0
- package/dist/store-operation-scheduler.js +49 -0
- package/dist/stores/memory-store.d.ts.map +1 -1
- package/dist/stores/memory-store.js +5 -1
- package/dist/stores/redis-store.d.ts.map +1 -1
- package/dist/stores/redis-store.js +12 -7
- package/dist/ttl-jitter.d.ts +19 -0
- package/dist/ttl-jitter.d.ts.map +1 -0
- package/dist/ttl-jitter.js +72 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/module.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
103
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
104
|
-
if (entry.invalidated || this.resetVersion !== resetVersion) {
|
|
105
|
-
await this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
163
|
-
if (this.
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 === '
|
|
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":"
|
|
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
|
|
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;
|
|
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"}
|