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.
@@ -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 { ...this.data };
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(),