layercache 1.0.2 → 1.1.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.ts CHANGED
@@ -29,6 +29,7 @@ interface CacheMSetEntry<T> {
29
29
  value: T;
30
30
  options?: CacheWriteOptions;
31
31
  }
32
+ /** Interface that all cache backend implementations must satisfy. */
32
33
  interface CacheLayer {
33
34
  readonly name: string;
34
35
  readonly defaultTtl?: number;
@@ -41,11 +42,28 @@ interface CacheLayer {
41
42
  clear(): Promise<void>;
42
43
  deleteMany?(keys: string[]): Promise<void>;
43
44
  keys?(): Promise<string[]>;
45
+ /**
46
+ * Returns true if the key exists and has not expired.
47
+ * Implementations may omit this; CacheStack will fall back to `get()`.
48
+ */
49
+ has?(key: string): Promise<boolean>;
50
+ /**
51
+ * Returns the remaining TTL in seconds for the key, or null if the key
52
+ * does not exist, has no TTL, or has already expired.
53
+ * Implementations may omit this.
54
+ */
55
+ ttl?(key: string): Promise<number | null>;
56
+ /**
57
+ * Returns the number of entries currently held by this layer.
58
+ * Implementations may omit this.
59
+ */
60
+ size?(): Promise<number>;
44
61
  }
45
62
  interface CacheSerializer {
46
63
  serialize(value: unknown): string | Buffer;
47
64
  deserialize<T>(payload: string | Buffer): T;
48
65
  }
66
+ /** Snapshot of cumulative cache counters. */
49
67
  interface CacheMetricsSnapshot {
50
68
  hits: number;
51
69
  misses: number;
@@ -64,6 +82,15 @@ interface CacheMetricsSnapshot {
64
82
  degradedOperations: number;
65
83
  hitsByLayer: Record<string, number>;
66
84
  missesByLayer: Record<string, number>;
85
+ /** Timestamp (ms since epoch) when metrics were last reset. */
86
+ resetAt: number;
87
+ }
88
+ /** Computed hit-rate statistics derived from CacheMetricsSnapshot. */
89
+ interface CacheHitRateSnapshot {
90
+ /** Overall hit rate across all layers (0–1). */
91
+ overall: number;
92
+ /** Per-layer hit rates (0–1 each). */
93
+ byLayer: Record<string, number>;
67
94
  }
68
95
  interface CacheLogger {
69
96
  debug?(message: string, context?: Record<string, unknown>): void;
@@ -104,6 +131,9 @@ interface CacheStackOptions {
104
131
  invalidationBus?: InvalidationBus;
105
132
  tagIndex?: CacheTagIndex;
106
133
  broadcastL1Invalidation?: boolean;
134
+ /**
135
+ * @deprecated Use `broadcastL1Invalidation` instead.
136
+ */
107
137
  publishSetInvalidation?: boolean;
108
138
  negativeCaching?: boolean;
109
139
  negativeTtl?: number | LayerTtlMap;
@@ -119,6 +149,12 @@ interface CacheStackOptions {
119
149
  singleFlightLeaseMs?: number;
120
150
  singleFlightTimeoutMs?: number;
121
151
  singleFlightPollMs?: number;
152
+ /**
153
+ * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
154
+ * before the oldest entries are pruned. Prevents unbounded memory growth.
155
+ * Defaults to 100 000.
156
+ */
157
+ maxProfileEntries?: number;
122
158
  }
123
159
  interface CacheAdaptiveTtlOptions {
124
160
  hotAfter?: number;
@@ -138,9 +174,19 @@ interface CacheWarmEntry<T = unknown> {
138
174
  options?: CacheGetOptions;
139
175
  priority?: number;
140
176
  }
177
+ /** Options controlling the cache warm-up process. */
141
178
  interface CacheWarmOptions {
142
179
  concurrency?: number;
143
180
  continueOnError?: boolean;
181
+ /** Called after each entry is processed (success or failure). */
182
+ onProgress?: (progress: CacheWarmProgress) => void;
183
+ }
184
+ /** Progress information delivered to `CacheWarmOptions.onProgress`. */
185
+ interface CacheWarmProgress {
186
+ completed: number;
187
+ total: number;
188
+ key: string;
189
+ success: boolean;
144
190
  }
145
191
  interface CacheWrapOptions<TArgs extends unknown[] = unknown[]> extends CacheGetOptions {
146
192
  keyResolver?: (...args: TArgs) => string;
@@ -159,14 +205,66 @@ interface CacheStatsSnapshot {
159
205
  }>;
160
206
  backgroundRefreshes: number;
161
207
  }
208
+ /** All events emitted by CacheStack and their payload shapes. */
209
+ interface CacheStackEvents {
210
+ /** Fired on every cache hit. */
211
+ hit: {
212
+ key: string;
213
+ layer: string;
214
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
215
+ };
216
+ /** Fired on every cache miss before the fetcher runs. */
217
+ miss: {
218
+ key: string;
219
+ mode: string;
220
+ };
221
+ /** Fired after a value is stored in the cache. */
222
+ set: {
223
+ key: string;
224
+ kind: string;
225
+ tags?: string[];
226
+ };
227
+ /** Fired after one or more keys are deleted. */
228
+ delete: {
229
+ keys: string[];
230
+ };
231
+ /** Fired when a value is backfilled into a faster layer. */
232
+ backfill: {
233
+ key: string;
234
+ layer: string;
235
+ };
236
+ /** Fired when a stale value is returned to the caller. */
237
+ 'stale-serve': {
238
+ key: string;
239
+ state: string;
240
+ layer: string;
241
+ };
242
+ /** Fired when a duplicate request is deduplicated in stampede prevention. */
243
+ 'stampede-dedupe': {
244
+ key: string;
245
+ };
246
+ /** Fired after a key is successfully warmed. */
247
+ warm: {
248
+ key: string;
249
+ };
250
+ /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
251
+ error: {
252
+ operation: string;
253
+ [key: string]: unknown;
254
+ };
255
+ }
162
256
 
163
257
  declare class CacheNamespace {
164
258
  private readonly cache;
165
259
  private readonly prefix;
166
260
  constructor(cache: CacheStack, prefix: string);
167
261
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
262
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
263
+ has(key: string): Promise<boolean>;
264
+ ttl(key: string): Promise<number | null>;
168
265
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
169
266
  delete(key: string): Promise<void>;
267
+ mdelete(keys: string[]): Promise<void>;
170
268
  clear(): Promise<void>;
171
269
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
172
270
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
@@ -175,40 +273,90 @@ declare class CacheNamespace {
175
273
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
176
274
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
177
275
  getMetrics(): CacheMetricsSnapshot;
276
+ getHitRate(): CacheHitRateSnapshot;
178
277
  qualify(key: string): string;
179
278
  }
180
279
 
280
+ /** Typed overloads for EventEmitter so callers get autocomplete on event names. */
281
+ interface CacheStack {
282
+ on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
283
+ once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
284
+ off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
285
+ emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
286
+ }
181
287
  declare class CacheStack extends EventEmitter {
182
288
  private readonly layers;
183
289
  private readonly options;
184
290
  private readonly stampedeGuard;
185
- private readonly metrics;
291
+ private readonly metricsCollector;
186
292
  private readonly instanceId;
187
293
  private readonly startup;
188
294
  private unsubscribeInvalidation?;
189
295
  private readonly logger;
190
296
  private readonly tagIndex;
191
297
  private readonly backgroundRefreshes;
192
- private readonly accessProfiles;
193
298
  private readonly layerDegradedUntil;
194
- private readonly circuitBreakers;
299
+ private readonly ttlResolver;
300
+ private readonly circuitBreakerManager;
195
301
  private isDisconnecting;
196
302
  private disconnectPromise?;
197
303
  constructor(layers: CacheLayer[], options?: CacheStackOptions);
304
+ /**
305
+ * Read-through cache get.
306
+ * Returns the cached value if present and fresh, or invokes `fetcher` on a miss
307
+ * and stores the result across all layers. Returns `null` if the key is not found
308
+ * and no `fetcher` is provided.
309
+ */
198
310
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
311
+ /**
312
+ * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
313
+ * Fetches and caches the value if not already present.
314
+ */
315
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
316
+ /**
317
+ * Returns true if the given key exists and is not expired in any layer.
318
+ */
319
+ has(key: string): Promise<boolean>;
320
+ /**
321
+ * Returns the remaining TTL in seconds for the key in the fastest layer
322
+ * that has it, or null if the key is not found / has no TTL.
323
+ */
324
+ ttl(key: string): Promise<number | null>;
325
+ /**
326
+ * Stores a value in all cache layers. Overwrites any existing value.
327
+ */
199
328
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
329
+ /**
330
+ * Deletes the key from all layers and publishes an invalidation message.
331
+ */
200
332
  delete(key: string): Promise<void>;
201
333
  clear(): Promise<void>;
334
+ /**
335
+ * Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
336
+ */
337
+ mdelete(keys: string[]): Promise<void>;
202
338
  mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
203
339
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
204
340
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
341
+ /**
342
+ * Returns a cached version of `fetcher`. The cache key is derived from
343
+ * `prefix` plus the serialized arguments unless a `keyResolver` is provided.
344
+ */
205
345
  wrap<TArgs extends unknown[], TResult>(prefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
346
+ /**
347
+ * Creates a `CacheNamespace` that automatically prefixes all keys with
348
+ * `prefix:`. Useful for multi-tenant or module-level isolation.
349
+ */
206
350
  namespace(prefix: string): CacheNamespace;
207
351
  invalidateByTag(tag: string): Promise<void>;
208
352
  invalidateByPattern(pattern: string): Promise<void>;
209
353
  getMetrics(): CacheMetricsSnapshot;
210
354
  getStats(): CacheStatsSnapshot;
211
355
  resetMetrics(): void;
356
+ /**
357
+ * Returns computed hit-rate statistics (overall and per-layer).
358
+ */
359
+ getHitRate(): CacheHitRateSnapshot;
212
360
  exportState(): Promise<CacheSnapshotEntry[]>;
213
361
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
214
362
  persistToFile(filePath: string): Promise<void>;
@@ -226,8 +374,6 @@ declare class CacheStack extends EventEmitter {
226
374
  private executeLayerOperations;
227
375
  private resolveFreshTtl;
228
376
  private resolveLayerSeconds;
229
- private readLayerNumber;
230
- private applyJitter;
231
377
  private shouldNegativeCache;
232
378
  private scheduleBackgroundRefresh;
233
379
  private resolveSingleFlightOptions;
@@ -248,15 +394,10 @@ declare class CacheStack extends EventEmitter {
248
394
  private validateAdaptiveTtlOptions;
249
395
  private validateCircuitBreakerOptions;
250
396
  private applyFreshReadPolicies;
251
- private applyAdaptiveTtl;
252
- private recordAccess;
253
- private incrementMetricMap;
254
397
  private shouldSkipLayer;
255
398
  private handleLayerFailure;
256
399
  private isGracefulDegradationEnabled;
257
- private assertCircuitClosed;
258
400
  private recordCircuitFailure;
259
- private resetCircuitBreaker;
260
401
  private isNegativeStoredValue;
261
402
  private emitError;
262
403
  private serializeKeyPart;
@@ -265,7 +406,17 @@ declare class CacheStack extends EventEmitter {
265
406
  }
266
407
 
267
408
  declare class PatternMatcher {
409
+ /**
410
+ * Tests whether a glob-style pattern matches a value.
411
+ * Supports `*` (any sequence of characters) and `?` (any single character).
412
+ * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
413
+ */
268
414
  static matches(pattern: string, value: string): boolean;
415
+ /**
416
+ * Linear-time glob matching using dynamic programming.
417
+ * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
418
+ */
419
+ private static matchLinear;
269
420
  }
270
421
 
271
422
  interface RedisInvalidationBusOptions {
@@ -369,28 +520,41 @@ interface MemoryLayerSnapshotEntry {
369
520
  value: unknown;
370
521
  expiresAt: number | null;
371
522
  }
523
+ /**
524
+ * Eviction policy applied when `maxSize` is reached.
525
+ * - `lru` (default): evicts the Least Recently Used entry.
526
+ * - `lfu`: evicts the Least Frequently Used entry.
527
+ * - `fifo`: evicts the oldest inserted entry.
528
+ */
529
+ type EvictionPolicy = 'lru' | 'lfu' | 'fifo';
372
530
  interface MemoryLayerOptions {
373
531
  ttl?: number;
374
532
  maxSize?: number;
375
533
  name?: string;
534
+ evictionPolicy?: EvictionPolicy;
376
535
  }
377
536
  declare class MemoryLayer implements CacheLayer {
378
537
  readonly name: string;
379
538
  readonly defaultTtl?: number;
380
539
  readonly isLocal = true;
381
540
  private readonly maxSize;
541
+ private readonly evictionPolicy;
382
542
  private readonly entries;
383
543
  constructor(options?: MemoryLayerOptions);
384
544
  get<T>(key: string): Promise<T | null>;
385
545
  getEntry<T = unknown>(key: string): Promise<T | null>;
386
546
  getMany<T>(keys: string[]): Promise<Array<T | null>>;
387
547
  set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
548
+ has(key: string): Promise<boolean>;
549
+ ttl(key: string): Promise<number | null>;
550
+ size(): Promise<number>;
388
551
  delete(key: string): Promise<void>;
389
552
  deleteMany(keys: string[]): Promise<void>;
390
553
  clear(): Promise<void>;
391
554
  keys(): Promise<string[]>;
392
555
  exportState(): MemoryLayerSnapshotEntry[];
393
556
  importState(entries: MemoryLayerSnapshotEntry[]): void;
557
+ private evict;
394
558
  private pruneExpired;
395
559
  private isExpired;
396
560
  }
@@ -425,6 +589,13 @@ declare class RedisLayer implements CacheLayer {
425
589
  set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
426
590
  delete(key: string): Promise<void>;
427
591
  deleteMany(keys: string[]): Promise<void>;
592
+ has(key: string): Promise<boolean>;
593
+ ttl(key: string): Promise<number | null>;
594
+ size(): Promise<number>;
595
+ /**
596
+ * Deletes all keys matching the layer's prefix in batches to avoid
597
+ * loading millions of keys into memory at once.
598
+ */
428
599
  clear(): Promise<void>;
429
600
  keys(): Promise<string[]>;
430
601
  private scanKeys;
@@ -435,6 +606,94 @@ declare class RedisLayer implements CacheLayer {
435
606
  private decodePayload;
436
607
  }
437
608
 
609
+ interface DiskLayerOptions {
610
+ directory: string;
611
+ ttl?: number;
612
+ name?: string;
613
+ serializer?: CacheSerializer;
614
+ }
615
+ /**
616
+ * A file-system backed cache layer.
617
+ * Each key is stored as a separate JSON file in `directory`.
618
+ * Useful for persisting cache across process restarts without needing Redis.
619
+ *
620
+ * NOTE: DiskLayer is designed for low-to-medium traffic scenarios.
621
+ * For high-throughput workloads, use MemoryLayer + RedisLayer.
622
+ */
623
+ declare class DiskLayer implements CacheLayer {
624
+ readonly name: string;
625
+ readonly defaultTtl?: number;
626
+ readonly isLocal = true;
627
+ private readonly directory;
628
+ private readonly serializer;
629
+ constructor(options: DiskLayerOptions);
630
+ get<T>(key: string): Promise<T | null>;
631
+ getEntry<T = unknown>(key: string): Promise<T | null>;
632
+ set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
633
+ has(key: string): Promise<boolean>;
634
+ ttl(key: string): Promise<number | null>;
635
+ delete(key: string): Promise<void>;
636
+ deleteMany(keys: string[]): Promise<void>;
637
+ clear(): Promise<void>;
638
+ keys(): Promise<string[]>;
639
+ size(): Promise<number>;
640
+ private keyToPath;
641
+ private safeDelete;
642
+ }
643
+
644
+ /**
645
+ * Minimal interface that MemcachedLayer expects from a Memcached client.
646
+ * Compatible with the `memjs` and `memcache-client` npm packages.
647
+ *
648
+ * Install one of:
649
+ * npm install memjs
650
+ * npm install memcache-client
651
+ */
652
+ interface MemcachedClient {
653
+ get(key: string): Promise<{
654
+ value: Buffer | null;
655
+ } | null>;
656
+ set(key: string, value: string | Buffer, options?: {
657
+ expires?: number;
658
+ }): Promise<boolean | undefined>;
659
+ delete(key: string): Promise<boolean | undefined>;
660
+ }
661
+ interface MemcachedLayerOptions {
662
+ client: MemcachedClient;
663
+ ttl?: number;
664
+ name?: string;
665
+ keyPrefix?: string;
666
+ }
667
+ /**
668
+ * Memcached-backed cache layer.
669
+ *
670
+ * Example usage with `memjs`:
671
+ * ```ts
672
+ * import Memjs from 'memjs'
673
+ * import { CacheStack, MemcachedLayer, MemoryLayer } from 'layercache'
674
+ *
675
+ * const memcached = Memjs.Client.create('localhost:11211')
676
+ * const cache = new CacheStack([
677
+ * new MemoryLayer({ ttl: 30 }),
678
+ * new MemcachedLayer({ client: memcached, ttl: 300 })
679
+ * ])
680
+ * ```
681
+ */
682
+ declare class MemcachedLayer implements CacheLayer {
683
+ readonly name: string;
684
+ readonly defaultTtl?: number;
685
+ readonly isLocal = false;
686
+ private readonly client;
687
+ private readonly keyPrefix;
688
+ constructor(options: MemcachedLayerOptions);
689
+ get<T>(key: string): Promise<T | null>;
690
+ set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
691
+ delete(key: string): Promise<void>;
692
+ deleteMany(keys: string[]): Promise<void>;
693
+ clear(): Promise<void>;
694
+ private withPrefix;
695
+ }
696
+
438
697
  declare class JsonSerializer implements CacheSerializer {
439
698
  serialize(value: unknown): string;
440
699
  deserialize<T>(payload: string | Buffer): T;
@@ -462,4 +721,25 @@ declare class StampedeGuard {
462
721
  private getMutexEntry;
463
722
  }
464
723
 
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 };
724
+ /**
725
+ * Returns a function that generates a Prometheus-compatible text exposition
726
+ * of the cache metrics from one or more CacheStack instances.
727
+ *
728
+ * Usage example:
729
+ * ```ts
730
+ * const collect = createPrometheusMetricsExporter(cache)
731
+ * http.createServer(async (_req, res) => {
732
+ * res.setHeader('content-type', 'text/plain; version=0.0.4; charset=utf-8')
733
+ * res.end(collect())
734
+ * }).listen(9091)
735
+ * ```
736
+ *
737
+ * @param stacks One or more CacheStack instances. When multiple stacks are
738
+ * given, each must be named via the optional `name` parameter.
739
+ */
740
+ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
741
+ stack: CacheStack;
742
+ name: string;
743
+ }>): () => string;
744
+
745
+ export { type CacheAdaptiveTtlOptions, type CacheCircuitBreakerOptions, type CacheDegradationOptions, type CacheGetOptions, type CacheHitRateSnapshot, type CacheLayer, type CacheLogger, type CacheMGetEntry, type CacheMSetEntry, type CacheMetricsSnapshot, 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, createFastifyLayercachePlugin, createPrometheusMetricsExporter, createTrpcCacheMiddleware };