@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/README.md
CHANGED
|
@@ -13,9 +13,13 @@ General-purpose cache manager for fluo with pluggable memory, Redis, and custom
|
|
|
13
13
|
- [Application-Level Caching](#application-level-caching)
|
|
14
14
|
- [Common Patterns](#common-patterns)
|
|
15
15
|
- [Redis Storage](#redis-storage)
|
|
16
|
+
- [TTL Jitter](#ttl-jitter)
|
|
16
17
|
- [Query-Sensitive Caching](#query-sensitive-caching)
|
|
17
18
|
- [Cache Ownership and Reset Scope](#cache-ownership-and-reset-scope)
|
|
19
|
+
- [Observing Cache Operations](#observing-cache-operations)
|
|
20
|
+
- [Async Configuration](#async-configuration)
|
|
18
21
|
- [Manual Module Composition](#manual-module-composition)
|
|
22
|
+
- [NestJS Cache Migration](#nestjs-cache-migration)
|
|
19
23
|
- [Public API Overview](#public-api-overview)
|
|
20
24
|
- [Related Packages](#related-packages)
|
|
21
25
|
- [Example Sources](#example-sources)
|
|
@@ -26,14 +30,18 @@ General-purpose cache manager for fluo with pluggable memory, Redis, and custom
|
|
|
26
30
|
npm install @fluojs/cache-manager
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
`@fluojs/cache-manager` supports Node.js `>=24.0.0 <27` and declares that exact range through `engines.node`. That package-owned support contract means Node versions below 24 and Node 27+ are excluded. Earlier 1.x releases advertised `engines.node >=20.0.0`, which never matched the effective dependency floor.
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
The root `@fluojs/cache-manager` import stays safe for memory-only installs. You only need a Redis client when you explicitly select the Redis-backed store path.
|
|
36
|
+
|
|
37
|
+
For Redis-backed caching with a lifecycle-managed `@fluojs/redis` client:
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
40
|
npm install @fluojs/cache-manager @fluojs/redis ioredis
|
|
35
41
|
```
|
|
36
42
|
|
|
43
|
+
You can instead pass an application-owned compatible client through `redis.client`. That path does not require `@fluojs/redis`; install whichever client package provides the required `get`, `set`, `del`, and tuple-returning `scan` operations, and close that client from the application lifecycle.
|
|
44
|
+
|
|
37
45
|
## When to Use
|
|
38
46
|
|
|
39
47
|
- When you want to cache expensive database queries or external API responses.
|
|
@@ -96,16 +104,30 @@ class UserService {
|
|
|
96
104
|
|
|
97
105
|
### Redis Storage
|
|
98
106
|
|
|
99
|
-
|
|
107
|
+
Set `store: 'redis'`, then choose one of two supported client integration paths:
|
|
108
|
+
|
|
109
|
+
1. Register a default or named raw client with `@fluojs/redis` and let the cache module resolve it through DI.
|
|
110
|
+
2. Pass an application-owned `RedisCompatibleClient` directly through `redis.client`.
|
|
100
111
|
|
|
101
112
|
Memory-only consumers can keep importing from `@fluojs/cache-manager` without installing `@fluojs/redis` or `ioredis`; those optional peers are resolved only when the Redis store path is selected.
|
|
102
113
|
|
|
103
114
|
```typescript
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
import { Module } from '@fluojs/core';
|
|
116
|
+
import { CacheModule } from '@fluojs/cache-manager';
|
|
117
|
+
import { RedisModule } from '@fluojs/redis';
|
|
118
|
+
|
|
119
|
+
@Module({
|
|
120
|
+
imports: [
|
|
121
|
+
RedisModule.forRoot({ name: 'cache', host: 'localhost', port: 6379 }),
|
|
122
|
+
CacheModule.forRoot({
|
|
123
|
+
store: 'redis',
|
|
124
|
+
ttl: 600,
|
|
125
|
+
keyPrefix: 'myapp:cache:',
|
|
126
|
+
redis: { clientName: 'cache' },
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
108
129
|
})
|
|
130
|
+
class AppModule {}
|
|
109
131
|
```
|
|
110
132
|
|
|
111
133
|
If you registered multiple Redis clients, set `redis.clientName` to target a named `@fluojs/redis` connection.
|
|
@@ -119,13 +141,54 @@ CacheModule.forRoot({
|
|
|
119
141
|
})
|
|
120
142
|
```
|
|
121
143
|
|
|
122
|
-
`redis.client`
|
|
144
|
+
`redis.client` is the highest-precedence override and bypasses DI-based client selection entirely. It accepts any client that satisfies the exported `RedisCompatibleClient` contract; `@fluojs/redis` is not loaded or required on this path. The application owns connection startup and shutdown for a directly supplied client.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import Redis from 'ioredis';
|
|
148
|
+
import { Module } from '@fluojs/core';
|
|
149
|
+
import { CacheModule } from '@fluojs/cache-manager';
|
|
150
|
+
|
|
151
|
+
const cacheClient = new Redis({ host: 'localhost', port: 6379 });
|
|
152
|
+
|
|
153
|
+
@Module({
|
|
154
|
+
imports: [
|
|
155
|
+
CacheModule.forRoot({
|
|
156
|
+
store: 'redis',
|
|
157
|
+
keyPrefix: 'myapp:cache:',
|
|
158
|
+
redis: { client: cacheClient },
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
})
|
|
162
|
+
class AppModule {}
|
|
163
|
+
```
|
|
123
164
|
|
|
124
165
|
The built-in `RedisStore` persists entries with `JSON.stringify(...)`. Cache values therefore need to be JSON-compatible: plain objects, arrays, strings, numbers, booleans, and `null` round-trip cleanly, while values such as `Date` come back as JSON output (for example ISO strings), functions/`undefined`/symbols do not survive, and non-serializable values like `bigint` or cyclic graphs should be normalized before caching.
|
|
125
166
|
|
|
126
167
|
Positive Redis TTL values are accepted in seconds and may be fractional. Redis expiry is rounded up to the next whole second because Redis `EX` uses integer seconds, while fluo also records the millisecond-precision expiry timestamp in the stored entry and treats the value as expired once that timestamp is reached. Use `ttl: 0` when you intentionally want no Redis expiry.
|
|
168
|
+
Exceptionally large finite TTL values are capped at the largest safe JavaScript expiry timestamp by both built-in stores, so Redis JSON metadata remains finite and aligns with the memory path.
|
|
169
|
+
|
|
170
|
+
Redis reset ownership is scoped by the top-level `keyPrefix` option, which defaults to `fluo:cache:` and is passed through to the built-in `RedisStore` namespace. `CacheService.reset()` deletes only keys under that prefix for Redis-backed stores, so application-owned Redis data outside the cache prefix is preserved. Redis glob metacharacters in a non-empty prefix (`*`, `?`, `[`, `]`, and `\`) are escaped before `SCAN`, so the configured prefix remains a literal namespace instead of broadening reset ownership. If you intentionally configure an empty `keyPrefix`, reset is limited to keys written by the current `RedisStore` instance instead of scanning `*`; use a non-empty, application-specific prefix when you need reset to cover cache entries across restarts or multiple processes.
|
|
171
|
+
|
|
172
|
+
### TTL Jitter
|
|
127
173
|
|
|
128
|
-
|
|
174
|
+
Popular keys written together can otherwise expire together and synchronize origin load. Opt in to centralized positive-TTL jitter with `ttlJitter`; `CacheService` calculates the effective TTL once before handing the write to memory, Redis, or a custom store.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
CacheModule.forRoot({
|
|
178
|
+
store: 'redis',
|
|
179
|
+
ttl: 600,
|
|
180
|
+
ttlJitter: {
|
|
181
|
+
ratio: 0.1,
|
|
182
|
+
mode: 'symmetric',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ratio` must be greater than `0` and at most `1`. The default `symmetric` mode samples within `ttl ± (ttl * ratio)`; `shorten` only subtracts from the TTL and `lengthen` only adds to it. A `CacheService.set(...)` or `remember(...)` per-call TTL override is jittered instead of the module default. `ttl: 0` remains a no-expiry write, and negative or non-finite TTL values still skip the write.
|
|
188
|
+
|
|
189
|
+
Jitter is disabled only when `ttlJitter` is omitted or `undefined`; `null`, primitives, arrays, and invalid option fields are rejected during module registration. The optional `random` function is a deterministic test seam and must return a finite value in `[0, 1]`; an invalid sample rejects the write instead of being coerced. Production code should normally keep the default `Math.random`.
|
|
190
|
+
|
|
191
|
+
Every jittered positive TTL remains positive and finite within its selected direction. A fully shortened TTL uses JavaScript's smallest positive finite value rather than becoming the no-expiry sentinel, while an upward result beyond the representable range saturates at `Number.MAX_VALUE`. TTL jitter spreads expiry times only. It is not distributed locking, refresh-ahead caching, or cross-instance stampede coordination.
|
|
129
192
|
|
|
130
193
|
### Query-Sensitive Caching
|
|
131
194
|
|
|
@@ -140,7 +203,7 @@ CacheModule.forRoot({
|
|
|
140
203
|
})
|
|
141
204
|
```
|
|
142
205
|
|
|
143
|
-
For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory. These function-based hooks are the supported extension path for request-aware keys; do not subclass `CacheInterceptor` just to replace cache-key generation.
|
|
206
|
+
For fully custom keying, pass a function as `httpKeyStrategy` or use `@CacheKey(...)` with either a literal key or a key factory. An empty literal `@CacheKey('')` remains an explicit key; only absent decorator metadata selects the configured `httpKeyStrategy`. These function-based hooks are the supported extension path for request-aware keys; do not subclass `CacheInterceptor` just to replace cache-key generation.
|
|
144
207
|
|
|
145
208
|
```typescript
|
|
146
209
|
CacheModule.forRoot({
|
|
@@ -170,7 +233,9 @@ The HTTP interceptor caches only successful, uncommitted GET handler results wit
|
|
|
170
233
|
|
|
171
234
|
### Cache Ownership and Reset Scope
|
|
172
235
|
|
|
173
|
-
`
|
|
236
|
+
Ordinary `get(...)`, `set(...)`, and `del(...)` calls run concurrently against the configured store, so a slow store call for one key does not delay unrelated keys.
|
|
237
|
+
|
|
238
|
+
`CacheService.reset()` clears entries owned by the configured store, not unrelated application state. It also serializes store reads/writes across the reset boundary and invalidates in-flight `remember(...)` loaders so loaders that started before the reset cannot repopulate stale entries after the reset completes. For the built-in memory store that means the in-process entries held by that store instance. For Redis, ownership is the configured `keyPrefix` namespace; keep the default `fluo:cache:` or choose a dedicated prefix such as `myapp:cache:` for shared Redis deployments.
|
|
174
239
|
|
|
175
240
|
```typescript
|
|
176
241
|
CacheModule.forRoot({
|
|
@@ -181,10 +246,89 @@ CacheModule.forRoot({
|
|
|
181
246
|
|
|
182
247
|
Avoid sharing a Redis cache prefix with non-cache data. `del(key)` removes the exact cache key resolved by this package, while `reset()` removes only the store-owned cache namespace described above.
|
|
183
248
|
|
|
184
|
-
When the application closes, `CacheService` forwards shutdown to custom stores that expose `close()` or `dispose()`. Use one of those optional hooks when a store owns sockets, pools, timers, or other external resources.
|
|
249
|
+
When the application closes, `CacheService` stops new store reads/writes, waits for already-started store operations, and then forwards shutdown to custom stores that expose `close()` or `dispose()`. Concurrent and repeated `close()` or lifecycle-hook calls share that first teardown completion and failure, so every caller observes the same shutdown boundary while store teardown runs once. Use one of those optional hooks when a store owns sockets, pools, timers, or other external resources.
|
|
185
250
|
|
|
186
251
|
Custom stores can be passed directly through `store` when they implement the `CacheStore` contract. This is the right option for in-process LRU stores, remote caches other than Redis, or test doubles that need to observe cache operations.
|
|
187
252
|
|
|
253
|
+
### Observing Cache Operations
|
|
254
|
+
|
|
255
|
+
The platform status helpers report cache availability only. To measure hit rate, latency, and error outcomes, pass an opt-in `observer` to `CacheModule.forRoot(...)`. The observer is independent of `@fluojs/metrics`; wire it to whichever metrics backend the application already uses.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { CacheModule, type CacheObservation } from '@fluojs/cache-manager';
|
|
259
|
+
|
|
260
|
+
CacheModule.forRoot({
|
|
261
|
+
store: 'memory',
|
|
262
|
+
observer: {
|
|
263
|
+
onCacheOperation(observation: CacheObservation) {
|
|
264
|
+
cacheOperationCounter.inc({
|
|
265
|
+
operation: observation.operation,
|
|
266
|
+
outcome: observation.outcome,
|
|
267
|
+
});
|
|
268
|
+
cacheOperationLatency.observe(observation.durationMs);
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The contract is intentionally narrow:
|
|
275
|
+
|
|
276
|
+
- **Privacy**: an observation carries only `operation`, `outcome`, and `durationMs`. Cache keys, cached values, loader results, and error objects are never passed to the observer, so instrumentation cannot leak application data.
|
|
277
|
+
- **Operation taxonomy**: `operation` is one of `get`, `set`, `del`, `remember`, `reset`, or `close`. `remember` is reported once per call; its internal read is not reported as a separate `get`.
|
|
278
|
+
- **Outcomes**: `CacheObservation` is a discriminated union: read operations (`get`, `remember`) can report only `hit`, `miss`, or `error`, while write, invalidation, and lifecycle operations can report only `success` or `error`. A `remember` call that joins an in-flight load for the same key reports `miss`, because that call did not read a cached value.
|
|
279
|
+
- **Timing**: `durationMs` measures the full `CacheService` operation, including store-queue serialization, with the runtime's monotonic `performance.now()` clock.
|
|
280
|
+
- **Failure containment**: observer errors are swallowed. A thrown error or a rejected promise never changes the value the caller receives and never surfaces as an unhandled rejection. Observer work is not awaited by the cache operation.
|
|
281
|
+
- **HTTP fail-soft interaction**: `CacheInterceptor` still swallows store failures so cache problems cannot fail an otherwise successful handler. The observer sees those failures as `error` observations, which is the supported way to alert on a degraded cache while keeping requests served.
|
|
282
|
+
|
|
283
|
+
When no `observer` is configured, the cache runs its original code path with no observation work.
|
|
284
|
+
Lifecycle diagnostics report the same teardown owner that shutdown actually uses. `createCacheManagerPlatformStatusSnapshot(...)` resolves ownership from lifecycle responsibility rather than treating every non-memory store alike:
|
|
285
|
+
|
|
286
|
+
- The built-in memory store is `framework`-owned because the framework creates and holds it in-process.
|
|
287
|
+
- A custom store is `framework`-owned by default because `CacheService.close()` owns teardown dispatch to its optional `close()` or `dispose()` hook.
|
|
288
|
+
- The Redis store is `external` to `CacheService`, which never closes the client. When the cache module resolves a client through `@fluojs/redis`, that integration owns its lifecycle; when `redis.client` supplies a client directly, the application owns its lifecycle.
|
|
289
|
+
|
|
290
|
+
An explicit `storeOwnershipMode` still wins over the store default. Set it to `external` when the application intentionally retains lifecycle responsibility for a custom store.
|
|
291
|
+
|
|
292
|
+
### Async Configuration
|
|
293
|
+
|
|
294
|
+
Use `CacheModule.forRootAsync(...)` when the final store, TTL, `keyPrefix`, or key strategy must come from DI or asynchronous bootstrap work. List the dependency tokens in `inject`, return ordinary `CacheModuleOptions` from `useFactory`, and the module normalizes that result with the same defaults as `CacheModule.forRoot(...)`.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Module } from '@fluojs/core';
|
|
298
|
+
import { CacheModule } from '@fluojs/cache-manager';
|
|
299
|
+
|
|
300
|
+
import { CacheSettingsService } from './cache-settings.service';
|
|
301
|
+
|
|
302
|
+
@Module({
|
|
303
|
+
imports: [
|
|
304
|
+
CacheModule.forRootAsync({
|
|
305
|
+
inject: [CacheSettingsService],
|
|
306
|
+
useFactory: async (settings: CacheSettingsService) => ({
|
|
307
|
+
store: 'redis',
|
|
308
|
+
ttl: await settings.resolveTtlSeconds(),
|
|
309
|
+
keyPrefix: settings.keyPrefix,
|
|
310
|
+
redis: { clientName: 'cache' },
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
],
|
|
314
|
+
})
|
|
315
|
+
class AppModule {}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Injected tokens must be visible to the container that instantiates the cache module. Provide them as bootstrap runtime providers or export them from a globally visible imported module before the cache options provider resolves. A provider local only to the importing parent module, or an ordinary sibling/parent export, is not visible to the async cache module. The factory runs once per registration when cache providers are first resolved, and a rejected factory fails bootstrap instead of registering a partially configured cache.
|
|
319
|
+
|
|
320
|
+
Module visibility stays on the registration call: pass `global: true` to `CacheModule.forRootAsync({ global: true, ... })`. `useFactory` may return a prepared `CacheModuleOptions` value, including its `global` property; any returned `global` is ignored because module metadata is fixed before the factory runs.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
CacheModule.forRootAsync({
|
|
324
|
+
global: true,
|
|
325
|
+
inject: [CacheSettingsService],
|
|
326
|
+
useFactory: (settings: CacheSettingsService) => ({ store: settings.store }),
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The async path supports the same store selection as `forRoot(...)`: `'memory'`, `'redis'` with a DI-resolved or directly supplied client, and any custom `CacheStore` instance.
|
|
331
|
+
|
|
188
332
|
### Manual Module Composition
|
|
189
333
|
|
|
190
334
|
Use `CacheModule.forRoot(...)` for normal application setup, including custom `defineModule(...)` composition.
|
|
@@ -201,6 +345,33 @@ defineModule(ManualCacheModule, {
|
|
|
201
345
|
});
|
|
202
346
|
```
|
|
203
347
|
|
|
348
|
+
### NestJS Cache Migration
|
|
349
|
+
|
|
350
|
+
`@nestjs/cache-manager` and `@fluojs/cache-manager` expose overlapping cache concepts, but their option names, units, defaults, and ownership do not all carry over. Convert each of the following, and see [NestJS → fluo Migration Map](../../docs/getting-started/migrate-from-nestjs.md) for the full migration contract.
|
|
351
|
+
|
|
352
|
+
| NestJS option or decorator | fluo equivalent | Conversion rule |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `ttl` when the installed underlying `cache-manager` generation uses milliseconds | `ttl` in seconds | Inspect the installed underlying `cache-manager` dependency/version. Divide by 1000 only when that generation defines TTLs in milliseconds. Omitting `ttl` applies `300` seconds on the memory path and `0` for the `redis` and custom-store paths. |
|
|
355
|
+
| `ttl: 0` | `ttl: 0` | Means no expiry, not "do not cache". Negative or non-finite values are invalid: `CacheService.set(...)` drops the write, and `CacheInterceptor` skips both the cache read and write for that handler. |
|
|
356
|
+
| `@CacheTTL(...)` | `@CacheTTL(ttlSeconds: number)` | Accepts one static number only. Move per-request lifetimes to `CacheService.set(key, value, ttlSeconds)`. |
|
|
357
|
+
| implicit query-sensitive keys | `httpKeyStrategy` | Defaults to path-only `'route'`. Select `'route+query'` (or `'full'`), a function strategy, or `@CacheKey(...)` when a response varies by query parameters. |
|
|
358
|
+
| `isGlobal: true` | `global: true` | Both NestJS `isGlobal` and fluo `global` default to `false`, so both cache modules are module-local unless you opt in or import the module everywhere it is resolved. |
|
|
359
|
+
| NestJS store adapters such as `cache-manager-redis-store` | `store: 'redis'` or a `CacheStore` object | NestJS adapters do not satisfy the `CacheStore` contract; use the built-in Redis path or wrap the adapter so callback/options completion becomes a Promise, `ttlSeconds` maps to the legacy TTL in seconds, and `reset()` clears only the cache namespace. Never forward `reset()` blindly to a whole-database `flushDb`. |
|
|
360
|
+
| adapter-owned client teardown | `close()` / `dispose()` on the store | Application shutdown forwards teardown only to those optional hooks. A raw client passed through `redis.client` stays application-owned and must be closed from the application lifecycle. |
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
CacheModule.forRoot({
|
|
364
|
+
// If the installed underlying cache-manager generation uses milliseconds,
|
|
365
|
+
// NestJS `ttl: 60_000` becomes 60 seconds.
|
|
366
|
+
ttl: 60,
|
|
367
|
+
// NestJS `isGlobal: true` becomes `global: true`.
|
|
368
|
+
global: true,
|
|
369
|
+
// Opt in explicitly when responses vary by query parameters.
|
|
370
|
+
httpKeyStrategy: 'route+query',
|
|
371
|
+
store: 'redis',
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
204
375
|
### Memory Store Operational Limits
|
|
205
376
|
|
|
206
377
|
The built-in memory store is designed for single-process, bounded caching:
|
|
@@ -211,29 +382,52 @@ The built-in memory store is designed for single-process, bounded caching:
|
|
|
211
382
|
|
|
212
383
|
### Deferred eviction timing
|
|
213
384
|
|
|
214
|
-
|
|
385
|
+
`@CacheEvict(...)` is HTTP route metadata, not a general service-method decorator. `CacheInterceptor` consumes it only when that interceptor runs around a non-GET controller handler. For service methods and other calls outside the HTTP interceptor pipeline, inject `CacheService` and call `del(...)` explicitly.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { CacheEvict, CacheInterceptor } from '@fluojs/cache-manager';
|
|
389
|
+
import { Controller, Post, UseInterceptors } from '@fluojs/http';
|
|
390
|
+
|
|
391
|
+
@Controller('/products')
|
|
392
|
+
@UseInterceptors(CacheInterceptor)
|
|
393
|
+
class ProductController {
|
|
394
|
+
@Post('/refresh')
|
|
395
|
+
@CacheEvict('/products')
|
|
396
|
+
refresh() {
|
|
397
|
+
return { refreshed: true };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
On that supported HTTP path, eviction is deferred until a framework response writer settles successfully and the response reports that it committed. If a writer rejects, settles without a confirmed commit, or the request aborts before commit because of disconnect or shutdown, deferred eviction is cancelled so the previous cached read result remains available. Adapter paths that commit without invoking `response.send(...)` retain the bounded five-second fallback: it evicts only when `response.committed` already confirms commit at the deadline. An unconfirmed response is cancelled instead, so elapsed time alone cannot evict before a later failed commit. The fallback timer is unreferenced on Node.js and cleared when a response writer settles, so pending fallback work does not keep process shutdown alive. Deferred eviction failures stay contained inside the interceptor, so cache-key factories or cache-store deletes cannot surface as post-response unhandled promise rejections.
|
|
215
403
|
|
|
216
404
|
## Public API Overview
|
|
217
405
|
|
|
218
406
|
### Modules
|
|
219
|
-
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis/custom), default TTL, key strategies, `global`, `principalScopeResolver`, the Redis namespace `keyPrefix`, and Redis options such as `redis.scanCount`.
|
|
407
|
+
- `CacheModule.forRoot(options)`: Configures the cache store (memory/redis/custom), default TTL, opt-in `ttlJitter`, key strategies, `global`, `principalScopeResolver`, the Redis namespace `keyPrefix`, and Redis options such as `redis.scanCount`.
|
|
220
408
|
This is the primary package entrypoint for application modules.
|
|
409
|
+
- `CacheModule.forRootAsync({ inject, useFactory, global? })`: Resolves the same options through an injected factory for applications that build cache configuration from DI or asynchronous bootstrap work. `global` belongs to this registration call, and a rejected factory fails bootstrap.
|
|
221
410
|
|
|
222
411
|
### Public types
|
|
223
|
-
- `CacheModuleOptions`: Application-facing configuration accepted by `CacheModule.forRoot(...)`.
|
|
224
|
-
- `
|
|
412
|
+
- `CacheModuleOptions`: Application-facing configuration accepted by `CacheModule.forRoot(...)`, including optional `ttlJitter` and `observer`.
|
|
413
|
+
- `CacheTtlJitterOptions` and `CacheTtlJitterMode`: Opt-in positive-TTL jitter bounds, direction, and deterministic randomness seam.
|
|
414
|
+
- `NormalizedCacheTtlJitterOptions`: Normalized TTL jitter configuration after defaults are applied.
|
|
415
|
+
- `CacheObserver`: Opt-in observation hook with a single `onCacheOperation(observation)` method.
|
|
416
|
+
- `CacheObservation`: Privacy-safe discriminated union coupling each operation category to its valid outcomes and carrying `durationMs`.
|
|
417
|
+
- `CacheAsyncModuleOptions`: Injected-factory configuration accepted by `CacheModule.forRootAsync(...)`. `useFactory` returns `CacheModuleOptions`; registration-level `global` alone controls module visibility.
|
|
418
|
+
- `NormalizedCacheModuleOptions`: Compatibility-only type export matching the normalized module configuration shape after defaults are applied. Prefer `CacheModuleOptions` for application code; this type remains public so consumers that referenced the previously shipped declaration surface can keep compiling.
|
|
225
419
|
|
|
226
420
|
### Services
|
|
227
|
-
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`, `close`). Application shutdown calls the same `close()` path, which forwards teardown to custom stores exposing `close()` or `dispose()
|
|
421
|
+
- `CacheService`: Main API for manual cache operations (`get`, `set`, `del`, `remember`, `reset`, `close`). Application shutdown calls the same `close()` path, which forwards teardown to custom stores exposing `close()` or `dispose()` and shares the first teardown completion across concurrent or repeated callers.
|
|
228
422
|
|
|
229
423
|
### Decorators
|
|
230
424
|
- `@CacheTTL(seconds)`: Sets the TTL for a specific handler.
|
|
231
425
|
- `@CacheKey(key)`: Sets a custom cache key or key factory for a specific handler.
|
|
232
|
-
- `@CacheEvict(key)`:
|
|
426
|
+
- `@CacheEvict(key)`: Stores HTTP route metadata that `CacheInterceptor` consumes after a successful non-GET controller handler completes; it does not intercept arbitrary service calls.
|
|
233
427
|
- `cacheRouteMetadataKey`, `getCacheKeyMetadata(...)`, `getCacheTtlMetadata(...)`, and `getCacheEvictMetadata(...)`: Low-level metadata helpers exported for first-party interceptor integration, diagnostics, and advanced tooling that needs to inspect cache decorator metadata without reimplementing the metadata keys.
|
|
234
428
|
|
|
235
429
|
### Interceptors
|
|
236
|
-
- `CacheInterceptor`: Handles automatic GET response caching and
|
|
430
|
+
- `CacheInterceptor`: Handles automatic GET response caching and consumes `@CacheEvict(...)` metadata for non-GET HTTP handlers.
|
|
237
431
|
|
|
238
432
|
### Stores and status helpers
|
|
239
433
|
- `MemoryStore` and `RedisStore`: Built-in store implementations.
|
|
@@ -242,7 +436,7 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
|
|
|
242
436
|
|
|
243
437
|
## Related Packages
|
|
244
438
|
|
|
245
|
-
- `@fluojs/redis`:
|
|
439
|
+
- `@fluojs/redis`: Optional lifecycle-managed Redis client integration. It is not required when `redis.client` supplies an application-owned `RedisCompatibleClient` directly.
|
|
246
440
|
- `@fluojs/http`: Required for HTTP interceptors and decorators.
|
|
247
441
|
|
|
248
442
|
## Example Sources
|
|
@@ -251,3 +445,4 @@ For non-GET handlers decorated with `@CacheEvict(...)`, eviction is deferred unt
|
|
|
251
445
|
- `packages/cache-manager/src/interceptor.test.ts`: HTTP caching and eviction tests.
|
|
252
446
|
- `packages/cache-manager/src/service.ts`: Core `CacheService` implementation.
|
|
253
447
|
- `packages/cache-manager/src/status.test.ts`: Status and diagnostic helper tests.
|
|
448
|
+
- `packages/cache-manager/src/cache-observer.test.ts`: Cache observation contract tests.
|
package/dist/decorators.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ensureRequestPipelineMetadataSymbol } from '@fluojs/core/request-pipeline';
|
|
2
2
|
/** Shared controller metadata key used to store per-route cache metadata records. */
|
|
3
3
|
export const cacheRouteMetadataKey = Symbol.for('fluo.standard.route');
|
|
4
4
|
const cacheKeyMetadataKey = Symbol.for('fluo.cache.key');
|
|
5
5
|
const cacheTtlMetadataKey = Symbol.for('fluo.cache.ttl');
|
|
6
6
|
const cacheEvictMetadataKey = Symbol.for('fluo.cache.evict');
|
|
7
|
-
|
|
7
|
+
ensureRequestPipelineMetadataSymbol();
|
|
8
8
|
function getMetadataBag(metadata) {
|
|
9
9
|
return metadata;
|
|
10
10
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InterceptorContext } from '@fluojs/http';
|
|
2
|
+
type RequestAbortState = Pick<InterceptorContext['requestContext']['request'], 'isAborted' | 'signal'>;
|
|
3
|
+
/**
|
|
4
|
+
* Defers cache eviction until a response writer or bounded fallback confirms commit.
|
|
5
|
+
*
|
|
6
|
+
* @param response Active framework response whose writers and committed flag own cleanup.
|
|
7
|
+
* @param request Request cancellation surfaces used to discard eviction during shutdown or disconnect.
|
|
8
|
+
* @param evict Cache eviction work to run after a confirmed successful commit.
|
|
9
|
+
* @returns A cancellation function that restores the response writers and clears the fallback timer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installDeferredEviction(response: InterceptorContext['requestContext']['response'], request: RequestAbortState, evict: () => Promise<void>): () => void;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=deferred-eviction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred-eviction.d.ts","sourceRoot":"","sources":["../src/deferred-eviction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAIvD,KAAK,iBAAiB,GAAG,IAAI,CAC3B,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,SAAS,CAAC,EAC/C,WAAW,GAAG,QAAQ,CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,UAAU,CAAC,EAC1D,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACzB,MAAM,IAAI,CAwFZ"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const EVICTION_FALLBACK_TIMEOUT_MS = 5_000;
|
|
2
|
+
/**
|
|
3
|
+
* Defers cache eviction until a response writer or bounded fallback confirms commit.
|
|
4
|
+
*
|
|
5
|
+
* @param response Active framework response whose writers and committed flag own cleanup.
|
|
6
|
+
* @param request Request cancellation surfaces used to discard eviction during shutdown or disconnect.
|
|
7
|
+
* @param evict Cache eviction work to run after a confirmed successful commit.
|
|
8
|
+
* @returns A cancellation function that restores the response writers and clears the fallback timer.
|
|
9
|
+
*/
|
|
10
|
+
export function installDeferredEviction(response, request, evict) {
|
|
11
|
+
const originalSend = response.send;
|
|
12
|
+
const originalSimpleJsonSend = Reflect.get(response, 'sendSimpleJson');
|
|
13
|
+
const signal = request.signal;
|
|
14
|
+
let restored = false;
|
|
15
|
+
let completed = false;
|
|
16
|
+
let abortListenerInstalled = false;
|
|
17
|
+
let responseWriteStarted = false;
|
|
18
|
+
const requestAborted = () => signal?.aborted === true || request.isAborted?.() === true;
|
|
19
|
+
const runEviction = () => {
|
|
20
|
+
if (completed) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
completed = true;
|
|
24
|
+
void evict().catch(() => {});
|
|
25
|
+
};
|
|
26
|
+
const restore = () => {
|
|
27
|
+
if (restored) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (abortListenerInstalled) {
|
|
31
|
+
signal?.removeEventListener('abort', cancel);
|
|
32
|
+
}
|
|
33
|
+
clearTimeout(fallbackTimer);
|
|
34
|
+
response.send = originalSend;
|
|
35
|
+
if (typeof originalSimpleJsonSend === 'function') {
|
|
36
|
+
Reflect.set(response, 'sendSimpleJson', originalSimpleJsonSend);
|
|
37
|
+
}
|
|
38
|
+
restored = true;
|
|
39
|
+
};
|
|
40
|
+
const cancel = () => {
|
|
41
|
+
completed = true;
|
|
42
|
+
restore();
|
|
43
|
+
};
|
|
44
|
+
const runResponseWrite = async write => {
|
|
45
|
+
responseWriteStarted = true;
|
|
46
|
+
try {
|
|
47
|
+
await write();
|
|
48
|
+
if (requestAborted()) {
|
|
49
|
+
cancel();
|
|
50
|
+
} else if (response.committed) {
|
|
51
|
+
runEviction();
|
|
52
|
+
} else {
|
|
53
|
+
cancel();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
cancel();
|
|
57
|
+
throw error;
|
|
58
|
+
} finally {
|
|
59
|
+
restore();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
response.send = body => runResponseWrite(() => originalSend.call(response, body));
|
|
63
|
+
if (typeof originalSimpleJsonSend === 'function') {
|
|
64
|
+
Reflect.set(response, 'sendSimpleJson', body => {
|
|
65
|
+
return runResponseWrite(() => Reflect.apply(originalSimpleJsonSend, response, [body]));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const fallbackTimer = setTimeout(() => {
|
|
69
|
+
if (!responseWriteStarted && !requestAborted() && response.committed) {
|
|
70
|
+
runEviction();
|
|
71
|
+
}
|
|
72
|
+
restore();
|
|
73
|
+
}, EVICTION_FALLBACK_TIMEOUT_MS);
|
|
74
|
+
fallbackTimer.unref?.();
|
|
75
|
+
if (signal) {
|
|
76
|
+
signal.addEventListener('abort', cancel, {
|
|
77
|
+
once: true
|
|
78
|
+
});
|
|
79
|
+
abortListenerInstalled = true;
|
|
80
|
+
}
|
|
81
|
+
if (requestAborted()) {
|
|
82
|
+
cancel();
|
|
83
|
+
}
|
|
84
|
+
return cancel;
|
|
85
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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';
|
|
5
|
+
export type { CacheManagerPlatformStatusSnapshot, CacheManagerStatusAdapterInput, CacheManagerStoreKind, CacheManagerStoreOwnershipMode, } from './status.js';
|
|
5
6
|
export { createCacheManagerPlatformDiagnosticIssues, createCacheManagerPlatformStatusSnapshot, } from './status.js';
|
|
6
7
|
export { MemoryStore } from './stores/memory-store.js';
|
|
7
8
|
export { RedisStore, type RedisStoreOptions } from './stores/redis-store.js';
|
|
8
9
|
export { CACHE_OPTIONS, CACHE_STORE } from './tokens.js';
|
|
9
|
-
export type { CacheEvictDecoratorValue, CacheEvictFactory, CacheKeyDecoratorValue, CacheKeyFactory, CacheKeyStrategy, CacheModuleOptions, CacheStore, NormalizedCacheModuleOptions, PrincipalScopeResolver, RedisCacheOptions, RedisCompatibleClient, } from './types.js';
|
|
10
|
-
export type { CacheManagerPlatformStatusSnapshot, CacheManagerStatusAdapterInput, CacheManagerStoreKind, CacheManagerStoreOwnershipMode, } from './status.js';
|
|
10
|
+
export type { CacheAsyncModuleOptions, CacheEvictDecoratorValue, CacheEvictFactory, CacheKeyDecoratorValue, CacheKeyFactory, CacheKeyStrategy, CacheModuleOptions, CacheObservation, CacheObserver, CacheStore, CacheTtlJitterMode, CacheTtlJitterOptions, NormalizedCacheModuleOptions, NormalizedCacheTtlJitterOptions, PrincipalScopeResolver, RedisCacheOptions, RedisCompatibleClient, } from './types.js';
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
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,43 +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
|
-
const runEviction = () => {
|
|
102
|
-
if (completed) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
completed = true;
|
|
106
|
-
void evict().catch(() => {});
|
|
107
|
-
};
|
|
108
|
-
const restore = () => {
|
|
109
|
-
if (restored) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
clearTimeout(fallbackTimer);
|
|
113
|
-
response.send = originalSend;
|
|
114
|
-
restored = true;
|
|
115
|
-
};
|
|
116
|
-
const fallbackTimer = setTimeout(() => {
|
|
117
|
-
runEviction();
|
|
118
|
-
restore();
|
|
119
|
-
}, EVICTION_FALLBACK_TIMEOUT_MS);
|
|
120
|
-
response.send = async body => {
|
|
121
|
-
try {
|
|
122
|
-
await originalSend(body);
|
|
123
|
-
runEviction();
|
|
124
|
-
} catch (error) {
|
|
125
|
-
completed = true;
|
|
126
|
-
throw error;
|
|
127
|
-
} finally {
|
|
128
|
-
restore();
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
return restore;
|
|
132
|
-
}
|
|
133
97
|
|
|
134
98
|
/**
|
|
135
99
|
* Caches GET responses and evicts related entries after successful write operations.
|
|
@@ -182,10 +146,14 @@ class CacheInterceptor {
|
|
|
182
146
|
}));
|
|
183
147
|
};
|
|
184
148
|
if (context.requestContext.response.committed) {
|
|
149
|
+
const request = context.requestContext.request;
|
|
150
|
+
if (request.signal?.aborted === true || request.isAborted?.() === true) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
185
153
|
await runEviction();
|
|
186
154
|
return;
|
|
187
155
|
}
|
|
188
|
-
installDeferredEviction(context.requestContext.response, runEviction);
|
|
156
|
+
installDeferredEviction(context.requestContext.response, context.requestContext.request, runEviction);
|
|
189
157
|
}
|
|
190
158
|
async resolveEvictKeys(metadata, context, value) {
|
|
191
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
|