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/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
  }
@@ -63,6 +80,15 @@ interface CacheSerializer {
63
80
  serialize(value: unknown): string | Buffer;
64
81
  deserialize<T>(payload: string | Buffer): T;
65
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
+ }
66
92
  /** Snapshot of cumulative cache counters. */
67
93
  interface CacheMetricsSnapshot {
68
94
  hits: number;
@@ -82,6 +108,8 @@ interface CacheMetricsSnapshot {
82
108
  degradedOperations: number;
83
109
  hitsByLayer: Record<string, number>;
84
110
  missesByLayer: Record<string, number>;
111
+ /** Per-layer read latency statistics (sampled from successful reads). */
112
+ latencyByLayer: Record<string, CacheLayerLatency>;
85
113
  /** Timestamp (ms since epoch) when metrics were last reset. */
86
114
  resetAt: number;
87
115
  }
@@ -103,6 +131,8 @@ interface CacheTagIndex {
103
131
  track(key: string, tags: string[]): Promise<void>;
104
132
  remove(key: string): Promise<void>;
105
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[]>;
106
136
  matchPattern(pattern: string): Promise<string[]>;
107
137
  clear(): Promise<void>;
108
138
  }
@@ -205,6 +235,22 @@ interface CacheStatsSnapshot {
205
235
  }>;
206
236
  backgroundRefreshes: number;
207
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
+ }
208
254
  /** All events emitted by CacheStack and their payload shapes. */
