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.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import Redis from 'ioredis';
3
3
 
4
+ /**
5
+ * Thrown by `CacheStack.getOrThrow()` when no value is found for the given key
6
+ * (fetcher returned null or no fetcher was provided and the key is absent).
7
+ */
8
+ declare class CacheMissError extends Error {
9
+ readonly key: string;
10
+ constructor(key: string);
11
+ }
4
12
  interface LayerTtlMap {
5
13
  [layerName: string]: number | undefined;
6
14
  }
@@ -16,6 +24,15 @@ interface CacheWriteOptions {
16
24
  refreshAhead?: number | LayerTtlMap;
17
25
  adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
18
26
  circuitBreaker?: CacheCircuitBreakerOptions;
27
+ /**
28
+ * Optional predicate called with the fetcher's return value before caching.
29
+ * Return `false` to skip storing the value in the cache (but still return it
30
+ * to the caller). Useful for not caching failed API responses or empty results.
31
+ *
32
+ * @example
33
+ * cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
34
+ */
35
+ shouldCache?: (value: unknown) => boolean;
19
36
  }
20
37
  interface CacheGetOptions extends CacheWriteOptions {
21
38
  }
@@ -29,6 +46,7 @@ interface CacheMSetEntry<T> {
29
46
  value: T;
30
47
  options?: CacheWriteOptions;
31
48
  }
