layercache 1.0.2 → 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.
@@ -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
  }
@@ -31,6 +40,7 @@ interface CacheMSetEntry<T> {
31
40
  value: T;
32
41
  options?: CacheWriteOptions;
33
42
  }
43
+ /** Interface that all cache backend implementations must satisfy. */
34
44
  interface CacheLayer {
35
45
  readonly name: string;
36
46
  readonly defaultTtl?: number;
@@ -43,7 +53,33 @@ interface CacheLayer {
43
53
  clear(): Promise<void>;
44
54
  deleteMany?(keys: string[]): Promise<void>;
45
55
  keys?(): Promise<string[]>;
56
+ /**
57
+ * Returns true if the key exists and has not expired.
58
+ * Implementations may omit this; CacheStack will fall back to `get()`.
59
+ */
60
+ has?(key: string): Promise<boolean>;
61
+ /**
62
+ * Returns the remaining TTL in seconds for the key, or null if the key
63
+ * does not exist, has no TTL, or has already expired.
64
+ * Implementations may omit this.
65
+ */
66
+ ttl?(key: string): Promise<number | null>;
67
+ /**
68
+ * Returns the number of entries currently held by this layer.
69
+ * Implementations may omit this.
70
+ */
71
+ size?(): Promise<number>;
46
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
+ }
82
+ /** Snapshot of cumulative cache counters. */
47
83
  interface CacheMetricsSnapshot {
48
84
  hits: number;
49
85
  misses: number;
@@ -62,6 +98,17 @@ interface CacheMetricsSnapshot {
62
98
  degradedOperations: number;
63
99
  hitsByLayer: Record<string, number>;
64
100
  missesByLayer: Record<string, number>;
101
+ /** Per-layer read latency statistics (sampled from successful reads). */
102
+ latencyByLayer: Record<string, CacheLayerLatency>;
103
+ /** Timestamp (ms since epoch) when metrics were last reset. */
104
+ resetAt: number;
105
+ }
106
+ /** Computed hit-rate statistics derived from CacheMetricsSnapshot. */
107
+ interface CacheHitRateSnapshot {
108
+ /** Overall hit rate across all layers (0–1). */
109
+ overall: number;
110
+ /** Per-layer hit rates (0–1 each). */
111
+ byLayer: Record<string, number>;
65
112
  }
66
113
  interface CacheLogger {
67
114
  debug?(message: string, context?: Record<string, unknown>): void;
@@ -74,6 +121,8 @@ interface CacheTagIndex {
74
121
  track(key: string, tags: string[]): Promise<void>;
75
122
  remove(key: string): Promise<void>;
76
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[]>;
77
126
  matchPattern(pattern: string): Promise<string[]>;
78
127
  clear(): Promise<void>;
79
128
  }
@@ -102,6 +151,9 @@ interface CacheStackOptions {
102
151
  invalidationBus?: InvalidationBus;
103
152
  tagIndex?: CacheTagIndex;
104
153
  broadcastL1Invalidation?: boolean;
154
+ /**
155
+ * @deprecated Use `broadcastL1Invalidation` instead.
156
+ */
105
157
  publishSetInvalidation?: boolean;
106
158
  negativeCaching?: boolean;
107
159
  negativeTtl?: number | LayerTtlMap;
@@ -117,6 +169,12 @@ interface CacheStackOptions {
117
169
  singleFlightLeaseMs?: number;
118
170
  singleFlightTimeoutMs?: number;
119
171
  singleFlightPollMs?: number;
172
+ /**
173
+ * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
174
+ * before the oldest entries are pruned. Prevents unbounded memory growth.
175
+ * Defaults to 100 000.
176
+ */
177
+ maxProfileEntries?: number;
120
178
  }
121
179
  interface CacheAdaptiveTtlOptions {
122
180
  hotAfter?: number;
@@ -136,9 +194,19 @@ interface CacheWarmEntry<T = unknown> {
136
194
  options?: CacheGetOptions;
137
195
  priority?: number;
138
196
  }
197
+ /** Options controlling the cache warm-up process. */
139
198
  interface CacheWarmOptions {
140
199
  concurrency?: number;
141
200
  continueOnError?: boolean;
201
+ /** Called after each entry is processed (success or failure). */
202
+ onProgress?: (progress: CacheWarmProgress) => void;
203
+ }
204
+ /** Progress information delivered to `CacheWarmOptions.onProgress`. */
205
+ interface CacheWarmProgress {
206
+ completed: number;
207
+ total: number;
208
+ key: string;
209
+ success: boolean;
142
210
  }
143
211
  interface CacheWrapOptions<TArgs extends unknown[] = unknown[]> extends CacheGetOptions {
144
212
  keyResolver?: (...args: TArgs) => string;
@@ -157,56 +225,204 @@ interface CacheStatsSnapshot {
157
225
  }>;
158
226
  backgroundRefreshes: number;
159
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
+ }
244
+ /** All events emitted by CacheStack and their payload shapes. */
245
+ interface CacheStackEvents {
246
+ /** Fired on every cache hit. */
247
+ hit: {
248
+ key: string;
249
+ layer: string;
250
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
251
+ };
252
+ /** Fired on every cache miss before the fetcher runs. */
253
+ miss: {
254
+ key: string;
255
+ mode: string;
256
+ };
257
+ /** Fired after a value is stored in the cache. */
258
+ set: {
259
+ key: string;
260
+ kind: string;
261
+ tags?: string[];
262
+ };
263
+ /** Fired after one or more keys are deleted. */
264
+ delete: {
265
+ keys: string[];
266
+ };
267
+ /** Fired when a value is backfilled into a faster layer. */
268
+ backfill: {
269
+ key: string;
270
+ layer: string;
271
+ };
272
+ /** Fired when a stale value is returned to the caller. */
273
+ 'stale-serve': {
274
+ key: string;
275
+ state: string;
276
+ layer: string;
277
+ };
278
+ /** Fired when a duplicate request is deduplicated in stampede prevention. */
279
+ 'stampede-dedupe': {
280
+ key: string;
281
+ };
282
+ /** Fired after a key is successfully warmed. */
283
+ warm: {
284
+ key: string;
285
+ };
286
+ /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
287
+ error: {
288
+ operation: string;
289
+ [key: string]: unknown;
290
+ };
291
+ }
160
292
 
161
293
  declare class CacheNamespace {
162
294
  private readonly cache;
163
295
  private readonly prefix;
164
296
  constructor(cache: CacheStack, prefix: string);
165
297
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
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>;
303
+ has(key: string): Promise<boolean>;
304
+ ttl(key: string): Promise<number | null>;
166
305
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
167
306
  delete(key: string): Promise<void>;
307
+ mdelete(keys: string[]): Promise<void>;
168
308
  clear(): Promise<void>;
169
309
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
170
310
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
171
311
  invalidateByTag(tag: string): Promise<void>;
172
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>;
173
317
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
174
318
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
175
319
  getMetrics(): CacheMetricsSnapshot;
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;
176
331
  qualify(key: string): string;
177
332
  }
178
333
 
334
+ /** Typed overloads for EventEmitter so callers get autocomplete on event names. */
335
+ interface CacheStack {
336
+ on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
337
+ once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
338
+ off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
339
+ emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
340
+ }
179
341
  declare class CacheStack extends EventEmitter {
180
342
  private readonly layers;
181
343
  private readonly options;
182
344
  private readonly stampedeGuard;
183
- private readonly metrics;
345
+ private readonly metricsCollector;
184
346
  private readonly instanceId;
185
347
  private readonly startup;
186
348
  private unsubscribeInvalidation?;
187
349
  private readonly logger;
188
350
  private readonly tagIndex;
189
351
  private readonly backgroundRefreshes;
190
- private readonly accessProfiles;
191
352
  private readonly layerDegradedUntil;
192
- private readonly circuitBreakers;
353
+ private readonly ttlResolver;
354
+ private readonly circuitBreakerManager;
193
355
  private isDisconnecting;
194
356
  private disconnectPromise?;
195
357
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
358
+ /**
359
+ * Read-through cache get.
360
+ * Returns the cached value if present and fresh, or invokes `fetcher` on a miss
361
+ * and stores the result across all layers. Returns `null` if the key is not found
362
+ * and no `fetcher` is provided.
363
+ */
196
364
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
365
+ /**
366
+ * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
367
+ * Fetches and caches the value if not already present.
368
+ */
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>;
376
+ /**
377
+ * Returns true if the given key exists and is not expired in any layer.
378
+ */
379
+ has(key: string): Promise<boolean>;
380
+ /**
381
+ * Returns the remaining TTL in seconds for the key in the fastest layer
382
+ * that has it, or null if the key is not found / has no TTL.
383
+ */
384
+ ttl(key: string): Promise<number | null>;
385
+ /**
386
+ * Stores a value in all cache layers. Overwrites any existing value.
387
+ */
197
388
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
389
+ /**
390
+ * Deletes the key from all layers and publishes an invalidation message.
391
+ */
198
392
  delete(key: string): Promise<void>;
199
393
  clear(): Promise<void>;
394
+ /**
395
+ * Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
396
+ */
397
+ mdelete(keys: string[]): Promise<void>;
200
398
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
201
399
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
202
400
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
401
+ /**
402
+ * Returns a cached version of `fetcher`. The cache key is derived from
403
+ * `prefix` plus the serialized arguments unless a `keyResolver` is provided.
404
+ */
203
405
  wrap<TArgs extends unknown[], TResult>(prefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
406
+ /**
407
+ * Creates a `CacheNamespace` that automatically prefixes all keys with
408
+ * `prefix:`. Useful for multi-tenant or module-level isolation.
409
+ */
204
410
  namespace(prefix: string): CacheNamespace;
205
411
  invalidateByTag(tag: string): Promise<void>;
206
412
  invalidateByPattern(pattern: string): Promise<void>;
207
413
  getMetrics(): CacheMetricsSnapshot;
208
414
  getStats(): CacheStatsSnapshot;
209
415
  resetMetrics(): void;
416
+ /**
417
+ * Returns computed hit-rate statistics (overall and per-layer).
418
+ */
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>;
210
426
  exportState(): Promise<CacheSnapshotEntry[]>;
211
427
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
212
428
  persistToFile(filePath: string): Promise<void>;
@@ -224,14 +440,13 @@ declare class CacheStack extends EventEmitter {
224
440
  private executeLayerOperations;
225
441
  private resolveFreshTtl;
226
442
  private resolveLayerSeconds;
227
- private readLayerNumber;
228
- private applyJitter;
229
443
  private shouldNegativeCache;
230
444
  private scheduleBackgroundRefresh;
231
445
  private resolveSingleFlightOptions;
232
446
  private deleteKeys;
233
447
  private publishInvalidation;
234
448
  private handleInvalidationMessage;
449
+ private getTagsForKey;
235
450
  private formatError;
236
451
  private sleep;
237
452
  private shouldBroadcastL1Invalidation;
@@ -246,15 +461,10 @@ declare class CacheStack extends EventEmitter {
246
461
  private validateAdaptiveTtlOptions;
247
462
  private validateCircuitBreakerOptions;
248
463
  private applyFreshReadPolicies;
249
- private applyAdaptiveTtl;
250
- private recordAccess;
251
- private incrementMetricMap;
252
464
  private shouldSkipLayer;
253
465
  private handleLayerFailure;
254
466
  private isGracefulDegradationEnabled;
255
- private assertCircuitClosed;
256
467
  private recordCircuitFailure;
257
- private resetCircuitBreaker;
258
468
  private isNegativeStoredValue;
259
469
  private emitError;
260
470
  private serializeKeyPart;
@@ -272,9 +482,31 @@ interface CacheStackModuleOptions {
272
482
  layers: CacheLayer[];
273
483
  bridgeOptions?: CacheStackOptions;
274
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
+ }
275
506
  declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
276
507
  declare class CacheStackModule {
277
508
  static forRoot(options: CacheStackModuleOptions): DynamicModule;
509
+ static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
278
510
  }
279
511
 
280
512
  export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };