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.
@@ -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 };