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
|
@@ -51,7 +51,7 @@ import { EventEmitter } from "events";
|
|
|
51
51
|
import { promises as fs } from "fs";
|
|
52
52
|
|
|
53
53
|
// ../../src/CacheNamespace.ts
|
|
54
|
-
var CacheNamespace = class {
|
|
54
|
+
var CacheNamespace = class _CacheNamespace {
|
|
55
55
|
constructor(cache, prefix) {
|
|
56
56
|
this.cache = cache;
|
|
57
57
|
this.prefix = prefix;
|
|
@@ -64,6 +64,12 @@ var CacheNamespace = class {
|
|
|
64
64
|
async getOrSet(key, fetcher, options) {
|
|
65
65
|
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
69
|
+
*/
|
|
70
|
+
async getOrThrow(key, fetcher, options) {
|
|
71
|
+
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
72
|
+
}
|
|
67
73
|
async has(key) {
|
|
68
74
|
return this.cache.has(this.qualify(key));
|
|
69
75
|
}
|
|
@@ -104,6 +110,12 @@ var CacheNamespace = class {
|
|
|
104
110
|
async invalidateByPattern(pattern) {
|
|
105
111
|
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
106
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
115
|
+
*/
|
|
116
|
+
async inspect(key) {
|
|
117
|
+
return this.cache.inspect(this.qualify(key));
|
|
118
|
+
}
|
|
107
119
|
wrap(keyPrefix, fetcher, options) {
|
|
108
120
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
109
121
|
}
|
|
@@ -122,6 +134,18 @@ var CacheNamespace = class {
|
|
|
122
134
|
getHitRate() {
|
|
123
135
|
return this.cache.getHitRate();
|
|
124
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
142
|
+
* const posts = tenant.namespace('posts')
|
|
143
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
namespace(childPrefix) {
|
|
147
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
148
|
+
}
|
|
125
149
|
qualify(key) {
|
|
126
150
|
return `${this.prefix}:${key}`;
|
|
127
151
|
}
|
|
@@ -223,7 +247,12 @@ var CircuitBreakerManager = class {
|
|
|
223
247
|
var MetricsCollector = class {
|
|
224
248
|
data = this.empty();
|
|
225
249
|
get snapshot() {
|
|
226
|
-
return {
|
|
250
|
+
return {
|
|
251
|
+
...this.data,
|
|
252
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
253
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
254
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
255
|
+
};
|
|
227
256
|
}
|
|
228
257
|
increment(field, amount = 1) {
|
|
229
258
|
;
|
|
@@ -232,6 +261,22 @@ var MetricsCollector = class {
|
|
|
232
261
|
incrementLayer(map, layerName) {
|
|
233
262
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
234
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Records a read latency sample for the given layer.
|
|
266
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
267
|
+
*/
|
|
268
|
+
recordLatency(layerName, durationMs) {
|
|
269
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
270
|
+
if (!existing) {
|
|
271
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
existing.count += 1;
|
|
275
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
276
|
+
if (durationMs > existing.maxMs) {
|
|
277
|
+
existing.maxMs = durationMs;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
235
280
|
reset() {
|
|
236
281
|
this.data = this.empty();
|
|
237
282
|
}
|
|
@@ -266,6 +311,7 @@ var MetricsCollector = class {
|
|
|
266
311
|
degradedOperations: 0,
|
|
267
312
|
hitsByLayer: {},
|
|
268
313
|
missesByLayer: {},
|
|
314
|
+
latencyByLayer: {},
|
|
269
315
|
resetAt: Date.now()
|
|
270
316
|
};
|
|
271
317
|
}
|
|
@@ -503,11 +549,17 @@ var TagIndex = class {
|
|
|
503
549
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
504
550
|
keyToTags = /* @__PURE__ */ new Map();
|
|
505
551
|
knownKeys = /* @__PURE__ */ new Set();
|
|
552
|
+
maxKnownKeys;
|
|
553
|
+
constructor(options = {}) {
|
|
554
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
555
|
+
}
|
|
506
556
|
async touch(key) {
|
|
507
557
|
this.knownKeys.add(key);
|
|
558
|
+
this.pruneKnownKeysIfNeeded();
|
|
508
559
|
}
|
|
509
560
|
async track(key, tags) {
|
|
510
561
|
this.knownKeys.add(key);
|
|
562
|
+
this.pruneKnownKeysIfNeeded();
|
|
511
563
|
if (tags.length === 0) {
|
|
512
564
|
return;
|
|
513
565
|
}
|
|
@@ -546,6 +598,9 @@ var TagIndex = class {
|
|
|
546
598
|
async keysForTag(tag) {
|
|
547
599
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
548
600
|
}
|
|
601
|
+
async tagsForKey(key) {
|
|
602
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
603
|
+
}
|
|
549
604
|
async matchPattern(pattern) {
|
|
550
605
|
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
551
606
|
}
|
|
@@ -554,6 +609,21 @@ var TagIndex = class {
|
|
|
554
609
|
this.keyToTags.clear();
|
|
555
610
|
this.knownKeys.clear();
|
|
556
611
|
}
|
|
612
|
+
pruneKnownKeysIfNeeded() {
|
|
613
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
617
|
+
let removed = 0;
|
|
618
|
+
for (const key of this.knownKeys) {
|
|
619
|
+
if (removed >= toRemove) {
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
this.knownKeys.delete(key);
|
|
623
|
+
this.keyToTags.delete(key);
|
|
624
|
+
removed += 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
557
627
|
};
|
|
558
628
|
|
|
559
629
|
// ../../node_modules/async-mutex/index.mjs
|
|
@@ -756,6 +826,16 @@ var StampedeGuard = class {
|
|
|
756
826
|
}
|
|
757
827
|
};
|
|
758
828
|
|
|
829
|
+
// ../../src/types.ts
|
|
830
|
+
var CacheMissError = class extends Error {
|
|
831
|
+
key;
|
|
832
|
+
constructor(key) {
|
|
833
|
+
super(`Cache miss for key "${key}".`);
|
|
834
|
+
this.name = "CacheMissError";
|
|
835
|
+
this.key = key;
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
759
839
|
// ../../src/CacheStack.ts
|
|
760
840
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
761
841
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
@@ -882,6 +962,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
882
962
|
async getOrSet(key, fetcher, options) {
|
|
883
963
|
return this.get(key, fetcher, options);
|
|
884
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
967
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
968
|
+
* return non-null.
|
|
969
|
+
*/
|
|
970
|
+
async getOrThrow(key, fetcher, options) {
|
|
971
|
+
const value = await this.get(key, fetcher, options);
|
|
972
|
+
if (value === null) {
|
|
973
|
+
throw new CacheMissError(key);
|
|
974
|
+
}
|
|
975
|
+
return value;
|
|
976
|
+
}
|
|
885
977
|
/**
|
|
886
978
|
* Returns true if the given key exists and is not expired in any layer.
|
|
887
979
|
*/
|
|
@@ -1155,6 +1247,46 @@ var CacheStack = class extends EventEmitter {
|
|
|
1155
1247
|
getHitRate() {
|
|
1156
1248
|
return this.metricsCollector.hitRate();
|
|
1157
1249
|
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1252
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1253
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1254
|
+
*/
|
|
1255
|
+
async inspect(key) {
|
|
1256
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
1257
|
+
await this.startup;
|
|
1258
|
+
const foundInLayers = [];
|
|
1259
|
+
let freshTtlSeconds = null;
|
|
1260
|
+
let staleTtlSeconds = null;
|
|
1261
|
+
let errorTtlSeconds = null;
|
|
1262
|
+
let isStale = false;
|
|
1263
|
+
for (const layer of this.layers) {
|
|
1264
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1268
|
+
if (stored === null) {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const resolved = resolveStoredValue(stored);
|
|
1272
|
+
if (resolved.state === "expired") {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
foundInLayers.push(layer.name);
|
|
1276
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1277
|
+
const now = Date.now();
|
|
1278
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1279
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1280
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1281
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (foundInLayers.length === 0) {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1288
|
+
return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1289
|
+
}
|
|
1158
1290
|
async exportState() {
|
|
1159
1291
|
await this.startup;
|
|
1160
1292
|
const exported = /* @__PURE__ */ new Map();
|
|
@@ -1291,6 +1423,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1291
1423
|
await this.storeEntry(key, "empty", null, options);
|
|
1292
1424
|
return null;
|
|
1293
1425
|
}
|
|
1426
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1427
|
+
return fetched;
|
|
1428
|
+
}
|
|
1294
1429
|
await this.storeEntry(key, "value", fetched, options);
|
|
1295
1430
|
return fetched;
|
|
1296
1431
|
}
|
|
@@ -1313,7 +1448,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1313
1448
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
1314
1449
|
const layer = this.layers[index];
|
|
1315
1450
|
if (!layer) continue;
|
|
1451
|
+
const readStart = performance.now();
|
|
1316
1452
|
const stored = await this.readLayerEntry(layer, key);
|
|
1453
|
+
const readDuration = performance.now() - readStart;
|
|
1454
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1317
1455
|
if (stored === null) {
|
|
1318
1456
|
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1319
1457
|
continue;
|
|
@@ -1515,6 +1653,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
1515
1653
|
}
|
|
1516
1654
|
}
|
|
1517
1655
|
}
|
|
1656
|
+
async getTagsForKey(key) {
|
|
1657
|
+
if (this.tagIndex.tagsForKey) {
|
|
1658
|
+
return this.tagIndex.tagsForKey(key);
|
|
1659
|
+
}
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1518
1662
|
formatError(error) {
|
|
1519
1663
|
if (error instanceof Error) {
|
|
1520
1664
|
return error.message;
|
|
@@ -1745,6 +1889,22 @@ var CacheStackModule = class {
|
|
|
1745
1889
|
exports: [provider]
|
|
1746
1890
|
};
|
|
1747
1891
|
}
|
|
1892
|
+
static forRootAsync(options) {
|
|
1893
|
+
const provider = {
|
|
1894
|
+
provide: CACHE_STACK,
|
|
1895
|
+
inject: options.inject ?? [],
|
|
1896
|
+
useFactory: async (...args) => {
|
|
1897
|
+
const resolved = await options.useFactory(...args);
|
|
1898
|
+
return new CacheStack(resolved.layers, resolved.bridgeOptions);
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
return {
|
|
1902
|
+
global: true,
|
|
1903
|
+
module: CacheStackModule,
|
|
1904
|
+
providers: [provider],
|
|
1905
|
+
exports: [provider]
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1748
1908
|
};
|
|
1749
1909
|
CacheStackModule = __decorateClass([
|
|
1750
1910
|
Global(),
|