layercache 1.0.1 → 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.
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from 'node:events';
1
2
  import { DynamicModule } from '@nestjs/common';
2
3
 
3
4
  declare const CACHE_STACK: unique symbol;
@@ -5,6 +6,32 @@ declare const CACHE_STACK: unique symbol;
5
6
  interface LayerTtlMap {
6
7
  [layerName: string]: number | undefined;
7
8
  }
9
+ interface CacheWriteOptions {
10
+ tags?: string[];
11
+ ttl?: number | LayerTtlMap;
12
+ negativeCache?: boolean;
13
+ negativeTtl?: number | LayerTtlMap;
14
+ staleWhileRevalidate?: number | LayerTtlMap;
15
+ staleIfError?: number | LayerTtlMap;
16
+ ttlJitter?: number | LayerTtlMap;
17
+ slidingTtl?: boolean;
18
+ refreshAhead?: number | LayerTtlMap;
19
+ adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
20
+ circuitBreaker?: CacheCircuitBreakerOptions;
21
+ }
22
+ interface CacheGetOptions extends CacheWriteOptions {
23
+ }
24
+ interface CacheMGetEntry<T> {
25
+ key: string;
26
+ fetch?: () => Promise<T>;
27
+ options?: CacheGetOptions;
28
+ }
29
+ interface CacheMSetEntry<T> {
30
+ key: string;
31
+ value: T;
32
+ options?: CacheWriteOptions;
33
+ }
34
+ /** Interface that all cache backend implementations must satisfy. */
8
35
  interface CacheLayer {
9
36
  readonly name: string;
10
37
  readonly defaultTtl?: number;
@@ -17,9 +44,57 @@ interface CacheLayer {
17
44
  clear(): Promise<void>;
18
45
  deleteMany?(keys: string[]): Promise<void>;
19
46
  keys?(): Promise<string[]>;
47
+ /**
48
+ * Returns true if the key exists and has not expired.
49
+ * Implementations may omit this; CacheStack will fall back to `get()`.
50
+ */
51
+ has?(key: string): Promise<boolean>;
52
+ /**
53
+ * Returns the remaining TTL in seconds for the key, or null if the key
54
+ * does not exist, has no TTL, or has already expired.
55
+ * Implementations may omit this.
56
+ */
57
+ ttl?(key: string): Promise<number | null>;
58
+ /**
59
+ * Returns the number of entries currently held by this layer.
60
+ * Implementations may omit this.
61
+ */
62
+ size?(): Promise<number>;
63
+ }
64
+ /** Snapshot of cumulative cache counters. */
65
+ interface CacheMetricsSnapshot {
66
+ hits: number;
67
+ misses: number;
68
+ fetches: number;
69
+ sets: number;
70
+ deletes: number;
71
+ backfills: number;
72
+ invalidations: number;
73
+ staleHits: number;
74
+ refreshes: number;
75
+ refreshErrors: number;
76
+ writeFailures: number;
77
+ singleFlightWaits: number;
78
+ negativeCacheHits: number;
79
+ circuitBreakerTrips: number;
80
+ degradedOperations: number;
81
+ hitsByLayer: Record<string, number>;
82
+ missesByLayer: Record<string, number>;
83
+ /** Timestamp (ms since epoch) when metrics were last reset. */
84
+ resetAt: number;
85
+ }
86
+ /** Computed hit-rate statistics derived from CacheMetricsSnapshot. */
87
+ interface CacheHitRateSnapshot {
88
+ /** Overall hit rate across all layers (0–1). */
89
+ overall: number;
90
+ /** Per-layer hit rates (0–1 each). */
91
+ byLayer: Record<string, number>;
20
92
  }
21
93
  interface CacheLogger {
22
- debug(message: string, context?: Record<string, unknown>): void;
94
+ debug?(message: string, context?: Record<string, unknown>): void;
95
+ info?(message: string, context?: Record<string, unknown>): void;
96
+ warn?(message: string, context?: Record<string, unknown>): void;
97
+ error?(message: string, context?: Record<string, unknown>): void;
23
98
  }
24
99
  interface CacheTagIndex {
25
100
  touch(key: string): Promise<void>;
@@ -53,18 +128,286 @@ interface CacheStackOptions {
53
128
  stampedePrevention?: boolean;
54
129
  invalidationBus?: InvalidationBus;
55
130
  tagIndex?: CacheTagIndex;
131
+ broadcastL1Invalidation?: boolean;
132
+ /**
133
+ * @deprecated Use `broadcastL1Invalidation` instead.
134
+ */
56
135
  publishSetInvalidation?: boolean;
57
136
  negativeCaching?: boolean;
58
137
  negativeTtl?: number | LayerTtlMap;
59
138
  staleWhileRevalidate?: number | LayerTtlMap;
60
139
  staleIfError?: number | LayerTtlMap;
61
140
  ttlJitter?: number | LayerTtlMap;
141
+ refreshAhead?: number | LayerTtlMap;
142
+ adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
143
+ circuitBreaker?: CacheCircuitBreakerOptions;
144
+ gracefulDegradation?: boolean | CacheDegradationOptions;
62
145
  writePolicy?: 'strict' | 'best-effort';
63
146
  singleFlightCoordinator?: CacheSingleFlightCoordinator;
64
147
  singleFlightLeaseMs?: number;
65
148
  singleFlightTimeoutMs?: number;
66
149
  singleFlightPollMs?: number;
150
+ /**
151
+ * Maximum number of entries in `accessProfiles` and `circuitBreakers` maps
152
+ * before the oldest entries are pruned. Prevents unbounded memory growth.
153
+ * Defaults to 100 000.
154
+ */
155
+ maxProfileEntries?: number;
156
+ }
157
+ interface CacheAdaptiveTtlOptions {
158
+ hotAfter?: number;
159
+ step?: number | LayerTtlMap;
160
+ maxTtl?: number | LayerTtlMap;
161
+ }
162
+ interface CacheCircuitBreakerOptions {
163
+ failureThreshold?: number;
164
+ cooldownMs?: number;
165
+ }
166
+ interface CacheDegradationOptions {
167
+ retryAfterMs?: number;
168
+ }
169
+ interface CacheWarmEntry<T = unknown> {
170
+ key: string;
171
+ fetcher: () => Promise<T>;
172
+ options?: CacheGetOptions;
173
+ priority?: number;
174
+ }
175
+ /** Options controlling the cache warm-up process. */
176
+ interface CacheWarmOptions {
177
+ concurrency?: number;
178
+ continueOnError?: boolean;
179
+ /** Called after each entry is processed (success or failure). */
180
+ onProgress?: (progress: CacheWarmProgress) => void;
181
+ }
182
+ /** Progress information delivered to `CacheWarmOptions.onProgress`. */
183
+ interface CacheWarmProgress {
184
+ completed: number;
185
+ total: number;
186
+ key: string;
187
+ success: boolean;
188
+ }
189
+ interface CacheWrapOptions<TArgs extends unknown[] = unknown[]> extends CacheGetOptions {
190
+ keyResolver?: (...args: TArgs) => string;
191
+ }
192
+ interface CacheSnapshotEntry {
193
+ key: string;
194
+ value: unknown;
195
+ ttl?: number;
196
+ }
197
+ interface CacheStatsSnapshot {
198
+ metrics: CacheMetricsSnapshot;
199
+ layers: Array<{
200
+ name: string;
201
+ isLocal: boolean;
202
+ degradedUntil: number | null;
203
+ }>;
204
+ backgroundRefreshes: number;
205
+ }
206
+ /** All events emitted by CacheStack and their payload shapes. */
207
+ interface CacheStackEvents {
208
+ /** Fired on every cache hit. */
209
+ hit: {
210
+ key: string;
211
+ layer: string;
212
+ state: 'fresh' | 'stale-while-revalidate' | 'stale-if-error';
213
+ };
214
+ /** Fired on every cache miss before the fetcher runs. */
215
+ miss: {
216
+ key: string;
217
+ mode: string;
218
+ };
219
+ /** Fired after a value is stored in the cache. */
220
+ set: {
221
+ key: string;
222
+ kind: string;
223
+ tags?: string[];
224
+ };
225
+ /** Fired after one or more keys are deleted. */
226
+ delete: {
227
+ keys: string[];
228
+ };
229
+ /** Fired when a value is backfilled into a faster layer. */
230
+ backfill: {
231
+ key: string;
232
+ layer: string;
233
+ };
234
+ /** Fired when a stale value is returned to the caller. */
235
+ 'stale-serve': {
236
+ key: string;
237
+ state: string;
238
+ layer: string;
239
+ };
240
+ /** Fired when a duplicate request is deduplicated in stampede prevention. */
241
+ 'stampede-dedupe': {
242
+ key: string;
243
+ };
244
+ /** Fired after a key is successfully warmed. */
245
+ warm: {
246
+ key: string;
247
+ };
248
+ /** Fired when an error occurs (layer failure, circuit breaker, etc.). */
249
+ error: {
250
+ operation: string;
251
+ [key: string]: unknown;
252
+ };
253
+ }
254
+
255
+ declare class CacheNamespace {
256
+ private readonly cache;
257
+ private readonly prefix;
258
+ constructor(cache: CacheStack, prefix: string);
259
+ get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
260
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
261
+ has(key: string): Promise<boolean>;
262
+ ttl(key: string): Promise<number | null>;
263
+ set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
264
+ delete(key: string): Promise<void>;
265
+ mdelete(keys: string[]): Promise<void>;
266
+ clear(): Promise<void>;
267
+ mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
268
+ mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
269
+ invalidateByTag(tag: string): Promise<void>;
270
+ invalidateByPattern(pattern: string): Promise<void>;
271
+ wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
272
+ warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
273
+ getMetrics(): CacheMetricsSnapshot;
274
+ getHitRate(): CacheHitRateSnapshot;
275
+ qualify(key: string): string;
276
+ }
277
+
278
+ /** Typed overloads for EventEmitter so callers get autocomplete on event names. */
279
+ interface CacheStack {
280
+ on<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
281
+ once<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
282
+ off<K extends keyof CacheStackEvents>(event: K, listener: (data: CacheStackEvents[K]) => void): this;
283
+ emit<K extends keyof CacheStackEvents>(event: K, data: CacheStackEvents[K]): boolean;
284
+ }
285
+ declare class CacheStack extends EventEmitter {
286
+ private readonly layers;
287
+ private readonly options;
288
+ private readonly stampedeGuard;
289
+ private readonly metricsCollector;
290
+ private readonly instanceId;
291
+ private readonly startup;
292
+ private unsubscribeInvalidation?;
293
+ private readonly logger;
294
+ private readonly tagIndex;
295
+ private readonly backgroundRefreshes;
296
+ private readonly layerDegradedUntil;
297
+ private readonly ttlResolver;
298
+ private readonly circuitBreakerManager;
299
+ private isDisconnecting;
300
+ private disconnectPromise?;
301
+ constructor(layers: CacheLayer[], options?: CacheStackOptions);
302
+ /**
303
+ * Read-through cache get.
304
+ * Returns the cached value if present and fresh, or invokes `fetcher` on a miss
305
+ * and stores the result across all layers. Returns `null` if the key is not found
306
+ * and no `fetcher` is provided.
307
+ */
308
+ get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
309
+ /**
310
+ * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
311
+ * Fetches and caches the value if not already present.
312
+ */
313
+ getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
314
+ /**
315
+ * Returns true if the given key exists and is not expired in any layer.
316
+ */
317
+ has(key: string): Promise<boolean>;
318
+ /**
319
+ * Returns the remaining TTL in seconds for the key in the fastest layer
320
+ * that has it, or null if the key is not found / has no TTL.
321
+ */
322
+ ttl(key: string): Promise<number | null>;
323
+ /**
324
+ * Stores a value in all cache layers. Overwrites any existing value.
325
+ */
326
+ set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
327
+ /**
328
+ * Deletes the key from all layers and publishes an invalidation message.
329
+ */
330
+ delete(key: string): Promise<void>;
331
+ clear(): Promise<void>;
332
+ /**
333
+ * Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
334
+ */
335
+ mdelete(keys: string[]): Promise<void>;
336
+ mget<T>(entries: CacheMGetEntry<T>[]): Promise<Array<T | null>>;
337
+ mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
338
+ warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
339
+ /**
340
+ * Returns a cached version of `fetcher`. The cache key is derived from
341
+ * `prefix` plus the serialized arguments unless a `keyResolver` is provided.
342
+ */
343
+ wrap<TArgs extends unknown[], TResult>(prefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
344
+ /**
345
+ * Creates a `CacheNamespace` that automatically prefixes all keys with
346
+ * `prefix:`. Useful for multi-tenant or module-level isolation.
347
+ */
348
+ namespace(prefix: string): CacheNamespace;
349
+ invalidateByTag(tag: string): Promise<void>;
350
+ invalidateByPattern(pattern: string): Promise<void>;
351
+ getMetrics(): CacheMetricsSnapshot;
352
+ getStats(): CacheStatsSnapshot;
353
+ resetMetrics(): void;
354
+ /**
355
+ * Returns computed hit-rate statistics (overall and per-layer).
356
+ */
357
+ getHitRate(): CacheHitRateSnapshot;
358
+ exportState(): Promise<CacheSnapshotEntry[]>;
359
+ importState(entries: CacheSnapshotEntry[]): Promise<void>;
360
+ persistToFile(filePath: string): Promise<void>;
361
+ restoreFromFile(filePath: string): Promise<void>;
362
+ disconnect(): Promise<void>;
363
+ private initialize;
364
+ private fetchWithGuards;
365
+ private waitForFreshValue;
366
+ private fetchAndPopulate;
367
+ private storeEntry;
368
+ private readFromLayers;
369
+ private readLayerEntry;
370
+ private backfill;
371
+ private writeAcrossLayers;
372
+ private executeLayerOperations;
373
+ private resolveFreshTtl;
374
+ private resolveLayerSeconds;
375
+ private shouldNegativeCache;
376
+ private scheduleBackgroundRefresh;
377
+ private resolveSingleFlightOptions;
378
+ private deleteKeys;
379
+ private publishInvalidation;
380
+ private handleInvalidationMessage;
381
+ private formatError;
382
+ private sleep;
383
+ private shouldBroadcastL1Invalidation;
384
+ private deleteKeysFromLayers;
385
+ private validateConfiguration;
386
+ private validateWriteOptions;
387
+ private validateLayerNumberOption;
388
+ private validatePositiveNumber;
389
+ private validateNonNegativeNumber;
390
+ private validateCacheKey;
391
+ private serializeOptions;
392
+ private validateAdaptiveTtlOptions;
393
+ private validateCircuitBreakerOptions;
394
+ private applyFreshReadPolicies;
395
+ private shouldSkipLayer;
396
+ private handleLayerFailure;
397
+ private isGracefulDegradationEnabled;
398
+ private recordCircuitFailure;
399
+ private isNegativeStoredValue;
400
+ private emitError;
401
+ private serializeKeyPart;
402
+ private isCacheSnapshotEntries;
403
+ private normalizeForSerialization;
404
+ }
405
+
406
+ interface CacheableOptions<TArgs extends unknown[]> extends CacheWrapOptions<TArgs> {
407
+ cache: (instance: unknown) => CacheStack;
408
+ prefix?: string;
67
409
  }
410
+ declare function Cacheable<TArgs extends unknown[] = unknown[]>(options: CacheableOptions<TArgs>): MethodDecorator;
68
411
 
69
412
  interface CacheStackModuleOptions {
70
413
  layers: CacheLayer[];
@@ -75,4 +418,4 @@ declare class CacheStackModule {
75
418
  static forRoot(options: CacheStackModuleOptions): DynamicModule;
76
419
  }
77
420
 
78
- export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, InjectCacheStack };
421
+ export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };