layercache 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layercache",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Unified multi-layer caching for Node.js with memory, Redis, stampede prevention, and invalidation helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,7 @@ var import_node_events = require("events");
77
77
  var import_node_fs = require("fs");
78
78
 
79
79
  // ../../src/CacheNamespace.ts
80
- var CacheNamespace = class {
80
+ var CacheNamespace = class _CacheNamespace {
81
81
  constructor(cache, prefix) {
82
82
  this.cache = cache;
83
83
  this.prefix = prefix;
@@ -90,6 +90,12 @@ var CacheNamespace = class {
90
90
  async getOrSet(key, fetcher, options) {
91
91
  return this.cache.getOrSet(this.qualify(key), fetcher, options);
92
92
  }
93
+ /**
94
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
95
+ */
96
+ async getOrThrow(key, fetcher, options) {
97
+ return this.cache.getOrThrow(this.qualify(key), fetcher, options);
98
+ }
93
99
  async has(key) {
94
100
  return this.cache.has(this.qualify(key));
95
101
  }
@@ -130,6 +136,12 @@ var CacheNamespace = class {
130
136
  async invalidateByPattern(pattern) {
131
137
  await this.cache.invalidateByPattern(this.qualify(pattern));
132
138
  }
139
+ /**
140
+ * Returns detailed metadata about a single cache key within this namespace.
141
+ */
142
+ async inspect(key) {
143
+ return this.cache.inspect(this.qualify(key));
144
+ }
133
145
  wrap(keyPrefix, fetcher, options) {
134
146
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
135
147
  }
@@ -148,6 +160,18 @@ var CacheNamespace = class {
148
160
  getHitRate() {
149
161
  return this.cache.getHitRate();
150
162
  }
163
+ /**
164
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
165
+ *
166
+ * ```ts
167
+ * const tenant = cache.namespace('tenant:abc')
168
+ * const posts = tenant.namespace('posts')
169
+ * // keys become: "tenant:abc:posts:mykey"
170
+ * ```
171
+ */
172
+ namespace(childPrefix) {
173
+ return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
174
+ }
151
175
  qualify(key) {
152
176
  return `${this.prefix}:${key}`;
153
177
  }
@@ -249,7 +273,12 @@ var CircuitBreakerManager = class {
249
273
  var MetricsCollector = class {
250
274
  data = this.empty();
251
275
  get snapshot() {
252
- return { ...this.data };
276
+ return {
277
+ ...this.data,
278
+ hitsByLayer: { ...this.data.hitsByLayer },
279
+ missesByLayer: { ...this.data.missesByLayer },
280
+ latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
281
+ };
253
282
  }
254
283
  increment(field, amount = 1) {
255
284
  ;
@@ -258,6 +287,22 @@ var MetricsCollector = class {
258
287
  incrementLayer(map, layerName) {
259
288
  this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
260
289
  }
290
+ /**
291
+ * Records a read latency sample for the given layer.
292
+ * Maintains a rolling average and max using Welford's online algorithm.
293
+ */
294
+ recordLatency(layerName, durationMs) {
295
+ const existing = this.data.latencyByLayer[layerName];
296
+ if (!existing) {
297
+ this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
298
+ return;
299
+ }
300
+ existing.count += 1;
301
+ existing.avgMs += (durationMs - existing.avgMs) / existing.count;
302
+ if (durationMs > existing.maxMs) {
303
+ existing.maxMs = durationMs;
304
+ }
305
+ }
261
306
  reset() {
262
307
  this.data = this.empty();
263
308
  }
@@ -292,6 +337,7 @@ var MetricsCollector = class {
292
337
  degradedOperations: 0,
293
338
  hitsByLayer: {},
294
339
  missesByLayer: {},
340
+ latencyByLayer: {},
295
341
  resetAt: Date.now()
296
342
  };
297
343
  }
@@ -529,11 +575,17 @@ var TagIndex = class {
529
575
  tagToKeys = /* @__PURE__ */ new Map();
530
576
  keyToTags = /* @__PURE__ */ new Map();
531
577
  knownKeys = /* @__PURE__ */ new Set();
578
+ maxKnownKeys;
579
+ constructor(options = {}) {
580
+ this.maxKnownKeys = options.maxKnownKeys;
581
+ }
532
582
  async touch(key) {
533
583
  this.knownKeys.add(key);
584
+ this.pruneKnownKeysIfNeeded();
534
585
  }
535
586
  async track(key, tags) {
536
587
  this.knownKeys.add(key);
588
+ this.pruneKnownKeysIfNeeded();
537
589
  if (tags.length === 0) {
538
590
  return;
539
591
  }
@@ -572,6 +624,9 @@ var TagIndex = class {
572
624
  async keysForTag(tag) {
573
625
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
574
626
  }
627
+ async tagsForKey(key) {
628
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
629
+ }
575
630
  async matchPattern(pattern) {
576
631
  return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
577
632
  }
@@ -580,6 +635,21 @@ var TagIndex = class {
580
635
  this.keyToTags.clear();
581
636
  this.knownKeys.clear();
582
637
  }
638
+ pruneKnownKeysIfNeeded() {
639
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
640
+ return;
641
+ }
642
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
643
+ let removed = 0;
644
+ for (const key of this.knownKeys) {
645
+ if (removed >= toRemove) {
646
+ break;
647
+ }
648
+ this.knownKeys.delete(key);
649
+ this.keyToTags.delete(key);
650
+ removed += 1;
651
+ }
652
+ }
583
653
  };
584
654
 
585
655
  // ../../node_modules/async-mutex/index.mjs
@@ -782,6 +852,16 @@ var StampedeGuard = class {
782
852
  }
783
853
  };
784
854
 
855
+ // ../../src/types.ts
856
+ var CacheMissError = class extends Error {
857
+ key;
858
+ constructor(key) {
859
+ super(`Cache miss for key "${key}".`);
860
+ this.name = "CacheMissError";
861
+ this.key = key;
862
+ }
863
+ };
864
+
785
865
  // ../../src/CacheStack.ts
786
866
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
787
867
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
@@ -908,6 +988,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
908
988
  async getOrSet(key, fetcher, options) {
909
989
  return this.get(key, fetcher, options);
910
990
  }
991
+ /**
992
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
993
+ * Useful when the value is expected to exist or the fetcher is expected to
994
+ * return non-null.
995
+ */
996
+ async getOrThrow(key, fetcher, options) {
997
+ const value = await this.get(key, fetcher, options);
998
+ if (value === null) {
999
+ throw new CacheMissError(key);
1000
+ }
1001
+ return value;
1002
+ }
911
1003
  /**
912
1004
  * Returns true if the given key exists and is not expired in any layer.
913
1005
  */
@@ -1181,6 +1273,46 @@ var CacheStack = class extends import_node_events.EventEmitter {
1181
1273
  getHitRate() {
1182
1274
  return this.metricsCollector.hitRate();
1183
1275
  }
1276
+ /**
1277
+ * Returns detailed metadata about a single cache key: which layers contain it,
1278
+ * remaining fresh/stale/error TTLs, and associated tags.
1279
+ * Returns `null` if the key does not exist in any layer.
1280
+ */
1281
+ async inspect(key) {
1282
+ const normalizedKey = this.validateCacheKey(key);
1283
+ await this.startup;
1284
+ const foundInLayers = [];
1285
+ let freshTtlSeconds = null;
1286
+ let staleTtlSeconds = null;
1287
+ let errorTtlSeconds = null;
1288
+ let isStale = false;
1289
+ for (const layer of this.layers) {
1290
+ if (this.shouldSkipLayer(layer)) {
1291
+ continue;
1292
+ }
1293
+ const stored = await this.readLayerEntry(layer, normalizedKey);
1294
+ if (stored === null) {
1295
+ continue;
1296
+ }
1297
+ const resolved = resolveStoredValue(stored);
1298
+ if (resolved.state === "expired") {
1299
+ continue;
1300
+ }
1301
+ foundInLayers.push(layer.name);
1302
+ if (foundInLayers.length === 1 && resolved.envelope) {
1303
+ const now = Date.now();
1304
+ freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
1305
+ staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
1306
+ errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
1307
+ isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
1308
+ }
1309
+ }
1310
+ if (foundInLayers.length === 0) {
1311
+ return null;
1312
+ }
1313
+ const tags = await this.getTagsForKey(normalizedKey);
1314
+ return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1315
+ }
1184
1316
  async exportState() {
1185
1317
  await this.startup;
1186
1318
  const exported = /* @__PURE__ */ new Map();
@@ -1317,6 +1449,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1317
1449
  await this.storeEntry(key, "empty", null, options);
1318
1450
  return null;
1319
1451
  }
1452
+ if (options?.shouldCache && !options.shouldCache(fetched)) {
1453
+ return fetched;
1454
+ }
1320
1455
  await this.storeEntry(key, "value", fetched, options);
1321
1456
  return fetched;
1322
1457
  }
@@ -1339,7 +1474,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1339
1474
  for (let index = 0; index < this.layers.length; index += 1) {
1340
1475
  const layer = this.layers[index];
1341
1476
  if (!layer) continue;
1477
+ const readStart = performance.now();
1342
1478
  const stored = await this.readLayerEntry(layer, key);
1479
+ const readDuration = performance.now() - readStart;
1480
+ this.metricsCollector.recordLatency(layer.name, readDuration);
1343
1481
  if (stored === null) {
1344
1482
  this.metricsCollector.incrementLayer("missesByLayer", layer.name);
1345
1483
  continue;
@@ -1541,6 +1679,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
1541
1679
  }
1542
1680
  }
1543
1681
  }
1682
+ async getTagsForKey(key) {
1683
+ if (this.tagIndex.tagsForKey) {
1684
+ return this.tagIndex.tagsForKey(key);
1685
+ }
1686
+ return [];
1687
+ }
1544
1688
  formatError(error) {
1545
1689
  if (error instanceof Error) {
1546
1690
  return error.message;
@@ -1771,6 +1915,22 @@ var CacheStackModule = class {
1771
1915
  exports: [provider]
1772
1916
  };
1773
1917
  }
1918
+ static forRootAsync(options) {
1919
+ const provider = {
1920
+ provide: CACHE_STACK,
1921
+ inject: options.inject ?? [],
1922
+ useFactory: async (...args) => {
1923
+ const resolved = await options.useFactory(...args);
1924
+ return new CacheStack(resolved.layers, resolved.bridgeOptions);
1925
+ }
1926
+ };
1927
+ return {
1928
+ global: true,
1929
+ module: CacheStackModule,
1930
+ providers: [provider],
1931
+ exports: [provider]
1932
+ };
1933
+ }
1774
1934
  };
1775
1935
  CacheStackModule = __decorateClass([
1776
1936
  (0, import_common.Global)(),
@@ -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
 
@@ -18,6 +18,15 @@ interface CacheWriteOptions {
18
18
  refreshAhead?: number | LayerTtlMap;
19
19
  adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
20
20
  circuitBreaker?: CacheCircuitBreakerOptions;
21
+ /**
22
+ * Optional predicate called with the fetcher's return value before caching.
23
+ * Return `false` to skip storing the value in the cache (but still return it
24
+ * to the caller). Useful for not caching failed API responses or empty results.
25
+ *
26
+ * @example
27
+ * cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
28
+ */
29
+ shouldCache?: (value: unknown) => boolean;
21
30
  }
22
31
  interface CacheGetOptions extends CacheWriteOptions {
23
32
  }
@@ -61,6 +70,15 @@ interface CacheLayer {
61
70
  */
62
71
  size?(): Promise<number>;
63
72
  }
73
+ /** Per-layer latency statistics (rolling window of sampled read durations). */
74
+ interface CacheLayerLatency {
75
+ /** Average read latency in milliseconds. */
76
+ avgMs: number;
77
+ /** Maximum observed read latency in milliseconds. */
78
+ maxMs: number;
79
+ /** Number of samples used to compute the statistics. */
80
+ count: number;
81
+ }
64
82
  /** Snapshot of cumulative cache counters. */
65
83
  interface CacheMetricsSnapshot {
66
84
  hits: number;
@@ -80,6 +98,8 @@ interface CacheMetricsSnapshot {
80
98
  degradedOperations: number;
81
99
  hitsByLayer: Record<string, number>;
82
100
  missesByLayer: Record<string, number>;
101
+ /** Per-layer read latency statistics (sampled from successful reads). */
102
+ latencyByLayer: Record<string, CacheLayerLatency>;
83
103
  /** Timestamp (ms since epoch) when metrics were last reset. */
84
104
  resetAt: number;
85
105
  }
@@ -101,6 +121,8 @@ interface CacheTagIndex {
101
121
  track(key: string, tags: string[]): Promise<void>;
102
122
  remove(key: string): Promise<void>;
103
123
  keysForTag(tag: string): Promise<string[]>;
124
+ /** Returns the tags associated with a specific key, or an empty array. */
125
+ tagsForKey?(key: string): Promise<string[]>;
104
126
  matchPattern(pattern: string): Promise<string[]>;
105
127
  clear(): Promise<void>;
106
128
  }
@@ -203,6 +225,22 @@ interface CacheStatsSnapshot {
203
225
  }>;
204
226
  backgroundRefreshes: number;
205
227
  }
228
+ /** Detailed inspection result for a single cache key. */
229
+ interface CacheInspectResult {
230
+ key: string;
231
+ /** Layers in which the key is currently stored (not expired). */
232
+ foundInLayers: string[];
233
+ /** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
234
+ freshTtlSeconds: number | null;
235
+ /** Remaining stale-while-revalidate window in seconds, or null. */
236
+ staleTtlSeconds: number | null;
237
+ /** Remaining stale-if-error window in seconds, or null. */
238
+ errorTtlSeconds: number | null;
239
+ /** Whether the key is currently serving stale-while-revalidate. */
240
+ isStale: boolean;
241
+ /** Tags associated with this key (from the TagIndex). */
242
+ tags: string[];
243
+ }
206
244
  /** All events emitted by CacheStack and their payload shapes. */
207
245
  interface CacheStackEvents {
208
246
  /** Fired on every cache hit. */
@@ -258,6 +296,10 @@ declare class CacheNamespace {
258
296
  constructor(cache: CacheStack, prefix: string);
259
297
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
260
298
  getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
299
+ /**
300
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
301
+ */
302
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
261
303
  has(key: string): Promise<boolean>;
262
304
  ttl(key: string): Promise<number | null>;
263
305
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
@@ -268,10 +310,24 @@ declare class CacheNamespace {
268
310
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
269
311
  invalidateByTag(tag: string): Promise<void>;
270
312
  invalidateByPattern(pattern: string): Promise<void>;
313
+ /**
314
+ * Returns detailed metadata about a single cache key within this namespace.
315
+ */
316
+ inspect(key: string): Promise<CacheInspectResult | null>;
271
317
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
272
318
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
273
319
  getMetrics(): CacheMetricsSnapshot;
274
320
  getHitRate(): CacheHitRateSnapshot;
321
+ /**
322
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
323
+ *
324
+ * ```ts
325
+ * const tenant = cache.namespace('tenant:abc')
326
+ * const posts = tenant.namespace('posts')
327
+ * // keys become: "tenant:abc:posts:mykey"
328
+ * ```
329
+ */
330
+ namespace(childPrefix: string): CacheNamespace;
275
331
  qualify(key: string): string;
276
332
  }
277
333
 
@@ -311,6 +367,12 @@ declare class CacheStack extends EventEmitter {
311
367
  * Fetches and caches the value if not already present.
312
368
  */
313
369
  getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
370
+ /**
371
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
372
+ * Useful when the value is expected to exist or the fetcher is expected to
373
+ * return non-null.
374
+ */
375
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
314
376
  /**
315
377
  * Returns true if the given key exists and is not expired in any layer.
316
378
  */
@@ -355,6 +417,12 @@ declare class CacheStack extends EventEmitter {
355
417
  * Returns computed hit-rate statistics (overall and per-layer).
356
418
  */
357
419
  getHitRate(): CacheHitRateSnapshot;
420
+ /**
421
+ * Returns detailed metadata about a single cache key: which layers contain it,
422
+ * remaining fresh/stale/error TTLs, and associated tags.
423
+ * Returns `null` if the key does not exist in any layer.
424
+ */
425
+ inspect(key: string): Promise<CacheInspectResult | null>;
358
426
  exportState(): Promise<CacheSnapshotEntry[]>;
359
427
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
360
428
  persistToFile(filePath: string): Promise<void>;
@@ -378,6 +446,7 @@ declare class CacheStack extends EventEmitter {
378
446
  private deleteKeys;
379
447
  private publishInvalidation;
380
448
  private handleInvalidationMessage;
449
+ private getTagsForKey;
381
450
  private formatError;
382
451
  private sleep;
383
452
  private shouldBroadcastL1Invalidation;
@@ -413,9 +482,31 @@ interface CacheStackModuleOptions {
413
482
  layers: CacheLayer[];
414
483
  bridgeOptions?: CacheStackOptions;
415
484
  }
485
+ interface CacheStackModuleAsyncOptions {
486
+ /**
487
+ * Tokens to inject into the `useFactory` function.
488
+ */
489
+ inject?: Array<Type | string | symbol>;
490
+ /**
491
+ * Async factory function that returns `CacheStackModuleOptions`.
492
+ * Useful when the Redis client or other dependencies must be resolved
493
+ * from the NestJS DI container first.
494
+ *
495
+ * ```ts
496
+ * CacheStackModule.forRootAsync({
497
+ * inject: [ConfigService],
498
+ * useFactory: (config: ConfigService) => ({
499
+ * layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
500
+ * })
501
+ * })
502
+ * ```
503
+ */
504
+ useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
505
+ }
416
506
  declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
417
507
  declare class CacheStackModule {
418
508
  static forRoot(options: CacheStackModuleOptions): DynamicModule;
509
+ static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
419
510
  }
420
511
 
421
512
  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
 
@@ -18,6 +18,15 @@ interface CacheWriteOptions {
18
18
  refreshAhead?: number | LayerTtlMap;
19
19
  adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
20
20
  circuitBreaker?: CacheCircuitBreakerOptions;
21
+ /**
22
+ * Optional predicate called with the fetcher's return value before caching.
23
+ * Return `false` to skip storing the value in the cache (but still return it
24
+ * to the caller). Useful for not caching failed API responses or empty results.
25
+ *
26
+ * @example
27
+ * cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
28
+ */
29
+ shouldCache?: (value: unknown) => boolean;
21
30
  }
22
31
  interface CacheGetOptions extends CacheWriteOptions {
23
32
  }
@@ -61,6 +70,15 @@ interface CacheLayer {
61
70
  */
62
71
  size?(): Promise<number>;
63
72
  }
73
+ /** Per-layer latency statistics (rolling window of sampled read durations). */
74
+ interface CacheLayerLatency {
75
+ /** Average read latency in milliseconds. */
76
+ avgMs: number;
77
+ /** Maximum observed read latency in milliseconds. */
78
+ maxMs: number;
79
+ /** Number of samples used to compute the statistics. */
80
+ count: number;
81
+ }
64
82
  /** Snapshot of cumulative cache counters. */
65
83
  interface CacheMetricsSnapshot {
66
84
  hits: number;
@@ -80,6 +98,8 @@ interface CacheMetricsSnapshot {
80
98
  degradedOperations: number;
81
99
  hitsByLayer: Record<string, number>;
82
100
  missesByLayer: Record<string, number>;
101
+ /** Per-layer read latency statistics (sampled from successful reads). */
102
+ latencyByLayer: Record<string, CacheLayerLatency>;
83
103
  /** Timestamp (ms since epoch) when metrics were last reset. */
84
104
  resetAt: number;
85
105
  }
@@ -101,6 +121,8 @@ interface CacheTagIndex {
101
121
  track(key: string, tags: string[]): Promise<void>;
102
122
  remove(key: string): Promise<void>;
103
123
  keysForTag(tag: string): Promise<string[]>;
124
+ /** Returns the tags associated with a specific key, or an empty array. */
125
+ tagsForKey?(key: string): Promise<string[]>;
104
126
  matchPattern(pattern: string): Promise<string[]>;
105
127
  clear(): Promise<void>;
106
128
  }
@@ -203,6 +225,22 @@ interface CacheStatsSnapshot {
203
225
  }>;
204
226
  backgroundRefreshes: number;
205
227
  }
228
+ /** Detailed inspection result for a single cache key. */
229
+ interface CacheInspectResult {
230
+ key: string;
231
+ /** Layers in which the key is currently stored (not expired). */
232
+ foundInLayers: string[];
233
+ /** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
234
+ freshTtlSeconds: number | null;
235
+ /** Remaining stale-while-revalidate window in seconds, or null. */
236
+ staleTtlSeconds: number | null;
237
+ /** Remaining stale-if-error window in seconds, or null. */
238
+ errorTtlSeconds: number | null;
239
+ /** Whether the key is currently serving stale-while-revalidate. */
240
+ isStale: boolean;
241
+ /** Tags associated with this key (from the TagIndex). */
242
+ tags: string[];
243
+ }
206
244
  /** All events emitted by CacheStack and their payload shapes. */
207
245
  interface CacheStackEvents {
208
246
  /** Fired on every cache hit. */
@@ -258,6 +296,10 @@ declare class CacheNamespace {
258
296
  constructor(cache: CacheStack, prefix: string);
259
297
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
260
298
  getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
299
+ /**
300
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
301
+ */
302
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
261
303
  has(key: string): Promise<boolean>;
262
304
  ttl(key: string): Promise<number | null>;
263
305
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
@@ -268,10 +310,24 @@ declare class CacheNamespace {
268
310
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
269
311
  invalidateByTag(tag: string): Promise<void>;
270
312
  invalidateByPattern(pattern: string): Promise<void>;
313
+ /**
314
+ * Returns detailed metadata about a single cache key within this namespace.
315
+ */
316
+ inspect(key: string): Promise<CacheInspectResult | null>;
271
317
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
272
318
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
273
319
  getMetrics(): CacheMetricsSnapshot;
274
320
  getHitRate(): CacheHitRateSnapshot;
321
+ /**
322
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
323
+ *
324
+ * ```ts
325
+ * const tenant = cache.namespace('tenant:abc')
326
+ * const posts = tenant.namespace('posts')
327
+ * // keys become: "tenant:abc:posts:mykey"
328
+ * ```
329
+ */
330
+ namespace(childPrefix: string): CacheNamespace;
275
331
  qualify(key: string): string;
276
332
  }
277
333
 
@@ -311,6 +367,12 @@ declare class CacheStack extends EventEmitter {
311
367
  * Fetches and caches the value if not already present.
312
368
  */
313
369
  getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
370
+ /**
371
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
372
+ * Useful when the value is expected to exist or the fetcher is expected to
373
+ * return non-null.
374
+ */
375
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
314
376
  /**
315
377
  * Returns true if the given key exists and is not expired in any layer.
316
378
  */
@@ -355,6 +417,12 @@ declare class CacheStack extends EventEmitter {
355
417
  * Returns computed hit-rate statistics (overall and per-layer).
356
418
  */
357
419
  getHitRate(): CacheHitRateSnapshot;
420
+ /**
421
+ * Returns detailed metadata about a single cache key: which layers contain it,
422
+ * remaining fresh/stale/error TTLs, and associated tags.
423
+ * Returns `null` if the key does not exist in any layer.
424
+ */
425
+ inspect(key: string): Promise<CacheInspectResult | null>;
358
426
  exportState(): Promise<CacheSnapshotEntry[]>;
359
427
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
360
428
  persistToFile(filePath: string): Promise<void>;
@@ -378,6 +446,7 @@ declare class CacheStack extends EventEmitter {
378
446
  private deleteKeys;
379
447
  private publishInvalidation;
380
448
  private handleInvalidationMessage;
449
+ private getTagsForKey;
381
450
  private formatError;
382
451
  private sleep;
383
452
  private shouldBroadcastL1Invalidation;
@@ -413,9 +482,31 @@ interface CacheStackModuleOptions {
413
482
  layers: CacheLayer[];
414
483
  bridgeOptions?: CacheStackOptions;
415
484
  }
485
+ interface CacheStackModuleAsyncOptions {
486
+ /**
487
+ * Tokens to inject into the `useFactory` function.
488
+ */
489
+ inject?: Array<Type | string | symbol>;
490
+ /**
491
+ * Async factory function that returns `CacheStackModuleOptions`.
492
+ * Useful when the Redis client or other dependencies must be resolved
493
+ * from the NestJS DI container first.
494
+ *
495
+ * ```ts
496
+ * CacheStackModule.forRootAsync({
497
+ * inject: [ConfigService],
498
+ * useFactory: (config: ConfigService) => ({
499
+ * layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
500
+ * })
501
+ * })
502
+ * ```
503
+ */
504
+ useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
505
+ }
416
506
  declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
417
507
  declare class CacheStackModule {
418
508
  static forRoot(options: CacheStackModuleOptions): DynamicModule;
509
+ static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
419
510
  }
420
511
 
421
512
  export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };