@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.
- package/README.ko.md +150 -7
- package/README.md +151 -8
- 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 -49
- 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 +21 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +58 -24
- 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/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,QAAQ,EACR,
|
|
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,
|
|
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;
|
|
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"}
|
package/dist/interceptor.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 (
|
|
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
|
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,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
|
|
15
|
-
private runStoreOperation;
|
|
18
|
+
private readonly storeOperations;
|
|
16
19
|
private beginPendingLoad;
|
|
17
20
|
private endPendingLoad;
|
|
18
|
-
|
|
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
|
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,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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
202
|
-
if (this.
|
|
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
|
-
|
|
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.
|
|
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 === '
|
|
21
|
+
return input.storeKind === 'redis' ? 'external' : 'framework';
|
|
22
22
|
}
|
|
23
23
|
function isBackingStoreReady(input) {
|
|
24
24
|
if (input.backingStoreReady !== undefined) {
|