49
+ /** Interface that all cache backend implementations must satisfy. */
32
50
  interface CacheLayer {
33
51
  readonly name: string;
34
52
  readonly defaultTtl?: number;
@@ -41,11 +59,37 @@ interface CacheLayer {
41
59
  clear(): Promise<void>;
42
60
  deleteMany?(keys: string[]): Promise<void>;
43
61
  keys?(): Promise<string[]>;
62
+ /**
63
+ * Returns true if the key exists and has not expired.
64
+ * Implementations may omit this; CacheStack will fall back to `get()`.
65
+ */
66
+ has?(key: string): Promise<boolean>;
67
+ /**
68
+ * Returns the remaining TTL in seconds for the key, or null if the key
69
+ * does not exist, has no TTL, or has already expired.
70
+ * Implementations may omit this.
71
+ */
72
+ ttl?(key: string): Promise<number | null>;
73
+ /**
74
+ * Returns the number of entries currently held by this layer.
75
+ * Implementations may omit this.
76
+ */
77
+ size?(): Promise<number>;
44
78
  }
45
79
  interface CacheSerializer {
46
80
  serialize(value: unknown): string | Buffer;
47
81
  deserialize<T>(payload: string | Buffer): T;
48
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
+ }
92
+ /** Snapshot of cumulative cache counters. */
49
93
  interface CacheMetricsSnapshot {
50
94
  hits: number;
51
95
  misses: number;
@@ -64,6 +108,17 @@ interface CacheMetricsSnapshot {
64
108
  degradedOperations: number;
65
109
  hitsByLayer: Record<string, number>;
66
110
  missesByLayer: Record<string, number>;
111
+ /** Per-layer read latency statistics (sampled from successful reads). */
112
+ latencyByLayer: Record<string, CacheLayerLatency>;
113
+ /** Timestamp (ms since epoch) when metrics were last reset. */
114
+ resetAt: number;
115
+ }
116
+ /** Computed hit-rate statistics derived from CacheMetricsSnapshot. */
117
+ interface CacheHitRateSnapshot {
118
+ /** Overall hit rate across all layers (0–1). */
119
+ overall: number;
120
+ /** Per-layer hit rates (0–1 each). */
121
+ byLayer: Record<string, number>;
67
122
  }
68
123
  interface CacheLogger {
69
124
  debug?(message: string, context?: Record<string, unknown>): void;
@@ -76,6 +131,8 @@ interface CacheTagIndex {
76
131
  track(key: string, tags: string[]): Promise<void>;
77
132
  remove(key: string): Promise<void>;
78
133
  keysForTag(tag: string): Promise<string[]>;
134
+ /** Returns the tags associated with a specific key, or an empty array. */
135
+ tagsForKey?(key: string): Promise<string[]>;
79
136
  matchPattern(pattern: string): Promise<string[]>;
80
137
  clear(): Promise<void>;
81
138
  }
@@ -104,6 +161,9 @@ interface CacheStackOptions {
104
161
  invalidationBus?: InvalidationBus;
105
162
  tagIndex?: CacheTagIndex;
106
163
  broadcastL1Invalidation?: boolean;
164
+ /**
165
+ * @deprecated Use `broadcastL1Invalidation` instead.
166
+ */
107
167
  publishSetInvalidation?: boolean;
108
168
  negativeCaching?: boolean;
109
169
  negativeTtl?: number | LayerTtlMap;
@@ -119,6 +179,12 @@ interface CacheStackOptions {
119
179
  singleFlightLeaseMs?: number;
120
180
  singleFlightTimeoutMs?: number;
121
181
  singleFlightPollMs?: number;
182
+ /**
183
+ * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
184
+ * before the oldest entries are pruned. Prevents unbounded memory growth.
185
+ * Defaults to 100 000.
186
+ */
187
+ maxProfileEntries?: number;
122
188
  }
123
189
  interface CacheAdaptiveTtlOptions {
124
190
  hotAfter?: number;
@@ -138,9 +204,19 @@ interface CacheWarmEntry<T = unknown> {
138
204
  options?: CacheGetOptions;
139
205
  priority?: number;
140
206
  }
207
+ /** Options controlling the cache warm-up process. */
141
208
  interface CacheWarmOptions {
142
209
  concurrency?: number;
143
210
  continueOnError?: boolean;
211
+ /** Called after each entry is processed (success or failure). */
212
+ onProgress?: (progress: CacheWarmProgress) => void;
213
+ }
214
+ /** Progress information delivered to `CacheWarmOptions.onProgress`. */
215
+ interface CacheWarmProgress {
216
+ completed: number;
217
+ total: number;
218
+ key: string;
219
+ success: boolean;
144
220
  }
145
221
  interface CacheWrapOptions<TArgs extends unknown[] = unknown[]> extends CacheGetOptions {
146
222
  keyResolver?: (...args: TArgs) => string;
@@ -159,56 +235,204 @@ interface CacheStatsSnapshot {
159
235
  }>;
160
236
  backgroundRefreshes: number;
161
237
  }
238
+ /** Detailed inspection result for a single cache key. */
239
+ interface CacheInspectResult {
240
+ key: string;
241
+ /** Layers in which the key is currently stored (not expired). */
242
+ foundInLayers: string[];
243
+ /** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
244
+ freshTtlSeconds: number | null;
245
+ /** Remaining stale-while-revalidate window in seconds, or null. */
246
+ staleTtlSeconds: number | null;
247
+ /** Remaining stale-if-error window in seconds, or null. */
248
+ errorTtlSeconds: number | null;
249
+ /** Whether the key is currently serving stale-while-revalidate. */
250
+ isStale: boolean;
251
+ /** Tags associated with this key (from the TagIndex). */
252
+ tags: string[];
253
+ }
254
+ /** All events emitted by CacheStack and their payload shapes. */
255
+ interface CacheStackEvents {
256
+ /** Fired on every cache hit. */
257
+ hit: {
258
+ key: string;
259
+ layer: string;
260
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
261
+ };
262
+ /** Fired on every cache miss before the fetcher runs. */
263
+ miss: {
264
+ key: string;
265
+ mode: string;
266
+ };
267
+ /** Fired after a value is stored in the cache. */
268
+ set: {
269
+ key: string;
270
+ kind: string;
271
+ tags?: string[];
272
+ };
273
+ /** Fired after one or more keys are deleted. */
274
+ delete: {
275
+ keys: string[];
276
+ };
277
+ /** Fired when a value is backfilled into a faster layer. */
278
+ backfill: {
279
+ key: string;
280
+ layer: string;
281
+ };
282
+ /** Fired when a stale value is returned to the caller. */
283
+ 'stale-serve': {
284
+ key: string;
285
+ state: string;
286
+ layer: string;
287
+ };
288
+ /** Fired when a duplicate request is deduplicated in stampede prevention. */
289
+ 'stampede-dedupe': {
290
+ key: string;
291
+ };
292
+ /** Fired after a key is successfully warmed. */
293
+ warm: {
294
+ key: string;
295
+ };
296
+ /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
297
+ error: {
298
+ operation: string;
299
+ [key: string]: unknown;
300
+ };
301
+ }
162
302
 
163
303
  declare class CacheNamespace {
164
304
  private readonly cache;
165
305
  private readonly prefix;
166
306
  constructor(cache: CacheStack, prefix: string);
167
307
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
308
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
309
+ /**
310
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
311
+ */
312
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
313
+ has(key: string): Promise<boolean>;
314
+ ttl(key: string): Promise<number | null>;
168
315
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
169
316
  delete(key: string): Promise<void>;
317
+ mdelete(keys: string[]): Promise<void>;
170
318
  clear(): Promise<void>;
171
319
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
172
320
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
173
321
  invalidateByTag(tag: string): Promise<void>;
174
322
  invalidateByPattern(pattern: string): Promise<void>;
323
+ /**
324
+ * Returns detailed metadata about a single cache key within this namespace.
325
+ */
326
+ inspect(key: string): Promise<CacheInspectResult | null>;
175
327
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
176
328
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
177
329
  getMetrics(): CacheMetricsSnapshot;
330
+ getHitRate(): CacheHitRateSnapshot;
331
+ /**
332
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
333
+ *
334
+ * ```ts
335
+ * const tenant = cache.namespace('tenant:abc')
336
+ * const posts = tenant.namespace('posts')
337
+ * // keys become: "tenant:abc:posts:mykey"
338
+ * ```
339
+ */
340
+ namespace(childPrefix: string): CacheNamespace;
178
341
  qualify(key: string): string;
179
342
  }
180
343
 
344
+ /** Typed overloads for EventEmitter so callers get autocomplete on event names. */
345
+ interface CacheStack {
346
+ on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
347
+ once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
348
+ off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
349
+ emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
350
+ }
181
351
  declare class CacheStack extends EventEmitter {
182
352
  private readonly layers;
183
353
  private readonly options;
184
354
  private readonly stampedeGuard;
185
- private readonly metrics;
355
+ private readonly metricsCollector;
186
356
  private readonly instanceId;
187
357
  private readonly startup;
188
358
  private unsubscribeInvalidation?;
189
359
  private readonly logger;
190
360
  private readonly tagIndex;
191
361
  private readonly backgroundRefreshes;
192
- private readonly accessProfiles;
193
362
  private readonly layerDegradedUntil;
194
- private readonly circuitBreakers;
363
+ private readonly ttlResolver;
364
+ private readonly circuitBreakerManager;
195
365
  private isDisconnecting;
196
366
  private disconnectPromise?;
197
367
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
368
+ /**
369
+ * Read-through cache get.
370
+ * Returns the cached value if present and fresh, or invokes `fetcher` on a miss
371
+ * and stores the result across all layers. Returns `null` if the key is not found
372
+ * and no `fetcher` is provided.
373
+ */
198
374
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
375
+ /**
376
+ * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
377
+ * Fetches and caches the value if not already present.
378
+ */
379
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
380
+ /**
381
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
382
+ * Useful when the value is expected to exist or the fetcher is expected to
383
+ * return non-null.
384
+ */
385
+ getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
386
+ /**
387
+ * Returns true if the given key exists and is not expired in any layer.
388
+ */
389
+ has(key: string): Promise<boolean>;
390
+ /**
391
+ * Returns the remaining TTL in seconds for the key in the fastest layer
392
+ * that has it, or null if the key is not found / has no TTL.
393
+ */
394
+ ttl(key: string): Promise<number | null>;
395
+ /**
396
+ * Stores a value in all cache layers. Overwrites any existing value.
397
+ */
199
398
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
399
+ /**
400
+ * Deletes the key from all layers and publishes an invalidation message.
401
+ */
200
402
  delete(key: string): Promise<void>;
201
403
  clear(): Promise<void>;
404
+ /**
405
+ * Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
406
+ */
407
+ mdelete(keys: string[]): Promise<void>;
202
408
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
203
409
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
204
410
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
411
+ /**
412
+ * Returns a cached version of `fetcher`. The cache key is derived from
413
+ * `prefix` plus the serialized arguments unless a `keyResolver` is provided.
414
+ */
205
415
  wrap<TArgs extends unknown[], TResult>(prefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
416
+ /**
417
+ * Creates a `CacheNamespace` that automatically prefixes all keys with
418
+ * `prefix:`. Useful for multi-tenant or module-level isolation.
419
+ */
206
420
  namespace(prefix: string): CacheNamespace;
207
421
  invalidateByTag(tag: string): Promise<void>;
208
422
  invalidateByPattern(pattern: string): Promise<void>;
209
423
  getMetrics(): CacheMetricsSnapshot;
210
424
  getStats(): CacheStatsSnapshot;
211
425
  resetMetrics(): void;
426
+ /**
427
+ * Returns computed hit-rate statistics (overall and per-layer).
428
+ */
429
+ getHitRate(): CacheHitRateSnapshot;
430
+ /**
431
+ * Returns detailed metadata about a single cache key: which layers contain it,
432
+ * remaining fresh/stale/error TTLs, and associated tags.
433
+ * Returns `null` if the key does not exist in any layer.
434
+ */
435
+ inspect(key: string): Promise<CacheInspectResult | null>;
212
436
  exportState(): Promise<CacheSnapshotEntry[]>;
213
437
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
214
438
  persistToFile(filePath: string): Promise<void>;
@@ -226,14 +450,13 @@ declare class CacheStack extends EventEmitter {
226
450
  private executeLayerOperations;
227
451
  private resolveFreshTtl;
228
452
  private resolveLayerSeconds;
229
- private readLayerNumber;
230
- private applyJitter;
231
453
  private shouldNegativeCache;
232
454
  private scheduleBackgroundRefresh;
233
455
  private resolveSingleFlightOptions;
234
456
  private deleteKeys;
235
457
  private publishInvalidation;
236
458
  private handleInvalidationMessage;
459
+ private getTagsForKey;
237
460
  private formatError;
238
461
  private sleep;
239
462
  private shouldBroadcastL1Invalidation;
@@ -248,15 +471,10 @@ declare class CacheStack extends EventEmitter {
248
471
  private validateAdaptiveTtlOptions;
249
472
  private validateCircuitBreakerOptions;
250
473
  private applyFreshReadPolicies;
251
- private applyAdaptiveTtl;
252
- private recordAccess;
253
- private incrementMetricMap;
254
474
  private shouldSkipLayer;
255
475
  private handleLayerFailure;
256
476
  private isGracefulDegradationEnabled;
257
- private assertCircuitClosed;
258
477
  private recordCircuitFailure;
259
- private resetCircuitBreaker;
260
478
  private isNegativeStoredValue;
261
479
  private emitError;
262
480
  private serializeKeyPart;
@@ -265,7 +483,17 @@ declare class CacheStack extends EventEmitter {
265
483
  }
266
484
 
267
485
  declare class PatternMatcher {
486
+ /**
487
+ * Tests whether a glob-style pattern matches a value.
488
+ * Supports `*` (any sequence of characters) and `?` (any single character).
489
+ * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
490
+ */
268
491
  static matches(pattern: string, value: string): boolean;
492
+ /**
493
+ * Linear-time glob matching using dynamic programming.
494
+ * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
495
+ */
496
+ private static matchLinear;
269
497
  }
270
498
 
271
499
  interface RedisInvalidationBusOptions {
@@ -273,15 +501,23 @@ interface RedisInvalidationBusOptions {
273
501
  subscriber?: Redis;
274
502
  channel?: string;
275
503
  }
504
+ /**
505
+ * Redis pub/sub invalidation bus.
506
+ *
507
+ * Supports multiple concurrent subscriptions — each `CacheStack` instance
508
+ * can independently call `subscribe()` and receive its own unsubscribe handle.
509
+ * The underlying Redis SUBSCRIBE is only issued once and shared across all handlers.
510
+ */
276
511
  declare class RedisInvalidationBus implements InvalidationBus {
277
512
  private readonly channel;
278
513
  private readonly publisher;
279
514
  private readonly subscriber;
280
- private activeListener?;
515
+ private readonly handlers;
516
+ private sharedListener?;
281
517
  constructor(options: RedisInvalidationBusOptions);
282
518
  subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
283
519
  publish(message: InvalidationMessage): Promise<void>;
284
- private handleMessage;
520
+ private dispatchToHandlers;
285
521
  private isInvalidationMessage;
286
522
  private reportError;
287
523
  }
@@ -300,6 +536,7 @@ declare class RedisTagIndex implements CacheTagIndex {
300
536
  track(key: string, tags: string[]): Promise<void>;
301
537
  remove(key: string): Promise<void>;
302
538
  keysForTag(tag: string): Promise<string[]>;
539
+ tagsForKey(key: string): Promise<string[]>;
303
540
  matchPattern(pattern: string): Promise<string[]>;
304
541
  clear(): Promise<void>;
305
542
  private scanIndexKeys;
@@ -308,16 +545,28 @@ declare class RedisTagIndex implements CacheTagIndex {
308
545
  private tagKeysKey;
309
546
  }
310
547
 
548
+ interface TagIndexOptions {
549
+ /**
550
+ * Maximum number of keys tracked in `knownKeys`. When exceeded, the oldest
551
+ * 10 % of keys are pruned to keep memory bounded.
552
+ * Defaults to unlimited.
553
+ */
554
+ maxKnownKeys?: number;
555
+ }
311
556
  declare class TagIndex implements CacheTagIndex {
312
557
  private readonly tagToKeys;
313
558
  private readonly keyToTags;
314
559
  private readonly knownKeys;
560
+ private readonly maxKnownKeys;
561
+ constructor(options?: TagIndexOptions);
315
562
  touch(key: string): Promise<void>;
316
563
  track(key: string, tags: string[]): Promise<void>;
317
564
  remove(key: string): Promise<void>;
318
565
  keysForTag(tag: string): Promise<string[]>;
566
+ tagsForKey(key: string): Promise<string[]>;
319
567
  matchPattern(pattern: string): Promise<string[]>;
320
568
  clear(): Promise<void>;
569
+ private pruneKnownKeysIfNeeded;
321
570
  }
322
571
 
323
572
  declare function createCacheStatsHandler(cache: CacheStack): (_request: unknown, response: {
@@ -342,6 +591,48 @@ interface FastifyLayercachePluginOptions {
342
591
  }
343
592
  declare function createFastifyLayercachePlugin(cache: CacheStack, options?: FastifyLayercachePluginOptions): (fastify: FastifyLike) => Promise<void>;
344
593
 
594
+ interface ExpressLikeRequest {
595
+ method?: string;
596
+ url?: string;
597
+ originalUrl?: string;
598
+ path?: string;
599
+ query?: Record<string, unknown>;
600
+ }
601
+ interface ExpressLikeResponse {
602
+ statusCode?: number;
603
+ setHeader?: (name: string, value: string) => void;
604
+ json?: (body: unknown) => void;
605
+ end?: (body?: string) => void;
606
+ }
607
+ type NextFunction = (error?: unknown) => void;
608
+ interface ExpressCacheMiddlewareOptions extends CacheGetOptions {
609
+ /**
610
+ * Resolves a cache key from the incoming request. Defaults to
611
+ * `GET:<req.originalUrl || req.url>`.
612
+ */
613
+ keyResolver?: (req: ExpressLikeRequest) => string;
614
+ /**
615
+ * Only cache responses for these HTTP methods. Defaults to `['GET']`.
616
+ */
617
+ methods?: string[];
618
+ }
619
+ /**
620
+ * Express/Connect-compatible middleware that caches JSON responses.
621
+ *
622
+ * ```ts
623
+ * import express from 'express'
624
+ * import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
625
+ *
626
+ * const cache = new CacheStack([new MemoryLayer({ ttl: 60 })])
627
+ * const app = express()
628
+ *
629
+ * app.get('/api/data', createExpressCacheMiddleware(cache, { ttl: 30 }), (req, res) => {
630
+ * res.json({ fresh: true })
631
+ * })
632
+ * ```
633
+ */
634
+ declare function createExpressCacheMiddleware(cache: CacheStack, options?: ExpressCacheMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: NextFunction) => Promise<void>;
635
+
345
636
  interface GraphqlCacheOptions<TArgs extends unknown[]> extends CacheGetOptions {
346
637
  keyResolver?: (...args: TArgs) => string;
347
638
  }
@@ -369,28 +660,41 @@ interface MemoryLayerSnapshotEntry {
369
660
  value: unknown;
370
661
  expiresAt: number | null;
371
662
  }
663
+ /**
664
+ * Eviction policy applied when `maxSize` is reached.
665
+ * - `lru` (default): evicts the Least Recently Used entry.
666
+ * - `lfu`: evicts the Least Frequently Used entry.
667
+ * - `fifo`: evicts the oldest inserted entry.
668
+ */
669
+ type EvictionPolicy = 'lru' | 'lfu' | 'fifo';
372
670
  interface MemoryLayerOptions {
373
671
  ttl?: number;
374
672
  maxSize?: number;
375
673
  name?: string;
674
+ evictionPolicy?: EvictionPolicy;
376
675
  }
377
676
  declare class MemoryLayer implements CacheLayer {
378
677
  readonly name: string;
379
678
  readonly defaultTtl?: number;
380
679
  readonly isLocal = true;
381
680
  private readonly maxSize;
681
+ private readonly evictionPolicy;
382
682
  private readonly entries;
383
683
  constructor(options?: MemoryLayerOptions);
384
684
  get<T>(key: string): Promise<T | null>;
385
685
  getEntry<T = unknown>(key: string): Promise<T | null>;
386
686
  getMany<T>(keys: string[]): Promise<Array<T | null>>;
387
687
  set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
688
+ has(key: string): Promise<boolean>;
689
+ ttl(key: string): Promise<number | null>;
690
+ size(): Promise<number>;
388
691
  delete(key: string): Promise<void>;
389
692
  deleteMany(keys: string[]): Promise<void>;
390
693
  clear(): Promise<void>;
391
694
  keys(): Promise<string[]>;
392
695
  exportState(): MemoryLayerSnapshotEntry[];
393
696
  importState(entries: MemoryLayerSnapshotEntry[]): void;
697
+ private evict;
394
698
  private pruneExpired;
395
699
  private isExpired;
396
700
  }
@@ -425,16 +729,144 @@ declare class RedisLayer implements CacheLayer {
425
729
  set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
426
730
  delete(key: string): Promise<void>;
427
731
  deleteMany(keys: string[]): Promise<void>;
732
+ has(key: string): Promise<boolean>;
733
+ ttl(key: string): Promise<number | null>;
734
+ size(): Promise<number>;
735
+ /**
736
+ * Deletes all keys matching the layer's prefix in batches to avoid
737
+ * loading millions of keys into memory at once.
738
+ */
428
739
  clear(): Promise<void>;
429
740
  keys(): Promise<string[]>;
430
741
  private scanKeys;
431
742
  private withPrefix;
432
743
  private deserializeOrDelete;
433
744
  private isSerializablePayload;
745
+ /**
746
+ * Compresses the payload asynchronously if compression is enabled and the
747
+ * payload exceeds the threshold. This avoids blocking the event loop.
748
+ */
434
749
  private encodePayload;
750
+ /**
751
+ * Decompresses the payload asynchronously if a compression header is present.
752
+ */
435
753
  private decodePayload;
436
754
  }
437
755
 
756
+ interface DiskLayerOptions {
757
+ directory: string;
758
+ ttl?: number;
759
+ name?: string;
760
+ serializer?: CacheSerializer;
761
+ /**
762
+ * Maximum number of cache files to store on disk. When exceeded, the oldest
763
+ * entries (by file mtime) are evicted to keep the directory bounded.
764
+ * Defaults to unlimited.
765
+ */
766
+ maxFiles?: number;
767
+ }
768
+ /**
769
+ * A file-system backed cache layer.
770
+ * Each key is stored as a separate JSON file in `directory`.
771
+ * Useful for persisting cache across process restarts without needing Redis.
772
+ *
773
+ * - `keys()` returns the original cache key strings (not hashes).
774
+ * - `maxFiles` limits on-disk entries; when exceeded, oldest files are evicted.
775
+ *
776
+ * NOTE: DiskLayer is designed for low-to-medium traffic scenarios.
777
+ * For high-throughput workloads, use MemoryLayer + RedisLayer.
778
+ */
779
+ declare class DiskLayer implements CacheLayer {
780
+ readonly name: string;
781
+ readonly defaultTtl?: number;
782
+ readonly isLocal = true;
783
+ private readonly directory;
784
+ private readonly serializer;
785
+ private readonly maxFiles;
786
+ constructor(options: DiskLayerOptions);
787
+ get<T>(key: string): Promise<T | null>;
788
+ getEntry<T = unknown>(key: string): Promise<T | null>;
789
+ set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
790
+ has(key: string): Promise<boolean>;
791
+ ttl(key: string): Promise<number | null>;
792
+ delete(key: string): Promise<void>;
793
+ deleteMany(keys: string[]): Promise<void>;
794
+ clear(): Promise<void>;
795
+ /**
796
+ * Returns the original cache key strings stored on disk.
797
+ * Expired entries are skipped and cleaned up during the scan.
798
+ */
799
+ keys(): Promise<string[]>;
800
+ size(): Promise<number>;
801
+ private keyToPath;
802
+ private safeDelete;
803
+ /**
804
+ * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
805
+ */
806
+ private enforceMaxFiles;
807
+ }
808
+
809
+ /**
810
+ * Minimal interface that MemcachedLayer expects from a Memcached client.
811
+ * Compatible with the `memjs` and `memcache-client` npm packages.
812
+ *
813
+ * Install one of:
814
+ * npm install memjs
815
+ * npm install memcache-client
816
+ */
817
+ interface MemcachedClient {
818
+ get(key: string): Promise<{
819
+ value: Buffer | null;
820
+ } | null>;
821
+ set(key: string, value: string | Buffer, options?: {
822
+ expires?: number;
823
+ }): Promise<boolean | undefined>;
824
+ delete(key: string): Promise<boolean | undefined>;
825
+ }
826
+ interface MemcachedLayerOptions {
827
+ client: MemcachedClient;
828
+ ttl?: number;
829
+ name?: string;
830
+ keyPrefix?: string;
831
+ serializer?: CacheSerializer;
832
+ }
833
+ /**
834
+ * Memcached-backed cache layer.
835
+ *
836
+ * Now supports pluggable serializers (default: JSON), StoredValueEnvelope
837
+ * for stale-while-revalidate / stale-if-error semantics, and bulk reads.
838
+ *
839
+ * Example usage with `memjs`:
840
+ * ```ts
841
+ * import Memjs from 'memjs'
842
+ * import { CacheStack, MemcachedLayer, MemoryLayer } from 'layercache'
843
+ *
844
+ * const memcached = Memjs.Client.create('localhost:11211')
845
+ * const cache = new CacheStack([
846
+ * new MemoryLayer({ ttl: 30 }),
847
+ * new MemcachedLayer({ client: memcached, ttl: 300 })
848
+ * ])
849
+ * ```
850
+ */
851
+ declare class MemcachedLayer implements CacheLayer {
852
+ readonly name: string;
853
+ readonly defaultTtl?: number;
854
+ readonly isLocal = false;
855
+ private readonly client;
856
+ private readonly keyPrefix;
857
+ private readonly serializer;
858
+ constructor(options: MemcachedLayerOptions);
859
+ get<T>(key: string): Promise<T | null>;
860
+ getEntry<T = unknown>(key: string): Promise<T | null>;
861
+ getMany<T>(keys: string[]): Promise<Array<T | null>>;
862
+ set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
863
+ has(key: string): Promise<boolean>;
864
+ delete(key: string): Promise<void>;
865
+ deleteMany(keys: string[]): Promise<void>;
866
+ clear(): Promise<void>;
867
+ private withPrefix;
868
+ }
869
+
438
870
  declare class JsonSerializer implements CacheSerializer {
439
871
  serialize(value: unknown): string;
440
872
  deserialize<T>(payload: string | Buffer): T;
@@ -462,4 +894,28 @@ declare class StampedeGuard {
462
894
  private getMutexEntry;
463
895
  }
464
896
 
465
- export { type CacheAdaptiveTtlOptions, type CacheCircuitBreakerOptions, type CacheDegradationOptions, type CacheGetOptions, type CacheLayer, type CacheLogger, type CacheMGetEntry, type CacheMSetEntry, type CacheMetricsSnapshot, CacheNamespace, type CacheSerializer, type CacheSingleFlightCoordinator, type CacheSingleFlightExecutionOptions, type CacheSnapshotEntry, CacheStack, type CacheStackOptions, type CacheStatsSnapshot, type CacheTagIndex, type CacheWarmEntry, type CacheWarmOptions, type CacheWrapOptions, type CacheWriteOptions, type InvalidationBus, type InvalidationMessage, JsonSerializer, type LayerTtlMap, MemoryLayer, MsgpackSerializer, PatternMatcher, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, TagIndex, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createFastifyLayercachePlugin, createTrpcCacheMiddleware };
897
+ /**
898
+ * Returns a function that generates a Prometheus-compatible text exposition
899
+ * of the cache metrics from one or more CacheStack instances.
900
+ *
901
+ * Now includes per-layer latency gauges (`layercache_layer_latency_avg_ms`,
902
+ * `layercache_layer_latency_max_ms`, `layercache_layer_latency_count`).
903
+ *
904
+ * Usage example:
905
+ * ```ts
906
+ * const collect = createPrometheusMetricsExporter(cache)
907
+ * http.createServer(async (_req, res) => {
908
+ * res.setHeader('content-type', 'text/plain; version=0.0.4; charset=utf-8')
909
+ * res.end(collect())
910
+ * }).listen(9091)
911
+ * ```
912
+ *
913
+ * @param stacks One or more CacheStack instances. When multiple stacks are
914
+ * given, each must be named via the optional `name` parameter.
915
+ */
916
+ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
917
+ stack: CacheStack;
918
+ name: string;
919
+ }>): () => string;
920
+
921
+ export { type CacheAdaptiveTtlOptions, type CacheCircuitBreakerOptions, type CacheDegradationOptions, type CacheGetOptions, type CacheHitRateSnapshot, type CacheInspectResult, type CacheLayer, type CacheLayerLatency, type CacheLogger, type CacheMGetEntry, type CacheMSetEntry, type CacheMetricsSnapshot, CacheMissError, CacheNamespace, type CacheSerializer, type CacheSingleFlightCoordinator, type CacheSingleFlightExecutionOptions, type CacheSnapshotEntry, CacheStack, type CacheStackEvents, type CacheStackOptions, type CacheStatsSnapshot, type CacheTagIndex, type CacheWarmEntry, type CacheWarmOptions, type CacheWarmProgress, type CacheWrapOptions, type CacheWriteOptions, DiskLayer, type EvictionPolicy, type InvalidationBus, type InvalidationMessage, JsonSerializer, type LayerTtlMap, type MemcachedClient, MemcachedLayer, MemoryLayer, type MemoryLayerSnapshotEntry, MsgpackSerializer, PatternMatcher, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator, RedisTagIndex, StampedeGuard, TagIndex, cacheGraphqlResolver, createCacheStatsHandler, createCachedMethodDecorator, createExpressCacheMiddleware, createFastifyLayercachePlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };