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/README.md +97 -5
- package/dist/{chunk-QUB5VZFZ.js → chunk-BWM4MU2X.js} +3 -0
- package/dist/cli.cjs +12 -2
- package/dist/cli.js +10 -3
- package/dist/index.cjs +333 -40
- package/dist/index.d.cts +179 -3
- package/dist/index.d.ts +179 -3
- package/dist/index.js +330 -42
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +162 -2
- package/packages/nestjs/dist/index.d.cts +92 -1
- package/packages/nestjs/dist/index.d.ts +92 -1
- package/packages/nestjs/dist/index.js +162 -2
package/package.json
CHANGED
|
@@ -77,7 +77,7 @@ var import_node_events = require("events");
|
|
|
77
77
|
var import_node_fs = require("fs");
|
|
78
78
|
|
|
79
79
|
// ../../src/CacheNamespace.ts
|
|
80
|
-
var CacheNamespace = class {
|
|
80
|
+
var CacheNamespace = class _CacheNamespace {
|
|
81
81
|
constructor(cache, prefix) {
|
|
82
82
|
this.cache = cache;
|
|
83
83
|
this.prefix = prefix;
|
|
@@ -90,6 +90,12 @@ var CacheNamespace = class {
|
|
|
90
90
|
async getOrSet(key, fetcher, options) {
|
|
91
91
|
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
95
|
+
*/
|
|
96
|
+
async getOrThrow(key, fetcher, options) {
|
|
97
|
+
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
98
|
+
}
|
|
93
99
|
async has(key) {
|
|
94
100
|
return this.cache.has(this.qualify(key));
|
|
95
101
|
}
|
|
@@ -130,6 +136,12 @@ var CacheNamespace = class {
|
|
|
130
136
|
async invalidateByPattern(pattern) {
|
|
131
137
|
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
132
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
141
|
+
*/
|
|
142
|
+
async inspect(key) {
|
|
143
|
+
return this.cache.inspect(this.qualify(key));
|
|
144
|
+
}
|
|
133
145
|
wrap(keyPrefix, fetcher, options) {
|
|
134
146
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
135
147
|
}
|
|
@@ -148,6 +160,18 @@ var CacheNamespace = class {
|
|
|
148
160
|
getHitRate() {
|
|
149
161
|
return this.cache.getHitRate();
|
|
150
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
165
|
+
*
|
|
166
|
+
* ```ts
|
|
167
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
168
|
+
* const posts = tenant.namespace('posts')
|
|
169
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
namespace(childPrefix) {
|
|
173
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
174
|
+
}
|
|
151
175
|
qualify(key) {
|
|
152
176
|
return `${this.prefix}:${key}`;
|
|
153
177
|
}
|
|
@@ -249,7 +273,12 @@ var CircuitBreakerManager = class {
|
|
|
249
273
|
var MetricsCollector = class {
|
|
250
274
|
data = this.empty();
|
|
251
275
|
get snapshot() {
|
|
252
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
...this.data,
|
|
278
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
279
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
280
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
281
|
+
};
|
|
253
282
|
}
|
|
254
283
|
increment(field, amount = 1) {
|
|
255
284
|
;
|
|
@@ -258,6 +287,22 @@ var MetricsCollector = class {
|
|
|
258
287
|
incrementLayer(map, layerName) {
|
|
259
288
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
260
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Records a read latency sample for the given layer.
|
|
292
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
293
|
+
*/
|
|
294
|
+
recordLatency(layerName, durationMs) {
|
|
295
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
296
|
+
if (!existing) {
|
|
297
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
existing.count += 1;
|
|
301
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
302
|
+
if (durationMs > existing.maxMs) {
|
|
303
|
+
existing.maxMs = durationMs;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
261
306
|
reset() {
|
|
262
307
|
this.data = this.empty();
|
|
263
308
|
}
|
|
@@ -292,6 +337,7 @@ var MetricsCollector = class {
|
|
|
292
337
|
degradedOperations: 0,
|
|
293
338
|
hitsByLayer: {},
|
|
294
339
|
missesByLayer: {},
|
|
340
|
+
latencyByLayer: {},
|
|
295
341
|
resetAt: Date.now()
|
|
296
342
|
};
|
|
297
343
|
}
|
|
@@ -529,11 +575,17 @@ var TagIndex = class {
|
|
|
529
575
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
530
576
|
keyToTags = /* @__PURE__ */ new Map();
|
|
531
577
|
knownKeys = /* @__PURE__ */ new Set();
|
|
578
|
+
maxKnownKeys;
|
|
579
|
+
constructor(options = {}) {
|
|
580
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
581
|
+
}
|
|
532
582
|
async touch(key) {
|
|
533
583
|
this.knownKeys.add(key);
|
|
584
|
+
this.pruneKnownKeysIfNeeded();
|
|
534
585
|
}
|
|
535
586
|
async track(key, tags) {
|
|
536
587
|
this.knownKeys.add(key);
|
|
588
|
+
this.pruneKnownKeysIfNeeded();
|
|
537
589
|
if (tags.length === 0) {
|
|
538
590
|
return;
|
|
539
591
|
}
|
|
@@ -572,6 +624,9 @@ var TagIndex = class {
|
|
|
572
624
|
async keysForTag(tag) {
|
|
573
625
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
574
626
|
}
|
|
627
|
+
async tagsForKey(key) {
|
|
628
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
629
|
+
}
|
|
575
630
|
async matchPattern(pattern) {
|
|
576
631
|
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
577
632
|
}
|
|
@@ -580,6 +635,21 @@ var TagIndex = class {
|
|
|
580
635
|
this.keyToTags.clear();
|
|
581
636
|
this.knownKeys.clear();
|
|
582
637
|
}
|
|
638
|
+
pruneKnownKeysIfNeeded() {
|
|
639
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
643
|
+
let removed = 0;
|
|
644
|
+
for (const key of this.knownKeys) {
|
|
645
|
+
if (removed >= toRemove) {
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
this.knownKeys.delete(key);
|
|
649
|
+
this.keyToTags.delete(key);
|
|
650
|
+
removed += 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
583
653
|
};
|
|
584
654
|
|
|
585
655
|
// ../../node_modules/async-mutex/index.mjs
|
|
@@ -782,6 +852,16 @@ var StampedeGuard = class {
|
|
|
782
852
|
}
|
|
783
853
|
};
|
|
784
854
|
|
|
855
|
+
// ../../src/types.ts
|
|
856
|
+
var CacheMissError = class extends Error {
|
|
857
|
+
key;
|
|
858
|
+
constructor(key) {
|
|
859
|
+
super(`Cache miss for key "${key}".`);
|
|
860
|
+
this.name = "CacheMissError";
|
|
861
|
+
this.key = key;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
785
865
|
// ../../src/CacheStack.ts
|
|
786
866
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
787
867
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
@@ -908,6 +988,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
908
988
|
async getOrSet(key, fetcher, options) {
|
|
909
989
|
return this.get(key, fetcher, options);
|
|
910
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
993
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
994
|
+
* return non-null.
|
|
995
|
+
*/
|
|
996
|
+
async getOrThrow(key, fetcher, options) {
|
|
997
|
+
const value = await this.get(key, fetcher, options);
|
|
998
|
+
if (value === null) {
|
|
999
|
+
throw new CacheMissError(key);
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
911
1003
|
/**
|
|
912
1004
|
* Returns true if the given key exists and is not expired in any layer.
|
|
913
1005
|
*/
|
|
@@ -1181,6 +1273,46 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1181
1273
|
getHitRate() {
|
|
1182
1274
|
return this.metricsCollector.hitRate();
|
|
1183
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1278
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1279
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1280
|
+
*/
|
|
1281
|
+
async inspect(key) {
|
|
1282
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1283
|
+
await this.startup;
|
|
1284
|
+
const foundInLayers = [];
|
|
1285
|
+
let freshTtlSeconds = null;
|
|
1286
|
+
let staleTtlSeconds = null;
|
|
1287
|
+
let errorTtlSeconds = null;
|
|
1288
|
+
let isStale = false;
|
|
1289
|
+
for (const layer of this.layers) {
|
|
1290
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1294
|
+
if (stored === null) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const resolved = resolveStoredValue(stored);
|
|
1298
|
+
if (resolved.state === "expired") {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
foundInLayers.push(layer.name);
|
|
1302
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1303
|
+
const now = Date.now();
|
|
1304
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1305
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1306
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1307
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (foundInLayers.length === 0) {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1314
|
+
return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1315
|
+
}
|
|
1184
1316
|
async exportState() {
|
|
1185
1317
|
await this.startup;
|
|
1186
1318
|
const exported = /* @__PURE__ */ new Map();
|
|
@@ -1317,6 +1449,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1317
1449
|
await this.storeEntry(key, "empty", null, options);
|
|
1318
1450
|
return null;
|
|
1319
1451
|
}
|
|
1452
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1453
|
+
return fetched;
|
|
1454
|
+
}
|
|
1320
1455
|
await this.storeEntry(key, "value", fetched, options);
|
|
1321
1456
|
return fetched;
|
|
1322
1457
|
}
|
|
@@ -1339,7 +1474,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1339
1474
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
1340
1475
|
const layer = this.layers[index];
|
|
1341
1476
|
if (!layer) continue;
|
|
1477
|
+
const readStart = performance.now();
|
|
1342
1478
|
const stored = await this.readLayerEntry(layer, key);
|
|
1479
|
+
const readDuration = performance.now() - readStart;
|
|
1480
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1343
1481
|
if (stored === null) {
|
|
1344
1482
|
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1345
1483
|
continue;
|
|
@@ -1541,6 +1679,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1541
1679
|
}
|
|
1542
1680
|
}
|
|
1543
1681
|
}
|
|
1682
|
+
async getTagsForKey(key) {
|
|
1683
|
+
if (this.tagIndex.tagsForKey) {
|
|
1684
|
+
return this.tagIndex.tagsForKey(key);
|
|
1685
|
+
}
|
|
1686
|
+
return [];
|
|
1687
|
+
}
|
|
1544
1688
|
formatError(error) {
|
|
1545
1689
|
if (error instanceof Error) {
|
|
1546
1690
|
return error.message;
|
|
@@ -1771,6 +1915,22 @@ var CacheStackModule = class {
|
|
|
1771
1915
|
exports: [provider]
|
|
1772
1916
|
};
|
|
1773
1917
|
}
|
|
1918
|
+
static forRootAsync(options) {
|
|
1919
|
+
const provider = {
|
|
1920
|
+
provide: CACHE_STACK,
|
|
1921
|
+
inject: options.inject ?? [],
|
|
1922
|
+
useFactory: async (...args) => {
|
|
1923
|
+
const resolved = await options.useFactory(...args);
|
|
1924
|
+
return new CacheStack(resolved.layers, resolved.bridgeOptions);
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
return {
|
|
1928
|
+
global: true,
|
|
1929
|
+
module: CacheStackModule,
|
|
1930
|
+
providers: [provider],
|
|
1931
|
+
exports: [provider]
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1774
1934
|
};
|
|
1775
1935
|
CacheStackModule = __decorateClass([
|
|
1776
1936
|
(0, import_common.Global)(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { DynamicModule, Type } from '@nestjs/common';
|
|
3
3
|
|
|
4
4
|
declare const CACHE_STACK: unique symbol;
|
|
5
5
|
|
|
@@ -18,6 +18,15 @@ interface CacheWriteOptions {
|
|
|
18
18
|
refreshAhead?: number | LayerTtlMap;
|
|
19
19
|
adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
|
|
20
20
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
21
|
+
/**
|
|
22
|
+
* Optional predicate called with the fetcher's return value before caching.
|
|
23
|
+
* Return `false` to skip storing the value in the cache (but still return it
|
|
24
|
+
* to the caller). Useful for not caching failed API responses or empty results.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
|
|
28
|
+
*/
|
|
29
|
+
shouldCache?: (value: unknown) => boolean;
|
|
21
30
|
}
|
|
22
31
|
interface CacheGetOptions extends CacheWriteOptions {
|
|
23
32
|
}
|
|
@@ -61,6 +70,15 @@ interface CacheLayer {
|
|
|
61
70
|
*/
|
|
62
71
|
size?(): Promise<number>;
|
|
63
72
|
}
|
|
73
|
+
/** Per-layer latency statistics (rolling window of sampled read durations). */
|
|
74
|
+
interface CacheLayerLatency {
|
|
75
|
+
/** Average read latency in milliseconds. */
|
|
76
|
+
avgMs: number;
|
|
77
|
+
/** Maximum observed read latency in milliseconds. */
|
|
78
|
+
maxMs: number;
|
|
79
|
+
/** Number of samples used to compute the statistics. */
|
|
80
|
+
count: number;
|
|
81
|
+
}
|
|
64
82
|
/** Snapshot of cumulative cache counters. */
|
|
65
83
|
interface CacheMetricsSnapshot {
|
|
66
84
|
hits: number;
|
|
@@ -80,6 +98,8 @@ interface CacheMetricsSnapshot {
|
|
|
80
98
|
degradedOperations: number;
|
|
81
99
|
hitsByLayer: Record<string, number>;
|
|
82
100
|
missesByLayer: Record<string, number>;
|
|
101
|
+
/** Per-layer read latency statistics (sampled from successful reads). */
|
|
102
|
+
latencyByLayer: Record<string, CacheLayerLatency>;
|
|
83
103
|
/** Timestamp (ms since epoch) when metrics were last reset. */
|
|
84
104
|
resetAt: number;
|
|
85
105
|
}
|
|
@@ -101,6 +121,8 @@ interface CacheTagIndex {
|
|
|
101
121
|
track(key: string, tags: string[]): Promise<void>;
|
|
102
122
|
remove(key: string): Promise<void>;
|
|
103
123
|
keysForTag(tag: string): Promise<string[]>;
|
|
124
|
+
/** Returns the tags associated with a specific key, or an empty array. */
|
|
125
|
+
tagsForKey?(key: string): Promise<string[]>;
|
|
104
126
|
matchPattern(pattern: string): Promise<string[]>;
|
|
105
127
|
clear(): Promise<void>;
|
|
106
128
|
}
|
|
@@ -203,6 +225,22 @@ interface CacheStatsSnapshot {
|
|
|
203
225
|
}>;
|
|
204
226
|
backgroundRefreshes: number;
|
|
205
227
|
}
|
|
228
|
+
/** Detailed inspection result for a single cache key. */
|
|
229
|
+
interface CacheInspectResult {
|
|
230
|
+
key: string;
|
|
231
|
+
/** Layers in which the key is currently stored (not expired). */
|
|
232
|
+
foundInLayers: string[];
|
|
233
|
+
/** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
|
|
234
|
+
freshTtlSeconds: number | null;
|
|
235
|
+
/** Remaining stale-while-revalidate window in seconds, or null. */
|
|
236
|
+
staleTtlSeconds: number | null;
|
|
237
|
+
/** Remaining stale-if-error window in seconds, or null. */
|
|
238
|
+
errorTtlSeconds: number | null;
|
|
239
|
+
/** Whether the key is currently serving stale-while-revalidate. */
|
|
240
|
+
isStale: boolean;
|
|
241
|
+
/** Tags associated with this key (from the TagIndex). */
|
|
242
|
+
tags: string[];
|
|
243
|
+
}
|
|
206
244
|
/** All events emitted by CacheStack and their payload shapes. */
|
|
207
245
|
interface CacheStackEvents {
|
|
208
246
|
/** Fired on every cache hit. */
|
|
@@ -258,6 +296,10 @@ declare class CacheNamespace {
|
|
|
258
296
|
constructor(cache: CacheStack, prefix: string);
|
|
259
297
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
260
298
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
299
|
+
/**
|
|
300
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
301
|
+
*/
|
|
302
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
261
303
|
has(key: string): Promise<boolean>;
|
|
262
304
|
ttl(key: string): Promise<number | null>;
|
|
263
305
|
set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
|
|
@@ -268,10 +310,24 @@ declare class CacheNamespace {
|
|
|
268
310
|
mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
|
|
269
311
|
invalidateByTag(tag: string): Promise<void>;
|
|
270
312
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
313
|
+
/**
|
|
314
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
315
|
+
*/
|
|
316
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
271
317
|
wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
|
|
272
318
|
warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
|
|
273
319
|
getMetrics(): CacheMetricsSnapshot;
|
|
274
320
|
getHitRate(): CacheHitRateSnapshot;
|
|
321
|
+
/**
|
|
322
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
323
|
+
*
|
|
324
|
+
* ```ts
|
|
325
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
326
|
+
* const posts = tenant.namespace('posts')
|
|
327
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
namespace(childPrefix: string): CacheNamespace;
|
|
275
331
|
qualify(key: string): string;
|
|
276
332
|
}
|
|
277
333
|
|
|
@@ -311,6 +367,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
311
367
|
* Fetches and caches the value if not already present.
|
|
312
368
|
*/
|
|
313
369
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
370
|
+
/**
|
|
371
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
372
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
373
|
+
* return non-null.
|
|
374
|
+
*/
|
|
375
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
314
376
|
/**
|
|
315
377
|
* Returns true if the given key exists and is not expired in any layer.
|
|
316
378
|
*/
|
|
@@ -355,6 +417,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
355
417
|
* Returns computed hit-rate statistics (overall and per-layer).
|
|
356
418
|
*/
|
|
357
419
|
getHitRate(): CacheHitRateSnapshot;
|
|
420
|
+
/**
|
|
421
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
422
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
423
|
+
* Returns `null` if the key does not exist in any layer.
|
|
424
|
+
*/
|
|
425
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
358
426
|
exportState(): Promise<CacheSnapshotEntry[]>;
|
|
359
427
|
importState(entries: CacheSnapshotEntry[]): Promise<void>;
|
|
360
428
|
persistToFile(filePath: string): Promise<void>;
|
|
@@ -378,6 +446,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
378
446
|
private deleteKeys;
|
|
379
447
|
private publishInvalidation;
|
|
380
448
|
private handleInvalidationMessage;
|
|
449
|
+
private getTagsForKey;
|
|
381
450
|
private formatError;
|
|
382
451
|
private sleep;
|
|
383
452
|
private shouldBroadcastL1Invalidation;
|
|
@@ -413,9 +482,31 @@ interface CacheStackModuleOptions {
|
|
|
413
482
|
layers: CacheLayer[];
|
|
414
483
|
bridgeOptions?: CacheStackOptions;
|
|
415
484
|
}
|
|
485
|
+
interface CacheStackModuleAsyncOptions {
|
|
486
|
+
/**
|
|
487
|
+
* Tokens to inject into the `useFactory` function.
|
|
488
|
+
*/
|
|
489
|
+
inject?: Array<Type | string | symbol>;
|
|
490
|
+
/**
|
|
491
|
+
* Async factory function that returns `CacheStackModuleOptions`.
|
|
492
|
+
* Useful when the Redis client or other dependencies must be resolved
|
|
493
|
+
* from the NestJS DI container first.
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* CacheStackModule.forRootAsync({
|
|
497
|
+
* inject: [ConfigService],
|
|
498
|
+
* useFactory: (config: ConfigService) => ({
|
|
499
|
+
* layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
|
|
500
|
+
* })
|
|
501
|
+
* })
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
|
|
505
|
+
}
|
|
416
506
|
declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
|
|
417
507
|
declare class CacheStackModule {
|
|
418
508
|
static forRoot(options: CacheStackModuleOptions): DynamicModule;
|
|
509
|
+
static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
|
|
419
510
|
}
|
|
420
511
|
|
|
421
512
|
export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { DynamicModule, Type } from '@nestjs/common';
|
|
3
3
|
|
|
4
4
|
declare const CACHE_STACK: unique symbol;
|
|
5
5
|
|
|
@@ -18,6 +18,15 @@ interface CacheWriteOptions {
|
|
|
18
18
|
refreshAhead?: number | LayerTtlMap;
|
|
19
19
|
adaptiveTtl?: boolean | CacheAdaptiveTtlOptions;
|
|
20
20
|
circuitBreaker?: CacheCircuitBreakerOptions;
|
|
21
|
+
/**
|
|
22
|
+
* Optional predicate called with the fetcher's return value before caching.
|
|
23
|
+
* Return `false` to skip storing the value in the cache (but still return it
|
|
24
|
+
* to the caller). Useful for not caching failed API responses or empty results.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* cache.get('key', fetchData, { shouldCache: (v) => v.status === 200 })
|
|
28
|
+
*/
|
|
29
|
+
shouldCache?: (value: unknown) => boolean;
|
|
21
30
|
}
|
|
22
31
|
interface CacheGetOptions extends CacheWriteOptions {
|
|
23
32
|
}
|
|
@@ -61,6 +70,15 @@ interface CacheLayer {
|
|
|
61
70
|
*/
|
|
62
71
|
size?(): Promise<number>;
|
|
63
72
|
}
|
|
73
|
+
/** Per-layer latency statistics (rolling window of sampled read durations). */
|
|
74
|
+
interface CacheLayerLatency {
|
|
75
|
+
/** Average read latency in milliseconds. */
|
|
76
|
+
avgMs: number;
|
|
77
|
+
/** Maximum observed read latency in milliseconds. */
|
|
78
|
+
maxMs: number;
|
|
79
|
+
/** Number of samples used to compute the statistics. */
|
|
80
|
+
count: number;
|
|
81
|
+
}
|
|
64
82
|
/** Snapshot of cumulative cache counters. */
|
|
65
83
|
interface CacheMetricsSnapshot {
|
|
66
84
|
hits: number;
|
|
@@ -80,6 +98,8 @@ interface CacheMetricsSnapshot {
|
|
|
80
98
|
degradedOperations: number;
|
|
81
99
|
hitsByLayer: Record<string, number>;
|
|
82
100
|
missesByLayer: Record<string, number>;
|
|
101
|
+
/** Per-layer read latency statistics (sampled from successful reads). */
|
|
102
|
+
latencyByLayer: Record<string, CacheLayerLatency>;
|
|
83
103
|
/** Timestamp (ms since epoch) when metrics were last reset. */
|
|
84
104
|
resetAt: number;
|
|
85
105
|
}
|
|
@@ -101,6 +121,8 @@ interface CacheTagIndex {
|
|
|
101
121
|
track(key: string, tags: string[]): Promise<void>;
|
|
102
122
|
remove(key: string): Promise<void>;
|
|
103
123
|
keysForTag(tag: string): Promise<string[]>;
|
|
124
|
+
/** Returns the tags associated with a specific key, or an empty array. */
|
|
125
|
+
tagsForKey?(key: string): Promise<string[]>;
|
|
104
126
|
matchPattern(pattern: string): Promise<string[]>;
|
|
105
127
|
clear(): Promise<void>;
|
|
106
128
|
}
|
|
@@ -203,6 +225,22 @@ interface CacheStatsSnapshot {
|
|
|
203
225
|
}>;
|
|
204
226
|
backgroundRefreshes: number;
|
|
205
227
|
}
|
|
228
|
+
/** Detailed inspection result for a single cache key. */
|
|
229
|
+
interface CacheInspectResult {
|
|
230
|
+
key: string;
|
|
231
|
+
/** Layers in which the key is currently stored (not expired). */
|
|
232
|
+
foundInLayers: string[];
|
|
233
|
+
/** Remaining fresh TTL in seconds, or null if no expiry or not an envelope. */
|
|
234
|
+
freshTtlSeconds: number | null;
|
|
235
|
+
/** Remaining stale-while-revalidate window in seconds, or null. */
|
|
236
|
+
staleTtlSeconds: number | null;
|
|
237
|
+
/** Remaining stale-if-error window in seconds, or null. */
|
|
238
|
+
errorTtlSeconds: number | null;
|
|
239
|
+
/** Whether the key is currently serving stale-while-revalidate. */
|
|
240
|
+
isStale: boolean;
|
|
241
|
+
/** Tags associated with this key (from the TagIndex). */
|
|
242
|
+
tags: string[];
|
|
243
|
+
}
|
|
206
244
|
/** All events emitted by CacheStack and their payload shapes. */
|
|
207
245
|
interface CacheStackEvents {
|
|
208
246
|
/** Fired on every cache hit. */
|
|
@@ -258,6 +296,10 @@ declare class CacheNamespace {
|
|
|
258
296
|
constructor(cache: CacheStack, prefix: string);
|
|
259
297
|
get<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
260
298
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
299
|
+
/**
|
|
300
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
301
|
+
*/
|
|
302
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
261
303
|
has(key: string): Promise<boolean>;
|
|
262
304
|
ttl(key: string): Promise<number | null>;
|
|
263
305
|
set<T>(key: string, value: T, options?: CacheWriteOptions): Promise<void>;
|
|
@@ -268,10 +310,24 @@ declare class CacheNamespace {
|
|
|
268
310
|
mset<T>(entries: CacheMSetEntry<T>[]): Promise<void>;
|
|
269
311
|
invalidateByTag(tag: string): Promise<void>;
|
|
270
312
|
invalidateByPattern(pattern: string): Promise<void>;
|
|
313
|
+
/**
|
|
314
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
315
|
+
*/
|
|
316
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
271
317
|
wrap<TArgs extends unknown[], TResult>(keyPrefix: string, fetcher: (...args: TArgs) => Promise<TResult>, options?: CacheWrapOptions<TArgs>): (...args: TArgs) => Promise<TResult | null>;
|
|
272
318
|
warm(entries: CacheWarmEntry[], options?: CacheWarmOptions): Promise<void>;
|
|
273
319
|
getMetrics(): CacheMetricsSnapshot;
|
|
274
320
|
getHitRate(): CacheHitRateSnapshot;
|
|
321
|
+
/**
|
|
322
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
323
|
+
*
|
|
324
|
+
* ```ts
|
|
325
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
326
|
+
* const posts = tenant.namespace('posts')
|
|
327
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
namespace(childPrefix: string): CacheNamespace;
|
|
275
331
|
qualify(key: string): string;
|
|
276
332
|
}
|
|
277
333
|
|
|
@@ -311,6 +367,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
311
367
|
* Fetches and caches the value if not already present.
|
|
312
368
|
*/
|
|
313
369
|
getOrSet<T>(key: string, fetcher: () => Promise<T>, options?: CacheGetOptions): Promise<T | null>;
|
|
370
|
+
/**
|
|
371
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
372
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
373
|
+
* return non-null.
|
|
374
|
+
*/
|
|
375
|
+
getOrThrow<T>(key: string, fetcher?: () => Promise<T>, options?: CacheGetOptions): Promise<T>;
|
|
314
376
|
/**
|
|
315
377
|
* Returns true if the given key exists and is not expired in any layer.
|
|
316
378
|
*/
|
|
@@ -355,6 +417,12 @@ declare class CacheStack extends EventEmitter {
|
|
|
355
417
|
* Returns computed hit-rate statistics (overall and per-layer).
|
|
356
418
|
*/
|
|
357
419
|
getHitRate(): CacheHitRateSnapshot;
|
|
420
|
+
/**
|
|
421
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
422
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
423
|
+
* Returns `null` if the key does not exist in any layer.
|
|
424
|
+
*/
|
|
425
|
+
inspect(key: string): Promise<CacheInspectResult | null>;
|
|
358
426
|
exportState(): Promise<CacheSnapshotEntry[]>;
|
|
359
427
|
importState(entries: CacheSnapshotEntry[]): Promise<void>;
|
|
360
428
|
persistToFile(filePath: string): Promise<void>;
|
|
@@ -378,6 +446,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
378
446
|
private deleteKeys;
|
|
379
447
|
private publishInvalidation;
|
|
380
448
|
private handleInvalidationMessage;
|
|
449
|
+
private getTagsForKey;
|
|
381
450
|
private formatError;
|
|
382
451
|
private sleep;
|
|
383
452
|
private shouldBroadcastL1Invalidation;
|
|
@@ -413,9 +482,31 @@ interface CacheStackModuleOptions {
|
|
|
413
482
|
layers: CacheLayer[];
|
|
414
483
|
bridgeOptions?: CacheStackOptions;
|
|
415
484
|
}
|
|
485
|
+
interface CacheStackModuleAsyncOptions {
|
|
486
|
+
/**
|
|
487
|
+
* Tokens to inject into the `useFactory` function.
|
|
488
|
+
*/
|
|
489
|
+
inject?: Array<Type | string | symbol>;
|
|
490
|
+
/**
|
|
491
|
+
* Async factory function that returns `CacheStackModuleOptions`.
|
|
492
|
+
* Useful when the Redis client or other dependencies must be resolved
|
|
493
|
+
* from the NestJS DI container first.
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* CacheStackModule.forRootAsync({
|
|
497
|
+
* inject: [ConfigService],
|
|
498
|
+
* useFactory: (config: ConfigService) => ({
|
|
499
|
+
* layers: [new MemoryLayer(), new RedisLayer({ client: createRedis(config) })],
|
|
500
|
+
* })
|
|
501
|
+
* })
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
useFactory: (...args: unknown[]) => CacheStackModuleOptions | Promise<CacheStackModuleOptions>;
|
|
505
|
+
}
|
|
416
506
|
declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
|
|
417
507
|
declare class CacheStackModule {
|
|
418
508
|
static forRoot(options: CacheStackModuleOptions): DynamicModule;
|
|
509
|
+
static forRootAsync(options: CacheStackModuleAsyncOptions): DynamicModule;
|
|
419
510
|
}
|
|
420
511
|
|
|
421
512
|
export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, Cacheable, InjectCacheStack };
|