layercache 1.1.0 → 1.2.1
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.md +165 -7
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-QUB5VZFZ.js → chunk-GF47Y3XR.js} +16 -38
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +133 -23
- package/dist/cli.js +66 -4
- package/dist/edge-C1sBhTfv.d.cts +667 -0
- package/dist/edge-C1sBhTfv.d.ts +667 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +1259 -192
- package/dist/index.d.cts +132 -480
- package/dist/index.d.ts +132 -480
- package/dist/index.js +1115 -474
- package/package.json +7 -2
- package/packages/nestjs/dist/index.cjs +1025 -327
- package/packages/nestjs/dist/index.d.cts +167 -1
- package/packages/nestjs/dist/index.d.ts +167 -1
- package/packages/nestjs/dist/index.js +1013 -325
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { DynamicModule, Type } from '@nestjs/common';
|
|
3
3
|
|
|
4
4
|
declare const CACHE_STACK: unique symbol;
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ interface LayerTtlMap {
|
|
|
9
9
|
interface CacheWriteOptions {
|
|
10
10
|
tags?: string[];
|
|
11
11
|
ttl?: number | LayerTtlMap;
|
|
12
|
+
ttlPolicy?: CacheTtlPolicy;
|
|
12
13
|
negativeCache?: boolean;
|
|
13
14
|
negativeTtl?: number | LayerTtlMap;
|
|
14
15
|
staleWhileRevalidate?: number | LayerTtlMap;
|
|
@@ -18,6 +19,16 @@ interface CacheWriteOptions {
|
|
|
18
19
|
refreshAhead?: number | LayerTtlMap;
|
|
19
20
|
adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
|
|
20
21
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
22
|
+
fetcherRateLimit?: CacheRateLimitOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Optional predicate called with the fetcher's return value before caching.
|
|
25
|
+
* Return `false` to skip storing the value in the cache (but still return it
|
|
26
|
+
* to the caller). Useful for not caching failed API responses or empty results.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
|
|
30
|
+
*/
|
|
31
|
+
shouldCache?: (value: unknown) => boolean;
|
|
21
32
|
}
|
|
22
33
|
interface CacheGetOptions extends CacheWriteOptions {
|
|
23
34
|
}
|
|
@@ -38,12 +49,20 @@ interface CacheLayer {
|
|
|
38
49
|
readonly isLocal?: boolean;
|
|
39
50
|
get<T>(key: string): Promise<T | null>;
|
|
40
51
|
getEntry?<T = unknown>(key: string): Promise<T | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Bulk read fast-path. Implementations should return raw stored entries using
|
|
54
|
+
* the same semantics as `getEntry()` so CacheStack can resolve envelopes,
|
|
55
|
+
* stale windows, and negative-cache markers consistently.
|
|
56
|
+
*/
|
|
41
57
|
getMany?<T>(keys: string[]): Promise<Array<T | null>>;
|
|
58
|
+
setMany?(entries: CacheLayerSetManyEntry[]): Promise<void>;
|
|
42
59
|
set(key: string, value: unknown, ttl?: number): Promise<void>;
|
|
43
60
|
delete(key: string): Promise<void>;
|
|
44
61
|
clear(): Promise<void>;
|
|
45
62
|
deleteMany?(keys: string[]): Promise<void>;
|
|
46
63
|
keys?(): Promise<string[]>;
|
|
64
|
+
ping?(): Promise<boolean>;
|
|
65
|
+
dispose?(): Promise<void>;
|
|
47
66
|
/**
|
|
48
67
|
* Returns true if the key exists and has not expired.
|
|
49
68
|
* Implementations may omit this; CacheStack will fall back to `get()`.
|
|
@@ -61,6 +80,15 @@ interface CacheLayer {
|
|
|
61
80
|
*/
|
|
62
81
|
size?(): Promise<number>;
|
|
63
82
|
}
|
|
83
|
+
/** Per-layer latency statistics (rolling window of sampled read durations). */
|
|
84
|
+
interface CacheLayerLatency {
|
|
85
|
+
/** Average read latency in milliseconds. */
|
|
86
|
+
avgMs: number;
|
|
87
|
+
/** Maximum observed read latency in milliseconds. */
|
|
88
|
+
maxMs: number;
|
|
89
|
+
/** Number of samples used to compute the statistics. */
|
|
90
|
+
count: number;
|
|
91
|
+
}
|
|
64
92
|
/** Snapshot of cumulative cache counters. */
|
|
65
93
|
interface CacheMetricsSnapshot {
|
|
66
94
|
hits: number;
|
|
@@ -80,6 +108,8 @@ interface CacheMetricsSnapshot {
|
|
|
80
108
|
degradedOperations: number;
|
|
81
109
|
hitsByLayer: Record<string, number>;
|
|
82
110
|
missesByLayer: Record<string, number>;
|
|
111
|
+
/** Per-layer read latency statistics (sampled from successful reads). */
|
|
112
|
+
latencyByLayer: Record<string, CacheLayerLatency>;
|
|
83
113
|
/** Timestamp (ms since epoch) when metrics were last reset. */
|
|
84
114
|
resetAt: number;
|
|
85
115
|
}
|
|
@@ -101,9 +131,17 @@ interface CacheTagIndex {
|
|
|
101
131
|
track(key: string, tags: string[]): Promise<void>;
|
|
102
132
|
remove(key: string): Promise<void>;
|
|
103
133
|
keysForTag(tag: string): Promise<string[]>;
|
|
134
|
+
keysForPrefix?(prefix: string): Promise<string[]>;
|
|
135
|
+
/** Returns the tags associated with a specific key, or an empty array. */
|
|
136
|
+
tagsForKey?(key: string): Promise<string[]>;
|
|
104
137
|
matchPattern(pattern: string): Promise<string[]>;
|
|
105
138
|
clear(): Promise<void>;
|
|
106
139
|
}
|
|
140
|
+
interface CacheLayerSetManyEntry {
|
|
141
|
+
key: string;
|
|
142
|
+
value: unknown;
|
|
143
|
+
ttl?: number;
|
|
144
|
+
}
|
|
107
145
|
interface InvalidationMessage {
|
|
108
146
|
scope: 'key' | 'keys' | 'clear';
|
|
109
147
|
sourceId: string;
|
|
@@ -128,6 +166,7 @@ interface CacheStackOptions {
|
|
|
128
166
|
stampedePrevention?: boolean;
|
|
129
167
|
invalidationBus?: InvalidationBus;
|
|
130
168
|
tagIndex?: CacheTagIndex;
|
|
169
|
+
generation?: number;
|
|
131
170
|
broadcastL1Invalidation?: boolean;
|
|
132
171
|
/**
|
|
133
172
|
* @deprecated Use `broadcastL1Invalidation` instead.
|
|
@@ -143,6 +182,9 @@ interface CacheStackOptions {
|
|
|
143
182
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
144
183
|
gracefulDegradation?: boolean | CacheDegradationOptions;
|
|
145
184
|
writePolicy?: 'strict' | 'best-effort';
|
|
185
|
+
writeStrategy?: 'write-through' | 'write-behind';
|
|
186
|
+
writeBehind?: CacheWriteBehindOptions;
|
|
187
|
+
fetcherRateLimit?: CacheRateLimitOptions;
|
|
146
188
|
singleFlightCoordinator?: CacheSingleFlightCoordinator;
|
|
147
189
|
singleFlightLeaseMs?: number;
|
|
148
190
|
singleFlightTimeoutMs?: number;
|
|
@@ -159,6 +201,13 @@ interface CacheAdaptiveTtlOptions {
|
|
|
159
201
|
step?: number | LayerTtlMap;
|
|
160
202
|
maxTtl?: number | LayerTtlMap;
|
|
161
203
|
}
|
|
204
|
+
type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
|
|
205
|
+
alignTo: number;
|
|
206
|
+
} | ((context: CacheTtlPolicyContext) => number | undefined);
|
|
207
|
+
interface CacheTtlPolicyContext {
|
|
208
|
+
key: string;
|
|
209
|
+
value: unknown;
|
|
210
|
+
}
|
|
162
211
|
interface CacheCircuitBreakerOptions {
|
|
163
212
|
failureThreshold?: number;
|
|
164
213
|
cooldownMs?: number;
|
|
@@ -166,6 +215,16 @@ interface CacheCircuitBreakerOptions {
|
|
|
166
215
|
interface CacheDegradationOptions {
|
|
167
216
|
retryAfterMs?: number;
|
|
168
217
|
}
|
|
218
|
+
interface CacheRateLimitOptions {
|
|
219
|
+
maxConcurrent?: number;
|
|
220
|
+
intervalMs?: number;
|
|
221
|
+
maxPerInterval?: number;
|
|
222
|
+
}
|
|
223
|
+
interface CacheWriteBehindOptions {
|
|
224
|
+
flushIntervalMs?: number;
|
|
225
|
+
batchSize?: number;
|
|
226
|
+
maxQueueSize?: number;
|
|
227
|
+
}
|
|
169
228
|
interface CacheWarmEntry<T = unknown> {
|
|
170
229
|
key: string;
|
|
171
230
|
fetcher: () => Promise<T>;
|
|
@@ -203,6 +262,28 @@ interface CacheStatsSnapshot {
|
|
|
203
262
|
}>;
|
|
204
263
|
backgroundRefreshes: number;
|
|
205
264
|
}
|
|
265
|
+
interface CacheHealthCheckResult {
|
|
266
|
+
layer: string;
|
|
267
|
+
healthy: boolean;
|
|
268
|
+
latencyMs: number;
|
|
269
|
+
error?: string;
|
|
270
|
+
}
|
|
271
|
+
/** Detailed inspection result for a single cache key. */
|
|
272
|
+
interface CacheInspectResult {
|
|
273
|
+
key: string;
|
|
274
|
+
/** Layers in which the key is currently stored (not expired). */
|
|
275
|
+
foundInLayers: string[];
|
|
276
|
+
/** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
|
|
277
|
+
freshTtlSeconds: number | null;
|
|
278
|
+
/** Remaining stale-while-revalidate window in seconds, or null. */
|
|
279
|
+
staleTtlSeconds: number | null;
|
|
280
|
+
/** Remaining stale-if-error window in seconds, or null. */
|
|
281
|
+
errorTtlSeconds: number | null;
|
|
282
|
+
/** Whether the key is currently serving stale-while-revalidate. */
|
|
283
|
+
isStale: boolean;
|
|
284
|
+
/** Tags associated with this key (from the TagIndex). */
|
|
285
|
+
tags: string[];
|
|
286
|
+
}
|
|
206
287
|
/** All events emitted by CacheStack and their payload shapes. */
|
|
207
288
|
interface CacheStackEvents {
|
|
208
289
|
/** Fired on every cache hit. */
|
|
@@ -255,9 +336,15 @@ interface CacheStackEvents {
|
|
|
255
336
|
declare class CacheNamespace {
|
|
256
337
|
private readonly cache;
|
|
257
338
|
private readonly prefix;
|
|
339
|
+
private static readonly metricsMutexes;
|
|
340
|
+
private metrics;
|
|
258
341
|
constructor(cache: CacheStack, prefix: string);
|
|
259
342
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
260
343
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
344
|
+
/**
|
|
345
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
346
|
+
*/
|
|
347
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
261
348
|
has(key: string): Promise<boolean>;
|
|
262
349
|
ttl(key: string): Promise<number | null>;
|
|
263
350
|
set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
|
|
@@ -267,12 +354,30 @@ declare class CacheNamespace {
|
|
|
267
354
|
mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
|
|
268
355
|
mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
|
|
269
356
|
invalidateByTag(tag: string): Promise<void>;
|
|
357
|
+
invalidateByTags(tags: string[], mode?: 'any' | 'all'): Promise<void>;
|
|
270
358
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
359
|
+
invalidateByPrefix(prefix: string): Promise<void>;
|
|
360
|
+
/**
|
|
361
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
362
|
+
*/
|
|
363
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
271
364
|
wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
|
|
272
365
|
warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
|
|
273
366
|
getMetrics(): CacheMetricsSnapshot;
|
|
274
367
|
getHitRate(): CacheHitRateSnapshot;
|
|
368
|
+
/**
|
|
369
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
370
|
+
*
|
|
371
|
+
* ```ts
|
|
372
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
373
|
+
* const posts = tenant.namespace('posts')
|
|
374
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
namespace(childPrefix: string): CacheNamespace;
|
|
275
378
|
qualify(key: string): string;
|
|
379
|
+
private trackMetrics;
|
|
380
|
+
private getMetricsMutex;
|
|
276
381
|
}
|
|
277
382
|
|
|
278
383
|
/** Typed overloads for EventEmitter so callers get autocomplete on event names. */
|
|
@@ -280,6 +385,9 @@ interface CacheStack {
|
|
|
280
385
|
on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
281
386
|
once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
282
387
|
off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
388
|
+
removeAllListeners<K extends keyof CacheStackEvents>(event?: K): this;
|
|
389
|
+
listeners<K extends keyof CacheStackEvents>(event: K): Array<(data: CacheStackEvents[K]) => void>;
|
|
390
|
+
listenerCount<K extends keyof CacheStackEvents>(event: K): number;
|
|
283
391
|
emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
|
|
284
392
|
}
|
|
285
393
|
declare class CacheStack extends EventEmitter {
|
|
@@ -292,10 +400,15 @@ declare class CacheStack extends EventEmitter {
|
|
|
292
400
|
private unsubscribeInvalidation?;
|
|
293
401
|
private readonly logger;
|
|
294
402
|
private readonly tagIndex;
|
|
403
|
+
private readonly fetchRateLimiter;
|
|
295
404
|
private readonly backgroundRefreshes;
|
|
296
405
|
private readonly layerDegradedUntil;
|
|
297
406
|
private readonly ttlResolver;
|
|
298
407
|
private readonly circuitBreakerManager;
|
|
408
|
+
private currentGeneration?;
|
|
409
|
+
private readonly writeBehindQueue;
|
|
410
|
+
private writeBehindTimer?;
|
|
411
|
+
private writeBehindFlushPromise?;
|
|
299
412
|
private isDisconnecting;
|
|
300
413
|
private disconnectPromise?;
|
|
301
414
|
constructor(layers: CacheLayer[], options?: CacheStackOptions);
|
|
@@ -311,6 +424,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
311
424
|
* Fetches and caches the value if not already present.
|
|
312
425
|
*/
|
|
313
426
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
427
|
+
/**
|
|
428
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
429
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
430
|
+
* return non-null.
|
|
431
|
+
*/
|
|
432
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
314
433
|
/**
|
|
315
434
|
* Returns true if the given key exists and is not expired in any layer.
|
|
316
435
|
*/
|
|
@@ -347,7 +466,9 @@ declare class CacheStack extends EventEmitter {
|
|
|
347
466
|
*/
|
|
348
467
|
namespace(prefix: string): CacheNamespace;
|
|
349
468
|
invalidateByTag(tag: string): Promise<void>;
|
|
469
|
+
invalidateByTags(tags: string[], mode?: 'any' | 'all'): Promise<void>;
|
|
350
470
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
471
|
+
invalidateByPrefix(prefix: string): Promise<void>;
|
|
351
472
|
getMetrics(): CacheMetricsSnapshot;
|
|
352
473
|
getStats(): CacheStatsSnapshot;
|
|
353
474
|
resetMetrics(): void;
|
|
@@ -355,6 +476,14 @@ declare class CacheStack extends EventEmitter {
|
|
|
355
476
|
* Returns computed hit-rate statistics (overall and per-layer).
|
|
356
477
|
*/
|
|
357
478
|
getHitRate(): CacheHitRateSnapshot;
|
|
479
|
+
healthCheck(): Promise<CacheHealthCheckResult[]>;
|
|
480
|
+
bumpGeneration(nextGeneration?: number): number;
|
|
481
|
+
/**
|
|
482
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
483
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
484
|
+
* Returns `null` if the key does not exist in any layer.
|
|
485
|
+
*/
|
|
486
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
358
487
|
exportState(): Promise<CacheSnapshotEntry[]>;
|
|
359
488
|
importState(entries: CacheSnapshotEntry[]): Promise<void>;
|
|
360
489
|
persistToFile(filePath: string): Promise<void>;
|
|
@@ -365,6 +494,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
365
494
|
private waitForFreshValue;
|
|
366
495
|
private fetchAndPopulate;
|
|
367
496
|
private storeEntry;
|
|
497
|
+
private writeBatch;
|
|
368
498
|
private readFromLayers;
|
|
369
499
|
private readLayerEntry;
|
|
370
500
|
private backfill;
|
|
@@ -378,9 +508,20 @@ declare class CacheStack extends EventEmitter {
|
|
|
378
508
|
private deleteKeys;
|
|
379
509
|
private publishInvalidation;
|
|
380
510
|
private handleInvalidationMessage;
|
|
511
|
+
private getTagsForKey;
|
|
381
512
|
private formatError;
|
|
382
513
|
private sleep;
|
|
383
514
|
private shouldBroadcastL1Invalidation;
|
|
515
|
+
private initializeWriteBehind;
|
|
516
|
+
private shouldWriteBehind;
|
|
517
|
+
private enqueueWriteBehind;
|
|
518
|
+
private flushWriteBehindQueue;
|
|
519
|
+
private buildLayerSetEntry;
|
|
520
|
+
private intersectKeys;
|
|
521
|
+
private qualifyKey;
|
|
522
|
+
private qualifyPattern;
|
|
523
|
+
private stripQualifiedKey;
|
|
524
|
+
private generationPrefix;
|
|
384
525
|
private deleteKeysFromLayers;
|
|
385
526
|
private validateConfiguration;
|
|
386
527
|
private validateWriteOptions;
|
|
@@ -388,6 +529,9 @@ declare class CacheStack extends EventEmitter {
|
|
|
388
529
|
private validatePositiveNumber;
|
|
389
530
|
private validateNonNegativeNumber;
|
|
390
531
|
private validateCacheKey;
|
|
532
|
+
private validateTtlPolicy;
|
|
533
|
+
private assertActive;
|
|
534
|
+
private awaitStartup;
|
|
391
535
|
private serializeOptions;
|
|
392
536
|
private validateAdaptiveTtlOptions;
|
|
393
537
|
private validateCircuitBreakerOptions;
|
|
@@ -413,9 +557,31 @@ interface CacheStackModuleOptions {
|
|
|
413
557
|
layers: CacheLayer[];
|
|
414
558
|
bridgeOptions?: CacheStackOptions;
|
|
415
559
|
}
|
|
560
|
+
interface CacheStackModuleAsyncOptions {
|
|
561
|
+
/**
|
|
562
|
+
* Tokens to inject into the `useFactory` function.
|
|
563
|
+
*/
|
|
564
|
+
inject?: Array<Type | string | symbol>;
|
|
565
|
+
/**
|
|
566
|
+
* Async factory function that returns `CacheStackModuleOptions`.
|
|
567
|
+
* Useful when the Redis client or other dependencies must be resolved
|
|
568
|
+
* from the NestJS DI container first.
|
|
569
|
+
*
|
|
570
|
+
* ```ts
|
|
571
|
+
* CacheStackModule.forRootAsync({
|
|
572
|
+
* inject: [ConfigService],
|
|
573
|
+
* useFactory: (config: ConfigService) => ({
|
|
574
|
+
* layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
|
|
575
|
+
* })
|
|
576
|
+
* })
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
|
|
580
|
+
}
|
|
416
581
|
declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
|
|
417
582
|
declare class CacheStackModule {
|
|
418
583
|
static forRoot(options: CacheStackModuleOptions): DynamicModule;
|
|
584
|
+
static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
|
|
419
585
|
}
|
|
420
586
|
|
|
421
587
|
export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { DynamicModule, Type } from '@nestjs/common';
|
|
3
3
|
|
|
4
4
|
declare const CACHE_STACK: unique symbol;
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ interface LayerTtlMap {
|
|
|
9
9
|
interface CacheWriteOptions {
|
|
10
10
|
tags?: string[];
|
|
11
11
|
ttl?: number | LayerTtlMap;
|
|
12
|
+
ttlPolicy?: CacheTtlPolicy;
|
|
12
13
|
negativeCache?: boolean;
|
|
13
14
|
negativeTtl?: number | LayerTtlMap;
|
|
14
15
|
staleWhileRevalidate?: number | LayerTtlMap;
|
|
@@ -18,6 +19,16 @@ interface CacheWriteOptions {
|
|
|
18
19
|
refreshAhead?: number | LayerTtlMap;
|
|
19
20
|
adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
|
|
20
21
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
22
|
+
fetcherRateLimit?: CacheRateLimitOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Optional predicate called with the fetcher's return value before caching.
|
|
25
|
+
* Return `false` to skip storing the value in the cache (but still return it
|
|
26
|
+
* to the caller). Useful for not caching failed API responses or empty results.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
|
|
30
|
+
*/
|
|
31
|
+
shouldCache?: (value: unknown) => boolean;
|
|
21
32
|
}
|
|
22
33
|
interface CacheGetOptions extends CacheWriteOptions {
|
|
23
34
|
}
|
|
@@ -38,12 +49,20 @@ interface CacheLayer {
|
|
|
38
49
|
readonly isLocal?: boolean;
|
|
39
50
|
get<T>(key: string): Promise<T | null>;
|
|
40
51
|
getEntry?<T = unknown>(key: string): Promise<T | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Bulk read fast-path. Implementations should return raw stored entries using
|
|
54
|
+
* the same semantics as `getEntry()` so CacheStack can resolve envelopes,
|
|
55
|
+
* stale windows, and negative-cache markers consistently.
|
|
56
|
+
*/
|
|
41
57
|
getMany?<T>(keys: string[]): Promise<Array<T | null>>;
|
|
58
|
+
setMany?(entries: CacheLayerSetManyEntry[]): Promise<void>;
|
|
42
59
|
set(key: string, value: unknown, ttl?: number): Promise<void>;
|
|
43
60
|
delete(key: string): Promise<void>;
|
|
44
61
|
clear(): Promise<void>;
|
|
45
62
|
deleteMany?(keys: string[]): Promise<void>;
|
|
46
63
|
keys?(): Promise<string[]>;
|
|
64
|
+
ping?(): Promise<boolean>;
|
|
65
|
+
dispose?(): Promise<void>;
|
|
47
66
|
/**
|
|
48
67
|
* Returns true if the key exists and has not expired.
|
|
49
68
|
* Implementations may omit this; CacheStack will fall back to `get()`.
|
|
@@ -61,6 +80,15 @@ interface CacheLayer {
|
|
|
61
80
|
*/
|
|
62
81
|
size?(): Promise<number>;
|
|
63
82
|
}
|
|
83
|
+
/** Per-layer latency statistics (rolling window of sampled read durations). */
|
|
84
|
+
interface CacheLayerLatency {
|
|
85
|
+
/** Average read latency in milliseconds. */
|
|
86
|
+
avgMs: number;
|
|
87
|
+
/** Maximum observed read latency in milliseconds. */
|
|
88
|
+
maxMs: number;
|
|
89
|
+
/** Number of samples used to compute the statistics. */
|
|
90
|
+
count: number;
|
|
91
|
+
}
|
|
64
92
|
/** Snapshot of cumulative cache counters. */
|
|
65
93
|
interface CacheMetricsSnapshot {
|
|
66
94
|
hits: number;
|
|
@@ -80,6 +108,8 @@ interface CacheMetricsSnapshot {
|
|
|
80
108
|
degradedOperations: number;
|
|
81
109
|
hitsByLayer: Record<string, number>;
|
|
82
110
|
missesByLayer: Record<string, number>;
|
|
111
|
+
/** Per-layer read latency statistics (sampled from successful reads). */
|
|
112
|
+
latencyByLayer: Record<string, CacheLayerLatency>;
|
|
83
113
|
/** Timestamp (ms since epoch) when metrics were last reset. */
|
|
84
114
|
resetAt: number;
|
|
85
115
|
}
|
|
@@ -101,9 +131,17 @@ interface CacheTagIndex {
|
|
|
101
131
|
track(key: string, tags: string[]): Promise<void>;
|
|
102
132
|
remove(key: string): Promise<void>;
|
|
103
133
|
keysForTag(tag: string): Promise<string[]>;
|
|
134
|
+
keysForPrefix?(prefix: string): Promise<string[]>;
|
|
135
|
+
/** Returns the tags associated with a specific key, or an empty array. */
|
|
136
|
+
tagsForKey?(key: string): Promise<string[]>;
|
|
104
137
|
matchPattern(pattern: string): Promise<string[]>;
|
|
105
138
|
clear(): Promise<void>;
|
|
106
139
|
}
|
|
140
|
+
interface CacheLayerSetManyEntry {
|
|
141
|
+
key: string;
|
|
142
|
+
value: unknown;
|
|
143
|
+
ttl?: number;
|
|
144
|
+
}
|
|
107
145
|
interface InvalidationMessage {
|
|
108
146
|
scope: 'key' | 'keys' | 'clear';
|
|
109
147
|
sourceId: string;
|
|
@@ -128,6 +166,7 @@ interface CacheStackOptions {
|
|
|
128
166
|
stampedePrevention?: boolean;
|
|
129
167
|
invalidationBus?: InvalidationBus;
|
|
130
168
|
tagIndex?: CacheTagIndex;
|
|
169
|
+
generation?: number;
|
|
131
170
|
broadcastL1Invalidation?: boolean;
|
|
132
171
|
/**
|
|
133
172
|
* @deprecated Use `broadcastL1Invalidation` instead.
|
|
@@ -143,6 +182,9 @@ interface CacheStackOptions {
|
|
|
143
182
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
144
183
|
gracefulDegradation?: boolean | CacheDegradationOptions;
|
|
145
184
|
writePolicy?: 'strict' | 'best-effort';
|
|
185
|
+
writeStrategy?: 'write-through' | 'write-behind';
|
|
186
|
+
writeBehind?: CacheWriteBehindOptions;
|
|
187
|
+
fetcherRateLimit?: CacheRateLimitOptions;
|
|
146
188
|
singleFlightCoordinator?: CacheSingleFlightCoordinator;
|
|
147
189
|
singleFlightLeaseMs?: number;
|
|
148
190
|
singleFlightTimeoutMs?: number;
|
|
@@ -159,6 +201,13 @@ interface CacheAdaptiveTtlOptions {
|
|
|
159
201
|
step?: number | LayerTtlMap;
|
|
160
202
|
maxTtl?: number | LayerTtlMap;
|
|
161
203
|
}
|
|
204
|
+
type CacheTtlPolicy = 'until-midnight' | 'next-hour' | {
|
|
205
|
+
alignTo: number;
|
|
206
|
+
} | ((context: CacheTtlPolicyContext) => number | undefined);
|
|
207
|
+
interface CacheTtlPolicyContext {
|
|
208
|
+
key: string;
|
|
209
|
+
value: unknown;
|
|
210
|
+
}
|
|
162
211
|
interface CacheCircuitBreakerOptions {
|
|
163
212
|
failureThreshold?: number;
|
|
164
213
|
cooldownMs?: number;
|
|
@@ -166,6 +215,16 @@ interface CacheCircuitBreakerOptions {
|
|
|
166
215
|
interface CacheDegradationOptions {
|
|
167
216
|
retryAfterMs?: number;
|
|
168
217
|
}
|
|
218
|
+
interface CacheRateLimitOptions {
|
|
219
|
+
maxConcurrent?: number;
|
|
220
|
+
intervalMs?: number;
|
|
221
|
+
maxPerInterval?: number;
|
|
222
|
+
}
|
|
223
|
+
interface CacheWriteBehindOptions {
|
|
224
|
+
flushIntervalMs?: number;
|
|
225
|
+
batchSize?: number;
|
|
226
|
+
maxQueueSize?: number;
|
|
227
|
+
}
|
|
169
228
|
interface CacheWarmEntry<T = unknown> {
|
|
170
229
|
key: string;
|
|
171
230
|
fetcher: () => Promise<T>;
|
|
@@ -203,6 +262,28 @@ interface CacheStatsSnapshot {
|
|
|
203
262
|
}>;
|
|
204
263
|
backgroundRefreshes: number;
|
|
205
264
|
}
|
|
265
|
+
interface CacheHealthCheckResult {
|
|
266
|
+
layer: string;
|
|
267
|
+
healthy: boolean;
|
|
268
|
+
latencyMs: number;
|
|
269
|
+
error?: string;
|
|
270
|
+
}
|
|
271
|
+
/** Detailed inspection result for a single cache key. */
|
|
272
|
+
interface CacheInspectResult {
|
|
273
|
+
key: string;
|
|
274
|
+
/** Layers in which the key is currently stored (not expired). */
|
|
275
|
+
foundInLayers: string[];
|
|
276
|
+
/** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
|
|
277
|
+
freshTtlSeconds: number | null;
|
|
278
|
+
/** Remaining stale-while-revalidate window in seconds, or null. */
|
|
279
|
+
staleTtlSeconds: number | null;
|
|
280
|
+
/** Remaining stale-if-error window in seconds, or null. */
|
|
281
|
+
errorTtlSeconds: number | null;
|
|
282
|
+
/** Whether the key is currently serving stale-while-revalidate. */
|
|
283
|
+
isStale: boolean;
|
|
284
|
+
/** Tags associated with this key (from the TagIndex). */
|
|
285
|
+
tags: string[];
|
|
286
|
+
}
|
|
206
287
|
/** All events emitted by CacheStack and their payload shapes. */
|
|
207
288
|
interface CacheStackEvents {
|
|
208
289
|
/** Fired on every cache hit. */
|
|
@@ -255,9 +336,15 @@ interface CacheStackEvents {
|
|
|
255
336
|
declare class CacheNamespace {
|
|
256
337
|
private readonly cache;
|
|
257
338
|
private readonly prefix;
|
|
339
|
+
private static readonly metricsMutexes;
|
|
340
|
+
private metrics;
|
|
258
341
|
constructor(cache: CacheStack, prefix: string);
|
|
259
342
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
260
343
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
344
|
+
/**
|
|
345
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
346
|
+
*/
|
|
347
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
261
348
|
has(key: string): Promise<boolean>;
|
|
262
349
|
ttl(key: string): Promise<number | null>;
|
|
263
350
|
set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
|
|
@@ -267,12 +354,30 @@ declare class CacheNamespace {
|
|
|
267
354
|
mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
|
|
268
355
|
mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
|
|
269
356
|
invalidateByTag(tag: string): Promise<void>;
|
|
357
|
+
invalidateByTags(tags: string[], mode?: 'any' | 'all'): Promise<void>;
|
|
270
358
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
359
|
+
invalidateByPrefix(prefix: string): Promise<void>;
|
|
360
|
+
/**
|
|
361
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
362
|
+
*/
|
|
363
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
271
364
|
wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
|
|
272
365
|
warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
|
|
273
366
|
getMetrics(): CacheMetricsSnapshot;
|
|
274
367
|
getHitRate(): CacheHitRateSnapshot;
|
|
368
|
+
/**
|
|
369
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
370
|
+
*
|
|
371
|
+
* ```ts
|
|
372
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
373
|
+
* const posts = tenant.namespace('posts')
|
|
374
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
namespace(childPrefix: string): CacheNamespace;
|
|
275
378
|
qualify(key: string): string;
|
|
379
|
+
private trackMetrics;
|
|
380
|
+
private getMetricsMutex;
|
|
276
381
|
}
|
|
277
382
|
|
|
278
383
|
/** Typed overloads for EventEmitter so callers get autocomplete on event names. */
|
|
@@ -280,6 +385,9 @@ interface CacheStack {
|
|
|
280
385
|
on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
281
386
|
once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
282
387
|
off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
|
|
388
|
+
removeAllListeners<K extends keyof CacheStackEvents>(event?: K): this;
|
|
389
|
+
listeners<K extends keyof CacheStackEvents>(event: K): Array<(data: CacheStackEvents[K]) => void>;
|
|
390
|
+
listenerCount<K extends keyof CacheStackEvents>(event: K): number;
|
|
283
391
|
emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
|
|
284
392
|
}
|
|
285
393
|
declare class CacheStack extends EventEmitter {
|
|
@@ -292,10 +400,15 @@ declare class CacheStack extends EventEmitter {
|
|
|
292
400
|
private unsubscribeInvalidation?;
|
|
293
401
|
private readonly logger;
|
|
294
402
|
private readonly tagIndex;
|
|
403
|
+
private readonly fetchRateLimiter;
|
|
295
404
|
private readonly backgroundRefreshes;
|
|
296
405
|
private readonly layerDegradedUntil;
|
|
297
406
|
private readonly ttlResolver;
|
|
298
407
|
private readonly circuitBreakerManager;
|
|
408
|
+
private currentGeneration?;
|
|
409
|
+
private readonly writeBehindQueue;
|
|
410
|
+
private writeBehindTimer?;
|
|
411
|
+
private writeBehindFlushPromise?;
|
|
299
412
|
private isDisconnecting;
|
|
300
413
|
private disconnectPromise?;
|
|
301
414
|
constructor(layers: CacheLayer[], options?: CacheStackOptions);
|
|
@@ -311,6 +424,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
311
424
|
* Fetches and caches the value if not already present.
|
|
312
425
|
*/
|
|
313
426
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
427
|
+
/**
|
|
428
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
429
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
430
|
+
* return non-null.
|
|
431
|
+
*/
|
|
432
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
314
433
|
/**
|
|
315
434
|
* Returns true if the given key exists and is not expired in any layer.
|
|
316
435
|
*/
|
|
@@ -347,7 +466,9 @@ declare class CacheStack extends EventEmitter {
|
|
|
347
466
|
*/
|
|
348
467
|
namespace(prefix: string): CacheNamespace;
|
|
349
468
|
invalidateByTag(tag: string): Promise<void>;
|
|
469
|
+
invalidateByTags(tags: string[], mode?: 'any' | 'all'): Promise<void>;
|
|
350
470
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
471
|
+
invalidateByPrefix(prefix: string): Promise<void>;
|
|
351
472
|
getMetrics(): CacheMetricsSnapshot;
|
|
352
473
|
getStats(): CacheStatsSnapshot;
|
|
353
474
|
resetMetrics(): void;
|
|
@@ -355,6 +476,14 @@ declare class CacheStack extends EventEmitter {
|
|
|
355
476
|
* Returns computed hit-rate statistics (overall and per-layer).
|
|
356
477
|
*/
|
|
357
478
|
getHitRate(): CacheHitRateSnapshot;
|
|
479
|
+
healthCheck(): Promise<CacheHealthCheckResult[]>;
|
|
480
|
+
bumpGeneration(nextGeneration?: number): number;
|
|
481
|
+
/**
|
|
482
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
483
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
484
|
+
* Returns `null` if the key does not exist in any layer.
|
|
485
|
+
*/
|
|
486
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
358
487
|
exportState(): Promise<CacheSnapshotEntry[]>;
|
|
359
488
|
importState(entries: CacheSnapshotEntry[]): Promise<void>;
|
|
360
489
|
persistToFile(filePath: string): Promise<void>;
|
|
@@ -365,6 +494,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
365
494
|
private waitForFreshValue;
|
|
366
495
|
private fetchAndPopulate;
|
|
367
496
|
private storeEntry;
|
|
497
|
+
private writeBatch;
|
|
368
498
|
private readFromLayers;
|
|
369
499
|
private readLayerEntry;
|
|
370
500
|
private backfill;
|
|
@@ -378,9 +508,20 @@ declare class CacheStack extends EventEmitter {
|
|
|
378
508
|
private deleteKeys;
|
|
379
509
|
private publishInvalidation;
|
|
380
510
|
private handleInvalidationMessage;
|
|
511
|
+
private getTagsForKey;
|
|
381
512
|
private formatError;
|
|
382
513
|
private sleep;
|
|
383
514
|
private shouldBroadcastL1Invalidation;
|
|
515
|
+
private initializeWriteBehind;
|
|
516
|
+
private shouldWriteBehind;
|
|
517
|
+
private enqueueWriteBehind;
|
|
518
|
+
private flushWriteBehindQueue;
|
|
519
|
+
private buildLayerSetEntry;
|
|
520
|
+
private intersectKeys;
|
|
521
|
+
private qualifyKey;
|
|
522
|
+
private qualifyPattern;
|
|
523
|
+
private stripQualifiedKey;
|
|
524
|
+
private generationPrefix;
|
|
384
525
|
private deleteKeysFromLayers;
|
|
385
526
|
private validateConfiguration;
|
|
386
527
|
private validateWriteOptions;
|
|
@@ -388,6 +529,9 @@ declare class CacheStack extends EventEmitter {
|
|
|
388
529
|
private validatePositiveNumber;
|
|
389
530
|
private validateNonNegativeNumber;
|
|
390
531
|
private validateCacheKey;
|
|
532
|
+
private validateTtlPolicy;
|
|
533
|
+
private assertActive;
|
|
534
|
+
private awaitStartup;
|
|
391
535
|
private serializeOptions;
|
|
392
536
|
private validateAdaptiveTtlOptions;
|
|
393
537
|
private validateCircuitBreakerOptions;
|
|
@@ -413,9 +557,31 @@ interface CacheStackModuleOptions {
|
|
|
413
557
|
layers: CacheLayer[];
|
|
414
558
|
bridgeOptions?: CacheStackOptions;
|
|
415
559
|
}
|
|
560
|
+
interface CacheStackModuleAsyncOptions {
|
|
561
|
+
/**
|
|
562
|
+
* Tokens to inject into the `useFactory` function.
|
|
563
|
+
*/
|
|
564
|
+
inject?: Array<Type | string | symbol>;
|
|
565
|
+
/**
|
|
566
|
+
* Async factory function that returns `CacheStackModuleOptions`.
|
|
567
|
+
* Useful when the Redis client or other dependencies must be resolved
|
|
568
|
+
* from the NestJS DI container first.
|
|
569
|
+
*
|
|
570
|
+
* ```ts
|
|
571
|
+
* CacheStackModule.forRootAsync({
|
|
572
|
+
* inject: [ConfigService],
|
|
573
|
+
* useFactory: (config: ConfigService) => ({
|
|
574
|
+
* layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
|
|
575
|
+
* })
|
|
576
|
+
* })
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
|
|
580
|
+
}
|
|
416
581
|
declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
|
|
417
582
|
declare class CacheStackModule {
|
|
418
583
|
static forRoot(options: CacheStackModuleOptions): DynamicModule;
|
|
584
|
+
static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
|
|
419
585
|
}
|
|
420
586
|
|
|
421
587
|
export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };
|