209
255
  interface CacheStackEvents {
210
256
  /** Fired on every cache hit. */
@@ -260,6 +306,10 @@ declare class CacheNamespace {
260
306
  constructor(cache: CacheStack, prefix: string);
261
307
  get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
262
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>;
263
313
  has(key: string): Promise<boolean>;
264
314
  ttl(key: string): Promise<number | null>;
265
315
  set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
@@ -270,10 +320,24 @@ declare class CacheNamespace {
270
320
  mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
271
321
  invalidateByTag(tag: string): Promise<void>;
272
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>;
273
327
  wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
274
328
  warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
275
329
  getMetrics(): CacheMetricsSnapshot;
276
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;
277
341
  qualify(key: string): string;
278
342
  }
279
343
 
@@ -313,6 +377,12 @@ declare class CacheStack extends EventEmitter {
313
377
  * Fetches and caches the value if not already present.
314
378
  */
315
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>;
316
386
  /**
317
387
  * Returns true if the given key exists and is not expired in any layer.
318
388
  */
@@ -357,6 +427,12 @@ declare class CacheStack extends EventEmitter {
357
427
  * Returns computed hit-rate statistics (overall and per-layer).
358
428
  */
359
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>;
360
436
  exportState(): Promise<CacheSnapshotEntry[]>;
361
437
  importState(entries: CacheSnapshotEntry[]): Promise<void>;
362
438
  persistToFile(filePath: string): Promise<void>;
@@ -380,6 +456,7 @@ declare class CacheStack extends EventEmitter {
380
456
  private deleteKeys;
381
457
  private publishInvalidation;
382
458
  private handleInvalidationMessage;
459
+ private getTagsForKey;
383
460
  private formatError;
384
461
  private sleep;
385
462
  private shouldBroadcastL1Invalidation;
@@ -424,15 +501,23 @@ interface RedisInvalidationBusOptions {
424
501
  subscriber?: Redis;
425
502
  channel?: string;
426
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
+ */
427
511
  declare class RedisInvalidationBus implements InvalidationBus {
428
512
  private readonly channel;
429
513
  private readonly publisher;
430
514
  private readonly subscriber;
431
- private activeListener?;
515
+ private readonly handlers;
516
+ private sharedListener?;
432
517
  constructor(options: RedisInvalidationBusOptions);
433
518
  subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void>>;
434
519
  publish(message: InvalidationMessage): Promise<void>;
435
- private handleMessage;
520
+ private dispatchToHandlers;
436
521
  private isInvalidationMessage;
437
522
  private reportError;
438
523
  }
@@ -451,6 +536,7 @@ declare class RedisTagIndex implements CacheTagIndex {
451
536
  track(key: string, tags: string[]): Promise<void>;
452
537
  remove(key: string): Promise<void>;
453
538
  keysForTag(tag: string): Promise<string[]>;
539
+ tagsForKey(key: string): Promise<string[]>;
454
540
  matchPattern(pattern: string): Promise<string[]>;
455
541
  clear(): Promise<void>;
456
542
  private scanIndexKeys;
@@ -459,16 +545,28 @@ declare class RedisTagIndex implements CacheTagIndex {
459
545
  private tagKeysKey;
460
546
  }
461
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
+ }
462
556
  declare class TagIndex implements CacheTagIndex {
463
557
  private readonly tagToKeys;
464
558
  private readonly keyToTags;
465
559
  private readonly knownKeys;
560
+ private readonly maxKnownKeys;
561
+ constructor(options?: TagIndexOptions);
466
562
  touch(key: string): Promise<void>;
467
563
  track(key: string, tags: string[]): Promise<void>;
468
564
  remove(key: string): Promise<void>;
469
565
  keysForTag(tag: string): Promise<string[]>;
566
+ tagsForKey(key: string): Promise<string[]>;
470
567
  matchPattern(pattern: string): Promise<string[]>;
471
568
  clear(): Promise<void>;
569
+ private pruneKnownKeysIfNeeded;
472
570
  }
473
571
 
474
572
  declare function createCacheStatsHandler(cache: CacheStack): (_request: unknown, response: {
@@ -493,6 +591,48 @@ interface FastifyLayercachePluginOptions {
493
591
  }
494
592
  declare function createFastifyLayercachePlugin(cache: CacheStack, options?: FastifyLayercachePluginOptions): (fastify: FastifyLike) => Promise<void>;
495
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
+
496
636
  interface GraphqlCacheOptions<TArgs extends unknown[]> extends CacheGetOptions {
497
637
  keyResolver?: (...args: TArgs) => string;
498
638
  }
@@ -602,7 +742,14 @@ declare class RedisLayer implements CacheLayer {
602
742
  private withPrefix;
603
743
  private deserializeOrDelete;
604
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
+ */
605
749
  private encodePayload;
750
+ /**
751
+ * Decompresses the payload asynchronously if a compression header is present.
752
+ */
606
753
  private decodePayload;
607
754
  }
608
755
 
@@ -611,12 +758,21 @@ interface DiskLayerOptions {
611
758
  ttl?: number;
612
759
  name?: string;
613
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;
614
767
  }
615
768
  /**
616
769
  * A file-system backed cache layer.
617
770
  * Each key is stored as a separate JSON file in `directory`.
618
771
  * Useful for persisting cache across process restarts without needing Redis.
619
772
  *
773
+ * - `keys()` returns the original cache key strings (not hashes).
774
+ * - `maxFiles` limits on-disk entries; when exceeded, oldest files are evicted.
775
+ *
620
776
  * NOTE: DiskLayer is designed for low-to-medium traffic scenarios.
621
777
  * For high-throughput workloads, use MemoryLayer + RedisLayer.
622
778
  */
@@ -626,6 +782,7 @@ declare class DiskLayer implements CacheLayer {
626
782
  readonly isLocal = true;
627
783
  private readonly directory;
628
784
  private readonly serializer;
785
+ private readonly maxFiles;
629
786
  constructor(options: DiskLayerOptions);
630
787
  get<T>(key: string): Promise<T | null>;
631
788
  getEntry<T = unknown>(key: string): Promise<T | null>;
@@ -635,10 +792,18 @@ declare class DiskLayer implements CacheLayer {
635
792
  delete(key: string): Promise<void>;
636
793
  deleteMany(keys: string[]): Promise<void>;
637
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
+ */
638
799
  keys(): Promise<string[]>;
639
800
  size(): Promise<number>;
640
801
  private keyToPath;
641
802
  private safeDelete;
803
+ /**
804
+ * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
805
+ */
806
+ private enforceMaxFiles;
642
807
  }
643
808
 
644
809
  /**
@@ -663,10 +828,14 @@ interface MemcachedLayerOptions {
663
828
  ttl?: number;
664
829
  name?: string;
665
830
  keyPrefix?: string;
831
+ serializer?: CacheSerializer;
666
832
  }
667
833
  /**
668
834
  * Memcached-backed cache layer.
669
835
  *
836
+ * Now supports pluggable serializers (default: JSON), StoredValueEnvelope
837
+ * for stale-while-revalidate / stale-if-error semantics, and bulk reads.
838
+ *
670
839
  * Example usage with `memjs`:
671
840
  * ```ts
672
841
  * import Memjs from 'memjs'
@@ -685,9 +854,13 @@ declare class MemcachedLayer implements CacheLayer {
685
854
  readonly isLocal = false;
686
855
  private readonly client;
687
856
  private readonly keyPrefix;
857
+ private readonly serializer;
688
858
  constructor(options: MemcachedLayerOptions);
689
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>>;
690
862
  set(key: string, value: unknown, ttl?: number | undefined): Promise<void>;
863
+ has(key: string): Promise<boolean>;
691
864
  delete(key: string): Promise<void>;
692
865
  deleteMany(keys: string[]): Promise<void>;
693
866
  clear(): Promise<void>;
@@ -725,6 +898,9 @@ declare class StampedeGuard {
725
898
  * Returns a function that generates a Prometheus-compatible text exposition
726
899
  * of the cache metrics from one or more CacheStack instances.
727
900
  *
901
+ * Now includes per-layer latency gauges (`layercache_layer_latency_avg_ms`,
902
+ * `layercache_layer_latency_max_ms`, `layercache_layer_latency_count`).
903
+ *
728
904
  * Usage example:
729
905
  * ```ts
730
906
  * const collect = createPrometheusMetricsExporter(cache)
@@ -742,4 +918,4 @@ declare function createPrometheusMetricsExporter(stacks: CacheStack | Array<{
742
918
  name: string;
743
919
  }>): () => string;
744
920
 
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 };
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 };