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/README.md +97 -5
- package/benchmarks/latency.ts +1 -1
- package/benchmarks/stampede.ts +1 -4
- package/dist/{chunk-IILH5XTS.js → chunk-BWM4MU2X.js} +36 -4
- package/dist/cli.cjs +87 -9
- package/dist/cli.js +52 -6
- package/dist/index.cjs +1219 -272
- package/dist/index.d.cts +469 -13
- package/dist/index.d.ts +469 -13
- package/dist/index.js +1181 -271
- package/examples/express-api/index.ts +12 -8
- package/examples/nestjs-module/app.module.ts +2 -5
- package/examples/nextjs-api-routes/route.ts +1 -4
- package/package.json +6 -1
- package/packages/nestjs/dist/index.cjs +712 -220
- package/packages/nestjs/dist/index.d.cts +243 -11
- package/packages/nestjs/dist/index.d.ts +243 -11
- package/packages/nestjs/dist/index.js +712 -220
package/dist/index.d.ts
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|