layercache 1.1.0 → 1.2.1

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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,11 +17,20 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ CacheMissError: () => CacheMissError,
23
34
  CacheNamespace: () => CacheNamespace,
24
35
  CacheStack: () => CacheStack,
25
36
  DiskLayer: () => DiskLayer,
@@ -37,70 +48,96 @@ __export(index_exports, {
37
48
  cacheGraphqlResolver: () => cacheGraphqlResolver,
38
49
  createCacheStatsHandler: () => createCacheStatsHandler,
39
50
  createCachedMethodDecorator: () => createCachedMethodDecorator,
51
+ createExpressCacheMiddleware: () => createExpressCacheMiddleware,
40
52
  createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
53
+ createHonoCacheMiddleware: () => createHonoCacheMiddleware,
54
+ createOpenTelemetryPlugin: () => createOpenTelemetryPlugin,
41
55
  createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
42
56
  createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
43
57
  });
44
58
  module.exports = __toCommonJS(index_exports);
45
59
 
46
60
  // src/CacheStack.ts
47
- var import_node_crypto = require("crypto");
48
61
  var import_node_events = require("events");
49
- var import_node_fs = require("fs");
50
62
 
51
63
  // src/CacheNamespace.ts
52
- var CacheNamespace = class {
64
+ var import_async_mutex = require("async-mutex");
65
+ var CacheNamespace = class _CacheNamespace {
53
66
  constructor(cache, prefix) {
54
67
  this.cache = cache;
55
68
  this.prefix = prefix;
56
69
  }
57
70
  cache;
58
71
  prefix;
72
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
73
+ metrics = emptyMetrics();
59
74
  async get(key, fetcher, options) {
60
- return this.cache.get(this.qualify(key), fetcher, options);
75
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
61
76
  }
62
77
  async getOrSet(key, fetcher, options) {
63
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
78
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
79
+ }
80
+ /**
81
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
82
+ */
83
+ async getOrThrow(key, fetcher, options) {
84
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
64
85
  }
65
86
  async has(key) {
66
- return this.cache.has(this.qualify(key));
87
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
67
88
  }
68
89
  async ttl(key) {
69
- return this.cache.ttl(this.qualify(key));
90
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
70
91
  }
71
92
  async set(key, value, options) {
72
- await this.cache.set(this.qualify(key), value, options);
93
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
73
94
  }
74
95
  async delete(key) {
75
- await this.cache.delete(this.qualify(key));
96
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
76
97
  }
77
98
  async mdelete(keys) {
78
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
99
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
79
100
  }
80
101
  async clear() {
81
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
102
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
82
103
  }
83
104
  async mget(entries) {
84
- return this.cache.mget(
85
- entries.map((entry) => ({
86
- ...entry,
87
- key: this.qualify(entry.key)
88
- }))
105
+ return this.trackMetrics(
106
+ () => this.cache.mget(
107
+ entries.map((entry) => ({
108
+ ...entry,
109
+ key: this.qualify(entry.key)
110
+ }))
111
+ )
89
112
  );
90
113
  }
91
114
  async mset(entries) {
92
- await this.cache.mset(
93
- entries.map((entry) => ({
94
- ...entry,
95
- key: this.qualify(entry.key)
96
- }))
115
+ await this.trackMetrics(
116
+ () => this.cache.mset(
117
+ entries.map((entry) => ({
118
+ ...entry,
119
+ key: this.qualify(entry.key)
120
+ }))
121
+ )
97
122
  );
98
123
  }
99
124
  async invalidateByTag(tag) {
100
- await this.cache.invalidateByTag(tag);
125
+ await this.trackMetrics(() => this.cache.invalidateByTag(tag));
126
+ }
127
+ async invalidateByTags(tags, mode = "any") {
128
+ await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
101
129
  }
102
130
  async invalidateByPattern(pattern) {
103
- await this.cache.invalidateByPattern(this.qualify(pattern));
131
+ await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
132
+ }
133
+ async invalidateByPrefix(prefix) {
134
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
135
+ }
136
+ /**
137
+ * Returns detailed metadata about a single cache key within this namespace.
138
+ */
139
+ async inspect(key) {
140
+ return this.cache.inspect(this.qualify(key));
104
141
  }
105
142
  wrap(keyPrefix, fetcher, options) {
106
143
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
@@ -115,15 +152,159 @@ var CacheNamespace = class {
115
152
  );
116
153
  }
117
154
  getMetrics() {
118
- return this.cache.getMetrics();
155
+ return cloneMetrics(this.metrics);
119
156
  }
120
157
  getHitRate() {
121
- return this.cache.getHitRate();
158
+ const total = this.metrics.hits + this.metrics.misses;
159
+ const overall = total === 0 ? 0 : this.metrics.hits / total;
160
+ const byLayer = {};
161
+ const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
162
+ for (const layer of layers) {
163
+ const hits = this.metrics.hitsByLayer[layer] ?? 0;
164
+ const misses = this.metrics.missesByLayer[layer] ?? 0;
165
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
166
+ }
167
+ return { overall, byLayer };
168
+ }
169
+ /**
170
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
171
+ *
172
+ * ```ts
173
+ * const tenant = cache.namespace('tenant:abc')
174
+ * const posts = tenant.namespace('posts')
175
+ * // keys become: "tenant:abc:posts:mykey"
176
+ * ```
177
+ */
178
+ namespace(childPrefix) {
179
+ return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
122
180
  }
123
181
  qualify(key) {
124
182
  return `${this.prefix}:${key}`;
125
183
  }
184
+ async trackMetrics(operation) {
185
+ return this.getMetricsMutex().runExclusive(async () => {
186
+ const before = this.cache.getMetrics();
187
+ const result = await operation();
188
+ const after = this.cache.getMetrics();
189
+ this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
190
+ return result;
191
+ });
192
+ }
193
+ getMetricsMutex() {
194
+ const existing = _CacheNamespace.metricsMutexes.get(this.cache);
195
+ if (existing) {
196
+ return existing;
197
+ }
198
+ const mutex = new import_async_mutex.Mutex();
199
+ _CacheNamespace.metricsMutexes.set(this.cache, mutex);
200
+ return mutex;
201
+ }
126
202
  };
203
+ function emptyMetrics() {
204
+ return {
205
+ hits: 0,
206
+ misses: 0,
207
+ fetches: 0,
208
+ sets: 0,
209
+ deletes: 0,
210
+ backfills: 0,
211
+ invalidations: 0,
212
+ staleHits: 0,
213
+ refreshes: 0,
214
+ refreshErrors: 0,
215
+ writeFailures: 0,
216
+ singleFlightWaits: 0,
217
+ negativeCacheHits: 0,
218
+ circuitBreakerTrips: 0,
219
+ degradedOperations: 0,
220
+ hitsByLayer: {},
221
+ missesByLayer: {},
222
+ latencyByLayer: {},
223
+ resetAt: Date.now()
224
+ };
225
+ }
226
+ function cloneMetrics(metrics) {
227
+ return {
228
+ ...metrics,
229
+ hitsByLayer: { ...metrics.hitsByLayer },
230
+ missesByLayer: { ...metrics.missesByLayer },
231
+ latencyByLayer: Object.fromEntries(
232
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
233
+ )
234
+ };
235
+ }
236
+ function diffMetrics(before, after) {
237
+ const latencyByLayer = Object.fromEntries(
238
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
239
+ layer,
240
+ {
241
+ avgMs: value.avgMs,
242
+ maxMs: value.maxMs,
243
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
244
+ }
245
+ ])
246
+ );
247
+ return {
248
+ hits: after.hits - before.hits,
249
+ misses: after.misses - before.misses,
250
+ fetches: after.fetches - before.fetches,
251
+ sets: after.sets - before.sets,
252
+ deletes: after.deletes - before.deletes,
253
+ backfills: after.backfills - before.backfills,
254
+ invalidations: after.invalidations - before.invalidations,
255
+ staleHits: after.staleHits - before.staleHits,
256
+ refreshes: after.refreshes - before.refreshes,
257
+ refreshErrors: after.refreshErrors - before.refreshErrors,
258
+ writeFailures: after.writeFailures - before.writeFailures,
259
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
260
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
261
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
262
+ degradedOperations: after.degradedOperations - before.degradedOperations,
263
+ hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
264
+ missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
265
+ latencyByLayer,
266
+ resetAt: after.resetAt
267
+ };
268
+ }
269
+ function addMetrics(base, delta) {
270
+ return {
271
+ hits: base.hits + delta.hits,
272
+ misses: base.misses + delta.misses,
273
+ fetches: base.fetches + delta.fetches,
274
+ sets: base.sets + delta.sets,
275
+ deletes: base.deletes + delta.deletes,
276
+ backfills: base.backfills + delta.backfills,
277
+ invalidations: base.invalidations + delta.invalidations,
278
+ staleHits: base.staleHits + delta.staleHits,
279
+ refreshes: base.refreshes + delta.refreshes,
280
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
281
+ writeFailures: base.writeFailures + delta.writeFailures,
282
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
283
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
284
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
285
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
286
+ hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
287
+ missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
288
+ latencyByLayer: cloneMetrics(delta).latencyByLayer,
289
+ resetAt: base.resetAt
290
+ };
291
+ }
292
+ function diffMap(before, after) {
293
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
294
+ const result = {};
295
+ for (const key of keys) {
296
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
297
+ }
298
+ return result;
299
+ }
300
+ function addMap(base, delta) {
301
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
302
+ const result = {};
303
+ for (const key of keys) {
304
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
305
+ }
306
+ return result;
307
+ }
127
308
 
128
309
  // src/internal/CircuitBreakerManager.ts
129
310
  var CircuitBreakerManager = class {
@@ -217,11 +398,105 @@ var CircuitBreakerManager = class {
217
398
  }
218
399
  };
219
400
 
401
+ // src/internal/FetchRateLimiter.ts
402
+ var FetchRateLimiter = class {
403
+ active = 0;
404
+ queue = [];
405
+ startedAt = [];
406
+ drainTimer;
407
+ async schedule(options, task) {
408
+ if (!options) {
409
+ return task();
410
+ }
411
+ const normalized = this.normalize(options);
412
+ if (!normalized) {
413
+ return task();
414
+ }
415
+ return new Promise((resolve, reject) => {
416
+ this.queue.push({ options: normalized, task, resolve, reject });
417
+ this.drain();
418
+ });
419
+ }
420
+ normalize(options) {
421
+ const maxConcurrent = options.maxConcurrent;
422
+ const intervalMs = options.intervalMs;
423
+ const maxPerInterval = options.maxPerInterval;
424
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
425
+ return void 0;
426
+ }
427
+ return {
428
+ maxConcurrent,
429
+ intervalMs,
430
+ maxPerInterval
431
+ };
432
+ }
433
+ drain() {
434
+ if (this.drainTimer) {
435
+ clearTimeout(this.drainTimer);
436
+ this.drainTimer = void 0;
437
+ }
438
+ while (this.queue.length > 0) {
439
+ const next = this.queue[0];
440
+ if (!next) {
441
+ return;
442
+ }
443
+ const waitMs = this.waitTime(next.options);
444
+ if (waitMs > 0) {
445
+ this.drainTimer = setTimeout(() => {
446
+ this.drainTimer = void 0;
447
+ this.drain();
448
+ }, waitMs);
449
+ this.drainTimer.unref?.();
450
+ return;
451
+ }
452
+ this.queue.shift();
453
+ this.active += 1;
454
+ this.startedAt.push(Date.now());
455
+ void next.task().then(next.resolve, next.reject).finally(() => {
456
+ this.active -= 1;
457
+ this.drain();
458
+ });
459
+ }
460
+ }
461
+ waitTime(options) {
462
+ const now = Date.now();
463
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
464
+ return 1;
465
+ }
466
+ if (!options.intervalMs || !options.maxPerInterval) {
467
+ return 0;
468
+ }
469
+ this.prune(now, options.intervalMs);
470
+ if (this.startedAt.length < options.maxPerInterval) {
471
+ return 0;
472
+ }
473
+ const oldest = this.startedAt[0];
474
+ if (!oldest) {
475
+ return 0;
476
+ }
477
+ return Math.max(1, options.intervalMs - (now - oldest));
478
+ }
479
+ prune(now, intervalMs) {
480
+ while (this.startedAt.length > 0) {
481
+ const startedAt = this.startedAt[0];
482
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
483
+ break;
484
+ }
485
+ this.startedAt.shift();
486
+ }
487
+ }
488
+ };
489
+
220
490
  // src/internal/MetricsCollector.ts
221
491
  var MetricsCollector = class {
222
492
  data = this.empty();
223
493
  get snapshot() {
224
- return { ...this.data };
494
+ return {
495
+ ...this.data,
496
+ hitsByLayer: { ...this.data.hitsByLayer },
497
+ missesByLayer: { ...this.data.missesByLayer },
498
+ latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
499
+ };
225
500
  }
226
501
  increment(field, amount = 1) {
227
502
  ;
@@ -230,6 +505,22 @@ var MetricsCollector = class {
230
505
  incrementLayer(map, layerName) {
231
506
  this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
232
507
  }
508
+ /**
509
+ * Records a read latency sample for the given layer.
510
+ * Maintains a rolling average and max using Welford's online algorithm.
511
+ */
512
+ recordLatency(layerName, durationMs) {
513
+ const existing = this.data.latencyByLayer[layerName];
514
+ if (!existing) {
515
+ this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
516
+ return;
517
+ }
518
+ existing.count += 1;
519
+ existing.avgMs += (durationMs - existing.avgMs) / existing.count;
520
+ if (durationMs > existing.maxMs) {
521
+ existing.maxMs = durationMs;
522
+ }
523
+ }
233
524
  reset() {
234
525
  this.data = this.empty();
235
526
  }
@@ -264,6 +555,7 @@ var MetricsCollector = class {
264
555
  degradedOperations: 0,
265
556
  hitsByLayer: {},
266
557
  missesByLayer: {},
558
+ latencyByLayer: {},
267
559
  resetAt: Date.now()
268
560
  };
269
561
  }
@@ -391,13 +683,14 @@ var TtlResolver = class {
391
683
  clearProfiles() {
392
684
  this.accessProfiles.clear();
393
685
  }
394
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
686
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
687
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
395
688
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
396
689
  layerName,
397
690
  options?.negativeTtl,
398
691
  globalNegativeTtl,
399
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
400
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
692
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
693
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
401
694
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
402
695
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
403
696
  return this.applyJitter(adaptiveTtl, jitter);
@@ -436,6 +729,29 @@ var TtlResolver = class {
436
729
  const delta = (Math.random() * 2 - 1) * jitter;
437
730
  return Math.max(1, Math.round(ttl + delta));
438
731
  }
732
+ resolvePolicyTtl(key, value, policy) {
733
+ if (!policy) {
734
+ return void 0;
735
+ }
736
+ if (typeof policy === "function") {
737
+ return policy({ key, value });
738
+ }
739
+ const now = /* @__PURE__ */ new Date();
740
+ if (policy === "until-midnight") {
741
+ const nextMidnight = new Date(now);
742
+ nextMidnight.setHours(24, 0, 0, 0);
743
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
744
+ }
745
+ if (policy === "next-hour") {
746
+ const nextHour = new Date(now);
747
+ nextHour.setMinutes(60, 0, 0);
748
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
749
+ }
750
+ const alignToSeconds = policy.alignTo;
751
+ const currentSeconds = Math.floor(Date.now() / 1e3);
752
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
753
+ return Math.max(1, nextBoundary - currentSeconds);
754
+ }
439
755
  readLayerNumber(layerName, value) {
440
756
  if (typeof value === "number") {
441
757
  return value;
@@ -463,36 +779,46 @@ var PatternMatcher = class _PatternMatcher {
463
779
  /**
464
780
  * Tests whether a glob-style pattern matches a value.
465
781
  * Supports `*` (any sequence of characters) and `?` (any single character).
466
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
782
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
783
+ * quadratic memory usage on long patterns/keys.
467
784
  */
468
785
  static matches(pattern, value) {
469
786
  return _PatternMatcher.matchLinear(pattern, value);
470
787
  }
471
788
  /**
472
- * Linear-time glob matching using dynamic programming.
473
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
789
+ * Linear-time glob matching with O(1) extra memory.
474
790
  */
475
791
  static matchLinear(pattern, value) {
476
- const m = pattern.length;
477
- const n = value.length;
478
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
479
- dp[0][0] = true;
480
- for (let i = 1; i <= m; i++) {
481
- if (pattern[i - 1] === "*") {
482
- dp[i][0] = dp[i - 1]?.[0];
483
- }
484
- }
485
- for (let i = 1; i <= m; i++) {
486
- for (let j = 1; j <= n; j++) {
487
- const pc = pattern[i - 1];
488
- if (pc === "*") {
489
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
490
- } else if (pc === "?" || pc === value[j - 1]) {
491
- dp[i][j] = dp[i - 1]?.[j - 1];
492
- }
792
+ let patternIndex = 0;
793
+ let valueIndex = 0;
794
+ let starIndex = -1;
795
+ let backtrackValueIndex = 0;
796
+ while (valueIndex < value.length) {
797
+ const patternChar = pattern[patternIndex];
798
+ const valueChar = value[valueIndex];
799
+ if (patternChar === "*" && patternIndex < pattern.length) {
800
+ starIndex = patternIndex;
801
+ patternIndex += 1;
802
+ backtrackValueIndex = valueIndex;
803
+ continue;
804
+ }
805
+ if (patternChar === "?" || patternChar === valueChar) {
806
+ patternIndex += 1;
807
+ valueIndex += 1;
808
+ continue;
809
+ }
810
+ if (starIndex !== -1) {
811
+ patternIndex = starIndex + 1;
812
+ backtrackValueIndex += 1;
813
+ valueIndex = backtrackValueIndex;
814
+ continue;
493
815
  }
816
+ return false;
494
817
  }
495
- return dp[m]?.[n];
818
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
819
+ patternIndex += 1;
820
+ }
821
+ return patternIndex === pattern.length;
496
822
  }
497
823
  };
498
824
 
@@ -501,11 +827,17 @@ var TagIndex = class {
501
827
  tagToKeys = /* @__PURE__ */ new Map();
502
828
  keyToTags = /* @__PURE__ */ new Map();
503
829
  knownKeys = /* @__PURE__ */ new Set();
830
+ maxKnownKeys;
831
+ constructor(options = {}) {
832
+ this.maxKnownKeys = options.maxKnownKeys;
833
+ }
504
834
  async touch(key) {
505
835
  this.knownKeys.add(key);
836
+ this.pruneKnownKeysIfNeeded();
506
837
  }
507
838
  async track(key, tags) {
508
839
  this.knownKeys.add(key);
840
+ this.pruneKnownKeysIfNeeded();
509
841
  if (tags.length === 0) {
510
842
  return;
511
843
  }
@@ -524,6 +856,40 @@ var TagIndex = class {
524
856
  }
525
857
  }
526
858
  async remove(key) {
859
+ this.removeKey(key);
860
+ }
861
+ async keysForTag(tag) {
862
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
863
+ }
864
+ async keysForPrefix(prefix) {
865
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
866
+ }
867
+ async tagsForKey(key) {
868
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
869
+ }
870
+ async matchPattern(pattern) {
871
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
872
+ }
873
+ async clear() {
874
+ this.tagToKeys.clear();
875
+ this.keyToTags.clear();
876
+ this.knownKeys.clear();
877
+ }
878
+ pruneKnownKeysIfNeeded() {
879
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
880
+ return;
881
+ }
882
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
883
+ let removed = 0;
884
+ for (const key of this.knownKeys) {
885
+ if (removed >= toRemove) {
886
+ break;
887
+ }
888
+ this.removeKey(key);
889
+ removed += 1;
890
+ }
891
+ }
892
+ removeKey(key) {
527
893
  this.knownKeys.delete(key);
528
894
  const tags = this.keyToTags.get(key);
529
895
  if (!tags) {
@@ -541,21 +907,10 @@ var TagIndex = class {
541
907
  }
542
908
  this.keyToTags.delete(key);
543
909
  }
544
- async keysForTag(tag) {
545
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
546
- }
547
- async matchPattern(pattern) {
548
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
549
- }
550
- async clear() {
551
- this.tagToKeys.clear();
552
- this.keyToTags.clear();
553
- this.knownKeys.clear();
554
- }
555
910
  };
556
911
 
557
912
  // src/stampede/StampedeGuard.ts
558
- var import_async_mutex = require("async-mutex");
913
+ var import_async_mutex2 = require("async-mutex");
559
914
  var StampedeGuard = class {
560
915
  mutexes = /* @__PURE__ */ new Map();
561
916
  async execute(key, task) {
@@ -572,7 +927,7 @@ var StampedeGuard = class {
572
927
  getMutexEntry(key) {
573
928
  let entry = this.mutexes.get(key);
574
929
  if (!entry) {
575
- entry = { mutex: new import_async_mutex.Mutex(), references: 0 };
930
+ entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
576
931
  this.mutexes.set(key, entry);
577
932
  }
578
933
  entry.references += 1;
@@ -580,6 +935,16 @@ var StampedeGuard = class {
580
935
  }
581
936
  };
582
937
 
938
+ // src/types.ts
939
+ var CacheMissError = class extends Error {
940
+ key;
941
+ constructor(key) {
942
+ super(`Cache miss for key "${key}".`);
943
+ this.name = "CacheMissError";
944
+ this.key = key;
945
+ }
946
+ };
947
+
583
948
  // src/CacheStack.ts
584
949
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
585
950
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
@@ -623,6 +988,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
623
988
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
624
989
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
625
990
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
991
+ this.currentGeneration = options.generation;
626
992
  if (options.publishSetInvalidation !== void 0) {
627
993
  console.warn(
628
994
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -631,21 +997,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
631
997
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
632
998
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
633
999
  this.tagIndex = options.tagIndex ?? new TagIndex();
1000
+ this.initializeWriteBehind(options.writeBehind);
634
1001
  this.startup = this.initialize();
635
1002
  }
636
1003
  layers;
637
1004
  options;
638
1005
  stampedeGuard = new StampedeGuard();
639
1006
  metricsCollector = new MetricsCollector();
640
- instanceId = (0, import_node_crypto.randomUUID)();
1007
+ instanceId = createInstanceId();
641
1008
  startup;
642
1009
  unsubscribeInvalidation;
643
1010
  logger;
644
1011
  tagIndex;
1012
+ fetchRateLimiter = new FetchRateLimiter();
645
1013
  backgroundRefreshes = /* @__PURE__ */ new Map();
646
1014
  layerDegradedUntil = /* @__PURE__ */ new Map();
647
1015
  ttlResolver;
648
1016
  circuitBreakerManager;
1017
+ currentGeneration;
1018
+ writeBehindQueue = [];
1019
+ writeBehindTimer;
1020
+ writeBehindFlushPromise;
649
1021
  isDisconnecting = false;
650
1022
  disconnectPromise;
651
1023
  /**
@@ -655,9 +1027,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
655
1027
  * and no `fetcher` is provided.
656
1028
  */
657
1029
  async get(key, fetcher, options) {
658
- const normalizedKey = this.validateCacheKey(key);
1030
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
659
1031
  this.validateWriteOptions(options);
660
- await this.startup;
1032
+ await this.awaitStartup("get");
661
1033
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
662
1034
  if (hit.found) {
663
1035
  this.ttlResolver.recordAccess(normalizedKey);
@@ -706,12 +1078,24 @@ var CacheStack = class extends import_node_events.EventEmitter {
706
1078
  async getOrSet(key, fetcher, options) {
707
1079
  return this.get(key, fetcher, options);
708
1080
  }
1081
+ /**
1082
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
1083
+ * Useful when the value is expected to exist or the fetcher is expected to
1084
+ * return non-null.
1085
+ */
1086
+ async getOrThrow(key, fetcher, options) {
1087
+ const value = await this.get(key, fetcher, options);
1088
+ if (value === null) {
1089
+ throw new CacheMissError(key);
1090
+ }
1091
+ return value;
1092
+ }
709
1093
  /**
710
1094
  * Returns true if the given key exists and is not expired in any layer.
711
1095
  */
712
1096
  async has(key) {
713
- const normalizedKey = this.validateCacheKey(key);
714
- await this.startup;
1097
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1098
+ await this.awaitStartup("has");
715
1099
  for (const layer of this.layers) {
716
1100
  if (this.shouldSkipLayer(layer)) {
717
1101
  continue;
@@ -741,8 +1125,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
741
1125
  * that has it, or null if the key is not found / has no TTL.
742
1126
  */
743
1127
  async ttl(key) {
744
- const normalizedKey = this.validateCacheKey(key);
745
- await this.startup;
1128
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1129
+ await this.awaitStartup("ttl");
746
1130
  for (const layer of this.layers) {
747
1131
  if (this.shouldSkipLayer(layer)) {
748
1132
  continue;
@@ -763,17 +1147,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
763
1147
  * Stores a value in all cache layers. Overwrites any existing value.
764
1148
  */
765
1149
  async set(key, value, options) {
766
- const normalizedKey = this.validateCacheKey(key);
1150
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
767
1151
  this.validateWriteOptions(options);
768
- await this.startup;
1152
+ await this.awaitStartup("set");
769
1153
  await this.storeEntry(normalizedKey, "value", value, options);
770
1154
  }
771
1155
  /**
772
1156
  * Deletes the key from all layers and publishes an invalidation message.
773
1157
  */
774
1158
  async delete(key) {
775
- const normalizedKey = this.validateCacheKey(key);
776
- await this.startup;
1159
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1160
+ await this.awaitStartup("delete");
777
1161
  await this.deleteKeys([normalizedKey]);
778
1162
  await this.publishInvalidation({
779
1163
  scope: "key",
@@ -783,7 +1167,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
783
1167
  });
784
1168
  }
785
1169
  async clear() {
786
- await this.startup;
1170
+ await this.awaitStartup("clear");
787
1171
  await Promise.all(this.layers.map((layer) => layer.clear()));
788
1172
  await this.tagIndex.clear();
789
1173
  this.ttlResolver.clearProfiles();
@@ -799,23 +1183,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
799
1183
  if (keys.length === 0) {
800
1184
  return;
801
1185
  }
802
- await this.startup;
1186
+ await this.awaitStartup("mdelete");
803
1187
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
804
- await this.deleteKeys(normalizedKeys);
1188
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1189
+ await this.deleteKeys(cacheKeys);
805
1190
  await this.publishInvalidation({
806
1191
  scope: "keys",
807
- keys: normalizedKeys,
1192
+ keys: cacheKeys,
808
1193
  sourceId: this.instanceId,
809
1194
  operation: "delete"
810
1195
  });
811
1196
  }
812
1197
  async mget(entries) {
1198
+ this.assertActive("mget");
813
1199
  if (entries.length === 0) {
814
1200
  return [];
815
1201
  }
816
1202
  const normalizedEntries = entries.map((entry) => ({
817
1203
  ...entry,
818
- key: this.validateCacheKey(entry.key)
1204
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
819
1205
  }));
820
1206
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
821
1207
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -841,7 +1227,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
841
1227
  })
842
1228
  );
843
1229
  }
844
- await this.startup;
1230
+ await this.awaitStartup("mget");
845
1231
  const pending = /* @__PURE__ */ new Set();
846
1232
  const indexesByKey = /* @__PURE__ */ new Map();
847
1233
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -889,14 +1275,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
889
1275
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
890
1276
  }
891
1277
  async mset(entries) {
1278
+ this.assertActive("mset");
892
1279
  const normalizedEntries = entries.map((entry) => ({
893
1280
  ...entry,
894
- key: this.validateCacheKey(entry.key)
1281
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
895
1282
  }));
896
1283
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
897
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1284
+ await this.awaitStartup("mset");
1285
+ await this.writeBatch(normalizedEntries);
898
1286
  }
899
1287
  async warm(entries, options = {}) {
1288
+ this.assertActive("warm");
900
1289
  const concurrency = Math.max(1, options.concurrency ?? 4);
901
1290
  const total = entries.length;
902
1291
  let completed = 0;
@@ -945,14 +1334,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
945
1334
  return new CacheNamespace(this, prefix);
946
1335
  }
947
1336
  async invalidateByTag(tag) {
948
- await this.startup;
1337
+ await this.awaitStartup("invalidateByTag");
949
1338
  const keys = await this.tagIndex.keysForTag(tag);
950
1339
  await this.deleteKeys(keys);
951
1340
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
952
1341
  }
1342
+ async invalidateByTags(tags, mode = "any") {
1343
+ if (tags.length === 0) {
1344
+ return;
1345
+ }
1346
+ await this.awaitStartup("invalidateByTags");
1347
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1348
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1349
+ await this.deleteKeys(keys);
1350
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1351
+ }
953
1352
  async invalidateByPattern(pattern) {
954
- await this.startup;
955
- const keys = await this.tagIndex.matchPattern(pattern);
1353
+ await this.awaitStartup("invalidateByPattern");
1354
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1355
+ await this.deleteKeys(keys);
1356
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1357
+ }
1358
+ async invalidateByPrefix(prefix) {
1359
+ await this.awaitStartup("invalidateByPrefix");
1360
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1361
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
956
1362
  await this.deleteKeys(keys);
957
1363
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
958
1364
  }
@@ -979,8 +1385,77 @@ var CacheStack = class extends import_node_events.EventEmitter {
979
1385
  getHitRate() {
980
1386
  return this.metricsCollector.hitRate();
981
1387
  }
982
- async exportState() {
1388
+ async healthCheck() {
983
1389
  await this.startup;
1390
+ return Promise.all(
1391
+ this.layers.map(async (layer) => {
1392
+ const startedAt = performance.now();
1393
+ try {
1394
+ const healthy = layer.ping ? await layer.ping() : true;
1395
+ return {
1396
+ layer: layer.name,
1397
+ healthy,
1398
+ latencyMs: performance.now() - startedAt
1399
+ };
1400
+ } catch (error) {
1401
+ return {
1402
+ layer: layer.name,
1403
+ healthy: false,
1404
+ latencyMs: performance.now() - startedAt,
1405
+ error: this.formatError(error)
1406
+ };
1407
+ }
1408
+ })
1409
+ );
1410
+ }
1411
+ bumpGeneration(nextGeneration) {
1412
+ const current = this.currentGeneration ?? 0;
1413
+ this.currentGeneration = nextGeneration ?? current + 1;
1414
+ return this.currentGeneration;
1415
+ }
1416
+ /**
1417
+ * Returns detailed metadata about a single cache key: which layers contain it,
1418
+ * remaining fresh/stale/error TTLs, and associated tags.
1419
+ * Returns `null` if the key does not exist in any layer.
1420
+ */
1421
+ async inspect(key) {
1422
+ const userKey = this.validateCacheKey(key);
1423
+ const normalizedKey = this.qualifyKey(userKey);
1424
+ await this.awaitStartup("inspect");
1425
+ const foundInLayers = [];
1426
+ let freshTtlSeconds = null;
1427
+ let staleTtlSeconds = null;
1428
+ let errorTtlSeconds = null;
1429
+ let isStale = false;
1430
+ for (const layer of this.layers) {
1431
+ if (this.shouldSkipLayer(layer)) {
1432
+ continue;
1433
+ }
1434
+ const stored = await this.readLayerEntry(layer, normalizedKey);
1435
+ if (stored === null) {
1436
+ continue;
1437
+ }
1438
+ const resolved = resolveStoredValue(stored);
1439
+ if (resolved.state === "expired") {
1440
+ continue;
1441
+ }
1442
+ foundInLayers.push(layer.name);
1443
+ if (foundInLayers.length === 1 && resolved.envelope) {
1444
+ const now = Date.now();
1445
+ freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
1446
+ staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
1447
+ errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
1448
+ isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
1449
+ }
1450
+ }
1451
+ if (foundInLayers.length === 0) {
1452
+ return null;
1453
+ }
1454
+ const tags = await this.getTagsForKey(normalizedKey);
1455
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1456
+ }
1457
+ async exportState() {
1458
+ await this.awaitStartup("exportState");
984
1459
  const exported = /* @__PURE__ */ new Map();
985
1460
  for (const layer of this.layers) {
986
1461
  if (!layer.keys) {
@@ -988,15 +1463,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
988
1463
  }
989
1464
  const keys = await layer.keys();
990
1465
  for (const key of keys) {
991
- if (exported.has(key)) {
1466
+ const exportedKey = this.stripQualifiedKey(key);
1467
+ if (exported.has(exportedKey)) {
992
1468
  continue;
993
1469
  }
994
1470
  const stored = await this.readLayerEntry(layer, key);
995
1471
  if (stored === null) {
996
1472
  continue;
997
1473
  }
998
- exported.set(key, {
999
- key,
1474
+ exported.set(exportedKey, {
1475
+ key: exportedKey,
1000
1476
  value: stored,
1001
1477
  ttl: remainingStoredTtlSeconds(stored)
1002
1478
  });
@@ -1005,20 +1481,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1005
1481
  return [...exported.values()];
1006
1482
  }
1007
1483
  async importState(entries) {
1008
- await this.startup;
1484
+ await this.awaitStartup("importState");
1009
1485
  await Promise.all(
1010
1486
  entries.map(async (entry) => {
1011
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1012
- await this.tagIndex.touch(entry.key);
1487
+ const qualifiedKey = this.qualifyKey(entry.key);
1488
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1489
+ await this.tagIndex.touch(qualifiedKey);
1013
1490
  })
1014
1491
  );
1015
1492
  }
1016
1493
  async persistToFile(filePath) {
1494
+ this.assertActive("persistToFile");
1017
1495
  const snapshot = await this.exportState();
1018
- await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1496
+ const { promises: fs2 } = await import("fs");
1497
+ await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1019
1498
  }
1020
1499
  async restoreFromFile(filePath) {
1021
- const raw = await import_node_fs.promises.readFile(filePath, "utf8");
1500
+ this.assertActive("restoreFromFile");
1501
+ const { promises: fs2 } = await import("fs");
1502
+ const raw = await fs2.readFile(filePath, "utf8");
1022
1503
  let parsed;
1023
1504
  try {
1024
1505
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1041,7 +1522,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1041
1522
  this.disconnectPromise = (async () => {
1042
1523
  await this.startup;
1043
1524
  await this.unsubscribeInvalidation?.();
1525
+ await this.flushWriteBehindQueue();
1044
1526
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1527
+ if (this.writeBehindTimer) {
1528
+ clearInterval(this.writeBehindTimer);
1529
+ this.writeBehindTimer = void 0;
1530
+ }
1531
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1045
1532
  })();
1046
1533
  }
1047
1534
  await this.disconnectPromise;
@@ -1101,7 +1588,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1101
1588
  const fetchStart = Date.now();
1102
1589
  let fetched;
1103
1590
  try {
1104
- fetched = await fetcher();
1591
+ fetched = await this.fetchRateLimiter.schedule(
1592
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1593
+ fetcher
1594
+ );
1105
1595
  this.circuitBreakerManager.recordSuccess(key);
1106
1596
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1107
1597
  } catch (error) {
@@ -1115,6 +1605,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1115
1605
  await this.storeEntry(key, "empty", null, options);
1116
1606
  return null;
1117
1607
  }
1608
+ if (options?.shouldCache && !options.shouldCache(fetched)) {
1609
+ return fetched;
1610
+ }
1118
1611
  await this.storeEntry(key, "value", fetched, options);
1119
1612
  return fetched;
1120
1613
  }
@@ -1132,12 +1625,70 @@ var CacheStack = class extends import_node_events.EventEmitter {
1132
1625
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1133
1626
  }
1134
1627
  }
1628
+ async writeBatch(entries) {
1629
+ const now = Date.now();
1630
+ const entriesByLayer = /* @__PURE__ */ new Map();
1631
+ const immediateOperations = [];
1632
+ const deferredOperations = [];
1633
+ for (const entry of entries) {
1634
+ for (const layer of this.layers) {
1635
+ if (this.shouldSkipLayer(layer)) {
1636
+ continue;
1637
+ }
1638
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1639
+ const bucket = entriesByLayer.get(layer) ?? [];
1640
+ bucket.push(layerEntry);
1641
+ entriesByLayer.set(layer, bucket);
1642
+ }
1643
+ }
1644
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1645
+ const operation = async () => {
1646
+ try {
1647
+ if (layer.setMany) {
1648
+ await layer.setMany(layerEntries);
1649
+ return;
1650
+ }
1651
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1652
+ } catch (error) {
1653
+ await this.handleLayerFailure(layer, "write", error);
1654
+ }
1655
+ };
1656
+ if (this.shouldWriteBehind(layer)) {
1657
+ deferredOperations.push(operation);
1658
+ } else {
1659
+ immediateOperations.push(operation);
1660
+ }
1661
+ }
1662
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1663
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1664
+ for (const entry of entries) {
1665
+ if (entry.options?.tags) {
1666
+ await this.tagIndex.track(entry.key, entry.options.tags);
1667
+ } else {
1668
+ await this.tagIndex.touch(entry.key);
1669
+ }
1670
+ this.metricsCollector.increment("sets");
1671
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1672
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1673
+ }
1674
+ if (this.shouldBroadcastL1Invalidation()) {
1675
+ await this.publishInvalidation({
1676
+ scope: "keys",
1677
+ keys: entries.map((entry) => entry.key),
1678
+ sourceId: this.instanceId,
1679
+ operation: "write"
1680
+ });
1681
+ }
1682
+ }
1135
1683
  async readFromLayers(key, options, mode) {
1136
1684
  let sawRetainableValue = false;
1137
1685
  for (let index = 0; index < this.layers.length; index += 1) {
1138
1686
  const layer = this.layers[index];
1139
1687
  if (!layer) continue;
1688
+ const readStart = performance.now();
1140
1689
  const stored = await this.readLayerEntry(layer, key);
1690
+ const readDuration = performance.now() - readStart;
1691
+ this.metricsCollector.recordLatency(layer.name, readDuration);
1141
1692
  if (stored === null) {
1142
1693
  this.metricsCollector.incrementLayer("missesByLayer", layer.name);
1143
1694
  continue;
@@ -1212,33 +1763,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1212
1763
  }
1213
1764
  async writeAcrossLayers(key, kind, value, options) {
1214
1765
  const now = Date.now();
1215
- const operations = this.layers.map((layer) => async () => {
1216
- if (this.shouldSkipLayer(layer)) {
1217
- return;
1218
- }
1219
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1220
- const staleWhileRevalidate = this.resolveLayerSeconds(
1221
- layer.name,
1222
- options?.staleWhileRevalidate,
1223
- this.options.staleWhileRevalidate
1224
- );
1225
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1226
- const payload = createStoredValueEnvelope({
1227
- kind,
1228
- value,
1229
- freshTtlSeconds: freshTtl,
1230
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1231
- staleIfErrorSeconds: staleIfError,
1232
- now
1233
- });
1234
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1235
- try {
1236
- await layer.set(key, payload, ttl);
1237
- } catch (error) {
1238
- await this.handleLayerFailure(layer, "write", error);
1766
+ const immediateOperations = [];
1767
+ const deferredOperations = [];
1768
+ for (const layer of this.layers) {
1769
+ const operation = async () => {
1770
+ if (this.shouldSkipLayer(layer)) {
1771
+ return;
1772
+ }
1773
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1774
+ try {
1775
+ await layer.set(entry.key, entry.value, entry.ttl);
1776
+ } catch (error) {
1777
+ await this.handleLayerFailure(layer, "write", error);
1778
+ }
1779
+ };
1780
+ if (this.shouldWriteBehind(layer)) {
1781
+ deferredOperations.push(operation);
1782
+ } else {
1783
+ immediateOperations.push(operation);
1239
1784
  }
1240
- });
1241
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1785
+ }
1786
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1787
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1242
1788
  }
1243
1789
  async executeLayerOperations(operations, context) {
1244
1790
  if (this.options.writePolicy !== "best-effort") {
@@ -1262,8 +1808,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1262
1808
  );
1263
1809
  }
1264
1810
  }
1265
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1266
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1811
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1812
+ return this.ttlResolver.resolveFreshTtl(
1813
+ key,
1814
+ layerName,
1815
+ kind,
1816
+ options,
1817
+ fallbackTtl,
1818
+ this.options.negativeTtl,
1819
+ void 0,
1820
+ value
1821
+ );
1267
1822
  }
1268
1823
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1269
1824
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1339,6 +1894,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
1339
1894
  }
1340
1895
  }
1341
1896
  }
1897
+ async getTagsForKey(key) {
1898
+ if (this.tagIndex.tagsForKey) {
1899
+ return this.tagIndex.tagsForKey(key);
1900
+ }
1901
+ return [];
1902
+ }
1342
1903
  formatError(error) {
1343
1904
  if (error instanceof Error) {
1344
1905
  return error.message;
@@ -1351,6 +1912,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
1351
1912
  shouldBroadcastL1Invalidation() {
1352
1913
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1353
1914
  }
1915
+ initializeWriteBehind(options) {
1916
+ if (this.options.writeStrategy !== "write-behind") {
1917
+ return;
1918
+ }
1919
+ const flushIntervalMs = options?.flushIntervalMs;
1920
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1921
+ return;
1922
+ }
1923
+ this.writeBehindTimer = setInterval(() => {
1924
+ void this.flushWriteBehindQueue();
1925
+ }, flushIntervalMs);
1926
+ this.writeBehindTimer.unref?.();
1927
+ }
1928
+ shouldWriteBehind(layer) {
1929
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
1930
+ }
1931
+ async enqueueWriteBehind(operation) {
1932
+ this.writeBehindQueue.push(operation);
1933
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1934
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
1935
+ if (this.writeBehindQueue.length >= batchSize) {
1936
+ await this.flushWriteBehindQueue();
1937
+ return;
1938
+ }
1939
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1940
+ await this.flushWriteBehindQueue();
1941
+ }
1942
+ }
1943
+ async flushWriteBehindQueue() {
1944
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1945
+ await this.writeBehindFlushPromise;
1946
+ return;
1947
+ }
1948
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1949
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1950
+ this.writeBehindFlushPromise = (async () => {
1951
+ await Promise.allSettled(batch.map((operation) => operation()));
1952
+ })();
1953
+ await this.writeBehindFlushPromise;
1954
+ this.writeBehindFlushPromise = void 0;
1955
+ if (this.writeBehindQueue.length > 0) {
1956
+ await this.flushWriteBehindQueue();
1957
+ }
1958
+ }
1959
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
1960
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
1961
+ const staleWhileRevalidate = this.resolveLayerSeconds(
1962
+ layer.name,
1963
+ options?.staleWhileRevalidate,
1964
+ this.options.staleWhileRevalidate
1965
+ );
1966
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1967
+ const payload = createStoredValueEnvelope({
1968
+ kind,
1969
+ value,
1970
+ freshTtlSeconds: freshTtl,
1971
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
1972
+ staleIfErrorSeconds: staleIfError,
1973
+ now
1974
+ });
1975
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1976
+ return {
1977
+ key,
1978
+ value: payload,
1979
+ ttl
1980
+ };
1981
+ }
1982
+ intersectKeys(groups) {
1983
+ if (groups.length === 0) {
1984
+ return [];
1985
+ }
1986
+ const [firstGroup, ...rest] = groups;
1987
+ if (!firstGroup) {
1988
+ return [];
1989
+ }
1990
+ const restSets = rest.map((group) => new Set(group));
1991
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
1992
+ }
1993
+ qualifyKey(key) {
1994
+ const prefix = this.generationPrefix();
1995
+ return prefix ? `${prefix}${key}` : key;
1996
+ }
1997
+ qualifyPattern(pattern) {
1998
+ const prefix = this.generationPrefix();
1999
+ return prefix ? `${prefix}${pattern}` : pattern;
2000
+ }
2001
+ stripQualifiedKey(key) {
2002
+ const prefix = this.generationPrefix();
2003
+ if (!prefix || !key.startsWith(prefix)) {
2004
+ return key;
2005
+ }
2006
+ return key.slice(prefix.length);
2007
+ }
2008
+ generationPrefix() {
2009
+ if (this.currentGeneration === void 0) {
2010
+ return "";
2011
+ }
2012
+ return `v${this.currentGeneration}:`;
2013
+ }
1354
2014
  async deleteKeysFromLayers(layers, keys) {
1355
2015
  await Promise.all(
1356
2016
  layers.map(async (layer) => {
@@ -1394,6 +2054,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1394
2054
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1395
2055
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1396
2056
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2057
+ if (this.options.generation !== void 0) {
2058
+ this.validateNonNegativeNumber("generation", this.options.generation);
2059
+ }
1397
2060
  }
1398
2061
  validateWriteOptions(options) {
1399
2062
  if (!options) {
@@ -1405,6 +2068,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1405
2068
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1406
2069
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1407
2070
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2071
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1408
2072
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1409
2073
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1410
2074
  }
@@ -1448,6 +2112,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1448
2112
  }
1449
2113
  return key;
1450
2114
  }
2115
+ validateTtlPolicy(name, policy) {
2116
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2117
+ return;
2118
+ }
2119
+ if ("alignTo" in policy) {
2120
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2121
+ return;
2122
+ }
2123
+ throw new Error(`${name} is invalid.`);
2124
+ }
2125
+ assertActive(operation) {
2126
+ if (this.isDisconnecting) {
2127
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2128
+ }
2129
+ }
2130
+ async awaitStartup(operation) {
2131
+ this.assertActive(operation);
2132
+ await this.startup;
2133
+ this.assertActive(operation);
2134
+ }
1451
2135
  serializeOptions(options) {
1452
2136
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1453
2137
  }
@@ -1553,41 +2237,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
1553
2237
  return value;
1554
2238
  }
1555
2239
  };
2240
+ function createInstanceId() {
2241
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2242
+ }
1556
2243
 
1557
2244
  // src/invalidation/RedisInvalidationBus.ts
1558
2245
  var RedisInvalidationBus = class {
1559
2246
  channel;
1560
2247
  publisher;
1561
2248
  subscriber;
1562
- activeListener;
2249
+ logger;
2250
+ handlers = /* @__PURE__ */ new Set();
2251
+ sharedListener;
1563
2252
  constructor(options) {
1564
2253
  this.publisher = options.publisher;
1565
2254
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1566
2255
  this.channel = options.channel ?? "layercache:invalidation";
2256
+ this.logger = options.logger;
1567
2257
  }
1568
2258
  async subscribe(handler) {
1569
- if (this.activeListener) {
1570
- throw new Error("RedisInvalidationBus already has an active subscription.");
2259
+ if (this.handlers.size === 0) {
2260
+ const listener = (_channel, payload) => {
2261
+ void this.dispatchToHandlers(payload);
2262
+ };
2263
+ this.sharedListener = listener;
2264
+ this.subscriber.on("message", listener);
2265
+ await this.subscriber.subscribe(this.channel);
1571
2266
  }
1572
- const listener = (_channel, payload) => {
1573
- void this.handleMessage(payload, handler);
1574
- };
1575
- this.activeListener = listener;
1576
- this.subscriber.on("message", listener);
1577
- await this.subscriber.subscribe(this.channel);
2267
+ this.handlers.add(handler);
1578
2268
  return async () => {
1579
- if (this.activeListener !== listener) {
1580
- return;
2269
+ this.handlers.delete(handler);
2270
+ if (this.handlers.size === 0 && this.sharedListener) {
2271
+ this.subscriber.off("message", this.sharedListener);
2272
+ this.sharedListener = void 0;
2273
+ await this.subscriber.unsubscribe(this.channel);
1581
2274
  }
1582
- this.activeListener = void 0;
1583
- this.subscriber.off("message", listener);
1584
- await this.subscriber.unsubscribe(this.channel);
1585
2275
  };
1586
2276
  }
1587
2277
  async publish(message) {
1588
2278
  await this.publisher.publish(this.channel, JSON.stringify(message));
1589
2279
  }
1590
- async handleMessage(payload, handler) {
2280
+ async dispatchToHandlers(payload) {
1591
2281
  let message;
1592
2282
  try {
1593
2283
  const parsed = JSON.parse(payload);
@@ -1599,11 +2289,15 @@ var RedisInvalidationBus = class {
1599
2289
  this.reportError("invalid invalidation payload", error);
1600
2290
  return;
1601
2291
  }
1602
- try {
1603
- await handler(message);
1604
- } catch (error) {
1605
- this.reportError("invalidation handler failed", error);
1606
- }
2292
+ await Promise.all(
2293
+ [...this.handlers].map(async (handler) => {
2294
+ try {
2295
+ await handler(message);
2296
+ } catch (error) {
2297
+ this.reportError("invalidation handler failed", error);
2298
+ }
2299
+ })
2300
+ );
1607
2301
  }
1608
2302
  isInvalidationMessage(value) {
1609
2303
  if (!value || typeof value !== "object") {
@@ -1616,6 +2310,10 @@ var RedisInvalidationBus = class {
1616
2310
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1617
2311
  }
1618
2312
  reportError(message, error) {
2313
+ if (this.logger?.error) {
2314
+ this.logger.error(message, { error });
2315
+ return;
2316
+ }
1619
2317
  console.error(`[layercache] ${message}`, error);
1620
2318
  }
1621
2319
  };
@@ -1664,6 +2362,19 @@ var RedisTagIndex = class {
1664
2362
  async keysForTag(tag) {
1665
2363
  return this.client.smembers(this.tagKeysKey(tag));
1666
2364
  }
2365
+ async keysForPrefix(prefix) {
2366
+ const matches = [];
2367
+ let cursor = "0";
2368
+ do {
2369
+ const [nextCursor, keys] = await this.client.sscan(this.knownKeysKey(), cursor, "COUNT", this.scanCount);
2370
+ cursor = nextCursor;
2371
+ matches.push(...keys.filter((key) => key.startsWith(prefix)));
2372
+ } while (cursor !== "0");
2373
+ return matches;
2374
+ }
2375
+ async tagsForKey(key) {
2376
+ return this.client.smembers(this.keyTagsKey(key));
2377
+ }
1667
2378
  async matchPattern(pattern) {
1668
2379
  const matches = [];
1669
2380
  let cursor = "0";
@@ -1754,6 +2465,43 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1754
2465
  };
1755
2466
  }
1756
2467
 
2468
+ // src/integrations/express.ts
2469
+ function createExpressCacheMiddleware(cache, options = {}) {
2470
+ const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
2471
+ return async (req, res, next) => {
2472
+ try {
2473
+ const method = (req.method ?? "GET").toUpperCase();
2474
+ if (!allowedMethods.has(method)) {
2475
+ next();
2476
+ return;
2477
+ }
2478
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
2479
+ const cached = await cache.get(key, void 0, options);
2480
+ if (cached !== null) {
2481
+ res.setHeader?.("content-type", "application/json; charset=utf-8");
2482
+ res.setHeader?.("x-cache", "HIT");
2483
+ if (res.json) {
2484
+ res.json(cached);
2485
+ } else {
2486
+ res.end?.(JSON.stringify(cached));
2487
+ }
2488
+ return;
2489
+ }
2490
+ const originalJson = res.json?.bind(res);
2491
+ if (originalJson) {
2492
+ res.json = (body) => {
2493
+ res.setHeader?.("x-cache", "MISS");
2494
+ void cache.set(key, body, options);
2495
+ return originalJson(body);
2496
+ };
2497
+ }
2498
+ next();
2499
+ } catch (error) {
2500
+ next(error);
2501
+ }
2502
+ };
2503
+ }
2504
+
1757
2505
  // src/integrations/graphql.ts
1758
2506
  function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1759
2507
  const wrapped = cache.wrap(prefix, resolver, {
@@ -1763,6 +2511,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1763
2511
  return (...args) => wrapped(...args);
1764
2512
  }
1765
2513
 
2514
+ // src/integrations/hono.ts
2515
+ function createHonoCacheMiddleware(cache, options = {}) {
2516
+ const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
2517
+ return async (context, next) => {
2518
+ const method = (context.req.method ?? "GET").toUpperCase();
2519
+ if (!allowedMethods.has(method)) {
2520
+ await next();
2521
+ return;
2522
+ }
2523
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
2524
+ const cached = await cache.get(key, void 0, options);
2525
+ if (cached !== null) {
2526
+ context.header?.("x-cache", "HIT");
2527
+ context.header?.("content-type", "application/json; charset=utf-8");
2528
+ context.json(cached);
2529
+ return;
2530
+ }
2531
+ const originalJson = context.json.bind(context);
2532
+ context.json = (body, status) => {
2533
+ context.header?.("x-cache", "MISS");
2534
+ void cache.set(key, body, options);
2535
+ return originalJson(body, status);
2536
+ };
2537
+ await next();
2538
+ };
2539
+ }
2540
+
2541
+ // src/integrations/opentelemetry.ts
2542
+ function createOpenTelemetryPlugin(cache, tracer) {
2543
+ const originals = {
2544
+ get: cache.get.bind(cache),
2545
+ set: cache.set.bind(cache),
2546
+ delete: cache.delete.bind(cache),
2547
+ mget: cache.mget.bind(cache),
2548
+ mset: cache.mset.bind(cache),
2549
+ invalidateByTag: cache.invalidateByTag.bind(cache),
2550
+ invalidateByTags: cache.invalidateByTags.bind(cache),
2551
+ invalidateByPattern: cache.invalidateByPattern.bind(cache),
2552
+ invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
2553
+ };
2554
+ cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
2555
+ "layercache.key": String(args[0] ?? "")
2556
+ }));
2557
+ cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
2558
+ "layercache.key": String(args[0] ?? "")
2559
+ }));
2560
+ cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
2561
+ "layercache.key": String(args[0] ?? "")
2562
+ }));
2563
+ cache.mget = instrument("layercache.mget", tracer, originals.mget);
2564
+ cache.mset = instrument("layercache.mset", tracer, originals.mset);
2565
+ cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
2566
+ cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
2567
+ cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
2568
+ cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
2569
+ return {
2570
+ uninstall() {
2571
+ cache.get = originals.get;
2572
+ cache.set = originals.set;
2573
+ cache.delete = originals.delete;
2574
+ cache.mget = originals.mget;
2575
+ cache.mset = originals.mset;
2576
+ cache.invalidateByTag = originals.invalidateByTag;
2577
+ cache.invalidateByTags = originals.invalidateByTags;
2578
+ cache.invalidateByPattern = originals.invalidateByPattern;
2579
+ cache.invalidateByPrefix = originals.invalidateByPrefix;
2580
+ }
2581
+ };
2582
+ }
2583
+ function instrument(name, tracer, method, attributes) {
2584
+ return (async (...args) => {
2585
+ const span = tracer.startSpan(name, { attributes: attributes?.(args) });
2586
+ try {
2587
+ const result = await method(...args);
2588
+ span.setAttribute?.("layercache.success", true);
2589
+ if (result === null) {
2590
+ span.setAttribute?.("layercache.result", "null");
2591
+ }
2592
+ return result;
2593
+ } catch (error) {
2594
+ span.setAttribute?.("layercache.success", false);
2595
+ span.recordException?.(error);
2596
+ throw error;
2597
+ } finally {
2598
+ span.end();
2599
+ }
2600
+ });
2601
+ }
2602
+
1766
2603
  // src/integrations/trpc.ts
1767
2604
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1768
2605
  return async (context) => {
@@ -1795,12 +2632,21 @@ var MemoryLayer = class {
1795
2632
  isLocal = true;
1796
2633
  maxSize;
1797
2634
  evictionPolicy;
2635
+ onEvict;
1798
2636
  entries = /* @__PURE__ */ new Map();
2637
+ cleanupTimer;
1799
2638
  constructor(options = {}) {
1800
2639
  this.name = options.name ?? "memory";
1801
2640
  this.defaultTtl = options.ttl;
1802
2641
  this.maxSize = options.maxSize ?? 1e3;
1803
2642
  this.evictionPolicy = options.evictionPolicy ?? "lru";
2643
+ this.onEvict = options.onEvict;
2644
+ if (options.cleanupIntervalMs && options.cleanupIntervalMs > 0) {
2645
+ this.cleanupTimer = setInterval(() => {
2646
+ this.pruneExpired();
2647
+ }, options.cleanupIntervalMs);
2648
+ this.cleanupTimer.unref?.();
2649
+ }
1804
2650
  }
1805
2651
  async get(key) {
1806
2652
  const value = await this.getEntry(key);
@@ -1817,10 +2663,10 @@ var MemoryLayer = class {
1817
2663
  }
1818
2664
  if (this.evictionPolicy === "lru") {
1819
2665
  this.entries.delete(key);
1820
- entry.frequency += 1;
2666
+ entry.accessCount += 1;
1821
2667
  this.entries.set(key, entry);
1822
- } else {
1823
- entry.frequency += 1;
2668
+ } else if (this.evictionPolicy === "lfu") {
2669
+ entry.accessCount += 1;
1824
2670
  }
1825
2671
  return entry.value;
1826
2672
  }
@@ -1831,12 +2677,17 @@ var MemoryLayer = class {
1831
2677
  }
1832
2678
  return values;
1833
2679
  }
2680
+ async setMany(entries) {
2681
+ for (const entry of entries) {
2682
+ await this.set(entry.key, entry.value, entry.ttl);
2683
+ }
2684
+ }
1834
2685
  async set(key, value, ttl = this.defaultTtl) {
1835
2686
  this.entries.delete(key);
1836
2687
  this.entries.set(key, {
1837
2688
  value,
1838
2689
  expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
1839
- frequency: 0,
2690
+ accessCount: 0,
1840
2691
  insertedAt: Date.now()
1841
2692
  });
1842
2693
  while (this.entries.size > this.maxSize) {
@@ -1883,6 +2734,15 @@ var MemoryLayer = class {
1883
2734
  async clear() {
1884
2735
  this.entries.clear();
1885
2736
  }
2737
+ async ping() {
2738
+ return true;
2739
+ }
2740
+ async dispose() {
2741
+ if (this.cleanupTimer) {
2742
+ clearInterval(this.cleanupTimer);
2743
+ this.cleanupTimer = void 0;
2744
+ }
2745
+ }
1886
2746
  async keys() {
1887
2747
  this.pruneExpired();
1888
2748
  return [...this.entries.keys()];
@@ -1903,7 +2763,7 @@ var MemoryLayer = class {
1903
2763
  this.entries.set(entry.key, {
1904
2764
  value: entry.value,
1905
2765
  expiresAt: entry.expiresAt,
1906
- frequency: 0,
2766
+ accessCount: 0,
1907
2767
  insertedAt: Date.now()
1908
2768
  });
1909
2769
  }
@@ -1915,22 +2775,30 @@ var MemoryLayer = class {
1915
2775
  if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
1916
2776
  const oldestKey = this.entries.keys().next().value;
1917
2777
  if (oldestKey !== void 0) {
2778
+ const entry = this.entries.get(oldestKey);
1918
2779
  this.entries.delete(oldestKey);
2780
+ if (entry) {
2781
+ this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
2782
+ }
1919
2783
  }
1920
2784
  return;
1921
2785
  }
1922
2786
  let victimKey;
1923
- let minFreq = Number.POSITIVE_INFINITY;
2787
+ let minCount = Number.POSITIVE_INFINITY;
1924
2788
  let minInsertedAt = Number.POSITIVE_INFINITY;
1925
2789
  for (const [key, entry] of this.entries.entries()) {
1926
- if (entry.frequency < minFreq || entry.frequency === minFreq && entry.insertedAt < minInsertedAt) {
1927
- minFreq = entry.frequency;
2790
+ if (entry.accessCount < minCount || entry.accessCount === minCount && entry.insertedAt < minInsertedAt) {
2791
+ minCount = entry.accessCount;
1928
2792
  minInsertedAt = entry.insertedAt;
1929
2793
  victimKey = key;
1930
2794
  }
1931
2795
  }
1932
2796
  if (victimKey !== void 0) {
2797
+ const victim = this.entries.get(victimKey);
1933
2798
  this.entries.delete(victimKey);
2799
+ if (victim) {
2800
+ this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
2801
+ }
1934
2802
  }
1935
2803
  }
1936
2804
  pruneExpired() {
@@ -1946,6 +2814,7 @@ var MemoryLayer = class {
1946
2814
  };
1947
2815
 
1948
2816
  // src/layers/RedisLayer.ts
2817
+ var import_node_util = require("util");
1949
2818
  var import_node_zlib = require("zlib");
1950
2819
 
1951
2820
  // src/serialization/JsonSerializer.ts
@@ -1961,27 +2830,33 @@ var JsonSerializer = class {
1961
2830
 
1962
2831
  // src/layers/RedisLayer.ts
1963
2832
  var BATCH_DELETE_SIZE = 500;
2833
+ var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
2834
+ var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
2835
+ var brotliCompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliCompress);
2836
+ var brotliDecompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliDecompress);
1964
2837
  var RedisLayer = class {
1965
2838
  name;
1966
2839
  defaultTtl;
1967
2840
  isLocal = false;
1968
2841
  client;
1969
- serializer;
2842
+ serializers;
1970
2843
  prefix;
1971
2844
  allowUnprefixedClear;
1972
2845
  scanCount;
1973
2846
  compression;
1974
2847
  compressionThreshold;
2848
+ disconnectOnDispose;
1975
2849
  constructor(options) {
1976
2850
  this.client = options.client;
1977
2851
  this.defaultTtl = options.ttl;
1978
2852
  this.name = options.name ?? "redis";
1979
- this.serializer = options.serializer ?? new JsonSerializer();
2853
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
1980
2854
  this.prefix = options.prefix ?? "";
1981
2855
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
1982
2856
  this.scanCount = options.scanCount ?? 100;
1983
2857
  this.compression = options.compression;
1984
2858
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2859
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
1985
2860
  }
1986
2861
  async get(key) {
1987
2862
  const payload = await this.getEntry(key);
@@ -2016,8 +2891,26 @@ var RedisLayer = class {
2016
2891
  })
2017
2892
  );
2018
2893
  }
2894
+ async setMany(entries) {
2895
+ if (entries.length === 0) {
2896
+ return;
2897
+ }
2898
+ const pipeline = this.client.pipeline();
2899
+ for (const entry of entries) {
2900
+ const serialized = this.primarySerializer().serialize(entry.value);
2901
+ const payload = await this.encodePayload(serialized);
2902
+ const normalizedKey = this.withPrefix(entry.key);
2903
+ if (entry.ttl && entry.ttl > 0) {
2904
+ pipeline.set(normalizedKey, payload, "EX", entry.ttl);
2905
+ } else {
2906
+ pipeline.set(normalizedKey, payload);
2907
+ }
2908
+ }
2909
+ await pipeline.exec();
2910
+ }
2019
2911
  async set(key, value, ttl = this.defaultTtl) {
2020
- const payload = this.encodePayload(this.serializer.serialize(value));
2912
+ const serialized = this.primarySerializer().serialize(value);
2913
+ const payload = await this.encodePayload(serialized);
2021
2914
  const normalizedKey = this.withPrefix(key);
2022
2915
  if (ttl && ttl > 0) {
2023
2916
  await this.client.set(normalizedKey, payload, "EX", ttl);
@@ -2049,6 +2942,18 @@ var RedisLayer = class {
2049
2942
  const keys = await this.keys();
2050
2943
  return keys.length;
2051
2944
  }
2945
+ async ping() {
2946
+ try {
2947
+ return await this.client.ping() === "PONG";
2948
+ } catch {
2949
+ return false;
2950
+ }
2951
+ }
2952
+ async dispose() {
2953
+ if (this.disconnectOnDispose) {
2954
+ this.client.disconnect();
2955
+ }
2956
+ }
2052
2957
  /**
2053
2958
  * Deletes all keys matching the layer's prefix in batches to avoid
2054
2959
  * loading millions of keys into memory at once.
@@ -2095,17 +3000,48 @@ var RedisLayer = class {
2095
3000
  return `${this.prefix}${key}`;
2096
3001
  }
2097
3002
  async deserializeOrDelete(key, payload) {
3003
+ const decodedPayload = await this.decodePayload(payload);
3004
+ for (const serializer of this.serializers) {
3005
+ try {
3006
+ const value = serializer.deserialize(decodedPayload);
3007
+ if (serializer !== this.primarySerializer()) {
3008
+ await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
3009
+ }
3010
+ return value;
3011
+ } catch {
3012
+ }
3013
+ }
2098
3014
  try {
2099
- return this.serializer.deserialize(this.decodePayload(payload));
2100
- } catch {
2101
3015
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
2102
- return null;
3016
+ } catch {
2103
3017
  }
3018
+ return null;
3019
+ }
3020
+ async rewriteWithPrimarySerializer(key, value) {
3021
+ const serialized = this.primarySerializer().serialize(value);
3022
+ const payload = await this.encodePayload(serialized);
3023
+ const ttl = await this.client.ttl(this.withPrefix(key));
3024
+ if (ttl > 0) {
3025
+ await this.client.set(this.withPrefix(key), payload, "EX", ttl);
3026
+ return;
3027
+ }
3028
+ await this.client.set(this.withPrefix(key), payload);
3029
+ }
3030
+ primarySerializer() {
3031
+ const serializer = this.serializers[0];
3032
+ if (!serializer) {
3033
+ throw new Error("RedisLayer requires at least one serializer.");
3034
+ }
3035
+ return serializer;
2104
3036
  }
2105
3037
  isSerializablePayload(payload) {
2106
3038
  return typeof payload === "string" || Buffer.isBuffer(payload);
2107
3039
  }
2108
- encodePayload(payload) {
3040
+ /**
3041
+ * Compresses the payload asynchronously if compression is enabled and the
3042
+ * payload exceeds the threshold. This avoids blocking the event loop.
3043
+ */
3044
+ async encodePayload(payload) {
2109
3045
  if (!this.compression) {
2110
3046
  return payload;
2111
3047
  }
@@ -2114,26 +3050,29 @@ var RedisLayer = class {
2114
3050
  return payload;
2115
3051
  }
2116
3052
  const header = Buffer.from(`LCZ1:${this.compression}:`);
2117
- const compressed = this.compression === "gzip" ? (0, import_node_zlib.gzipSync)(source) : (0, import_node_zlib.brotliCompressSync)(source);
3053
+ const compressed = this.compression === "gzip" ? await gzipAsync(source) : await brotliCompressAsync(source);
2118
3054
  return Buffer.concat([header, compressed]);
2119
3055
  }
2120
- decodePayload(payload) {
3056
+ /**
3057
+ * Decompresses the payload asynchronously if a compression header is present.
3058
+ */
3059
+ async decodePayload(payload) {
2121
3060
  if (!Buffer.isBuffer(payload)) {
2122
3061
  return payload;
2123
3062
  }
2124
3063
  if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
2125
- return (0, import_node_zlib.gunzipSync)(payload.subarray(10));
3064
+ return gunzipAsync(payload.subarray(10));
2126
3065
  }
2127
3066
  if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
2128
- return (0, import_node_zlib.brotliDecompressSync)(payload.subarray(12));
3067
+ return brotliDecompressAsync(payload.subarray(12));
2129
3068
  }
2130
3069
  return payload;
2131
3070
  }
2132
3071
  };
2133
3072
 
2134
3073
  // src/layers/DiskLayer.ts
2135
- var import_node_crypto2 = require("crypto");
2136
- var import_node_fs2 = require("fs");
3074
+ var import_node_crypto = require("crypto");
3075
+ var import_node_fs = require("fs");
2137
3076
  var import_node_path = require("path");
2138
3077
  var DiskLayer = class {
2139
3078
  name;
@@ -2141,11 +3080,14 @@ var DiskLayer = class {
2141
3080
  isLocal = true;
2142
3081
  directory;
2143
3082
  serializer;
3083
+ maxFiles;
3084
+ writeQueue = Promise.resolve();
2144
3085
  constructor(options) {
2145
3086
  this.directory = options.directory;
2146
3087
  this.defaultTtl = options.ttl;
2147
3088
  this.name = options.name ?? "disk";
2148
3089
  this.serializer = options.serializer ?? new JsonSerializer();
3090
+ this.maxFiles = options.maxFiles;
2149
3091
  }
2150
3092
  async get(key) {
2151
3093
  return unwrapStoredValue(await this.getEntry(key));
@@ -2154,7 +3096,7 @@ var DiskLayer = class {
2154
3096
  const filePath = this.keyToPath(key);
2155
3097
  let raw;
2156
3098
  try {
2157
- raw = await import_node_fs2.promises.readFile(filePath);
3099
+ raw = await import_node_fs.promises.readFile(filePath);
2158
3100
  } catch {
2159
3101
  return null;
2160
3102
  }
@@ -2172,13 +3114,30 @@ var DiskLayer = class {
2172
3114
  return entry.value;
2173
3115
  }
2174
3116
  async set(key, value, ttl = this.defaultTtl) {
2175
- await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
2176
- const entry = {
2177
- value,
2178
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2179
- };
2180
- const payload = this.serializer.serialize(entry);
2181
- await import_node_fs2.promises.writeFile(this.keyToPath(key), payload);
3117
+ await this.enqueueWrite(async () => {
3118
+ await import_node_fs.promises.mkdir(this.directory, { recursive: true });
3119
+ const entry = {
3120
+ key,
3121
+ value,
3122
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
3123
+ };
3124
+ const payload = this.serializer.serialize(entry);
3125
+ const targetPath = this.keyToPath(key);
3126
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3127
+ await import_node_fs.promises.writeFile(tempPath, payload);
3128
+ await import_node_fs.promises.rename(tempPath, targetPath);
3129
+ if (this.maxFiles !== void 0) {
3130
+ await this.enforceMaxFiles();
3131
+ }
3132
+ });
3133
+ }
3134
+ async getMany(keys) {
3135
+ return Promise.all(keys.map((key) => this.getEntry(key)));
3136
+ }
3137
+ async setMany(entries) {
3138
+ for (const entry of entries) {
3139
+ await this.set(entry.key, entry.value, entry.ttl);
3140
+ }
2182
3141
  }
2183
3142
  async has(key) {
2184
3143
  const value = await this.getEntry(key);
@@ -2188,7 +3147,7 @@ var DiskLayer = class {
2188
3147
  const filePath = this.keyToPath(key);
2189
3148
  let raw;
2190
3149
  try {
2191
- raw = await import_node_fs2.promises.readFile(filePath);
3150
+ raw = await import_node_fs.promises.readFile(filePath);
2192
3151
  } catch {
2193
3152
  return null;
2194
3153
  }
@@ -2208,44 +3167,124 @@ var DiskLayer = class {
2208
3167
  return remaining;
2209
3168
  }
2210
3169
  async delete(key) {
2211
- await this.safeDelete(this.keyToPath(key));
3170
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2212
3171
  }
2213
3172
  async deleteMany(keys) {
2214
- await Promise.all(keys.map((key) => this.delete(key)));
3173
+ await this.enqueueWrite(async () => {
3174
+ await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
3175
+ });
2215
3176
  }
2216
3177
  async clear() {
2217
- let entries;
2218
- try {
2219
- entries = await import_node_fs2.promises.readdir(this.directory);
2220
- } catch {
2221
- return;
2222
- }
2223
- await Promise.all(
2224
- entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
2225
- );
3178
+ await this.enqueueWrite(async () => {
3179
+ let entries;
3180
+ try {
3181
+ entries = await import_node_fs.promises.readdir(this.directory);
3182
+ } catch {
3183
+ return;
3184
+ }
3185
+ await Promise.all(
3186
+ entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
3187
+ );
3188
+ });
2226
3189
  }
3190
+ /**
3191
+ * Returns the original cache key strings stored on disk.
3192
+ * Expired entries are skipped and cleaned up during the scan.
3193
+ */
2227
3194
  async keys() {
2228
3195
  let entries;
2229
3196
  try {
2230
- entries = await import_node_fs2.promises.readdir(this.directory);
3197
+ entries = await import_node_fs.promises.readdir(this.directory);
2231
3198
  } catch {
2232
3199
  return [];
2233
3200
  }
2234
- return entries.filter((name) => name.endsWith(".lc")).map((name) => name.slice(0, -3));
3201
+ const lcFiles = entries.filter((name) => name.endsWith(".lc"));
3202
+ const keys = [];
3203
+ await Promise.all(
3204
+ lcFiles.map(async (name) => {
3205
+ const filePath = (0, import_node_path.join)(this.directory, name);
3206
+ let raw;
3207
+ try {
3208
+ raw = await import_node_fs.promises.readFile(filePath);
3209
+ } catch {
3210
+ return;
3211
+ }
3212
+ let entry;
3213
+ try {
3214
+ entry = this.serializer.deserialize(raw);
3215
+ } catch {
3216
+ await this.safeDelete(filePath);
3217
+ return;
3218
+ }
3219
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
3220
+ await this.safeDelete(filePath);
3221
+ return;
3222
+ }
3223
+ keys.push(entry.key);
3224
+ })
3225
+ );
3226
+ return keys;
2235
3227
  }
2236
3228
  async size() {
2237
3229
  const keys = await this.keys();
2238
3230
  return keys.length;
2239
3231
  }
3232
+ async ping() {
3233
+ try {
3234
+ await import_node_fs.promises.mkdir(this.directory, { recursive: true });
3235
+ return true;
3236
+ } catch {
3237
+ return false;
3238
+ }
3239
+ }
3240
+ async dispose() {
3241
+ }
2240
3242
  keyToPath(key) {
2241
- const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
3243
+ const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
2242
3244
  return (0, import_node_path.join)(this.directory, `${hash}.lc`);
2243
3245
  }
2244
3246
  async safeDelete(filePath) {
2245
3247
  try {
2246
- await import_node_fs2.promises.unlink(filePath);
3248
+ await import_node_fs.promises.unlink(filePath);
3249
+ } catch {
3250
+ }
3251
+ }
3252
+ enqueueWrite(operation) {
3253
+ const next = this.writeQueue.then(operation, operation);
3254
+ this.writeQueue = next.catch(() => void 0);
3255
+ return next;
3256
+ }
3257
+ /**
3258
+ * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
3259
+ */
3260
+ async enforceMaxFiles() {
3261
+ if (this.maxFiles === void 0) {
3262
+ return;
3263
+ }
3264
+ let entries;
3265
+ try {
3266
+ entries = await import_node_fs.promises.readdir(this.directory);
2247
3267
  } catch {
3268
+ return;
3269
+ }
3270
+ const lcFiles = entries.filter((name) => name.endsWith(".lc"));
3271
+ if (lcFiles.length <= this.maxFiles) {
3272
+ return;
2248
3273
  }
3274
+ const withStats = await Promise.all(
3275
+ lcFiles.map(async (name) => {
3276
+ const filePath = (0, import_node_path.join)(this.directory, name);
3277
+ try {
3278
+ const stat = await import_node_fs.promises.stat(filePath);
3279
+ return { filePath, mtimeMs: stat.mtimeMs };
3280
+ } catch {
3281
+ return { filePath, mtimeMs: 0 };
3282
+ }
3283
+ })
3284
+ );
3285
+ withStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
3286
+ const toEvict = withStats.slice(0, lcFiles.length - this.maxFiles);
3287
+ await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
2249
3288
  }
2250
3289
  };
2251
3290
 
@@ -2256,29 +3295,41 @@ var MemcachedLayer = class {
2256
3295
  isLocal = false;
2257
3296
  client;
2258
3297
  keyPrefix;
3298
+ serializer;
2259
3299
  constructor(options) {
2260
3300
  this.client = options.client;
2261
3301
  this.defaultTtl = options.ttl;
2262
3302
  this.name = options.name ?? "memcached";
2263
3303
  this.keyPrefix = options.keyPrefix ?? "";
3304
+ this.serializer = options.serializer ?? new JsonSerializer();
2264
3305
  }
2265
3306
  async get(key) {
3307
+ return unwrapStoredValue(await this.getEntry(key));
3308
+ }
3309
+ async getEntry(key) {
2266
3310
  const result = await this.client.get(this.withPrefix(key));
2267
3311
  if (!result || result.value === null) {
2268
3312
  return null;
2269
3313
  }
2270
3314
  try {
2271
- return JSON.parse(result.value.toString("utf8"));
3315
+ return this.serializer.deserialize(result.value);
2272
3316
  } catch {
2273
3317
  return null;
2274
3318
  }
2275
3319
  }
3320
+ async getMany(keys) {
3321
+ return Promise.all(keys.map((key) => this.getEntry(key)));
3322
+ }
2276
3323
  async set(key, value, ttl = this.defaultTtl) {
2277
- const payload = JSON.stringify(value);
3324
+ const payload = this.serializer.serialize(value);
2278
3325
  await this.client.set(this.withPrefix(key), payload, {
2279
3326
  expires: ttl && ttl > 0 ? ttl : void 0
2280
3327
  });
2281
3328
  }
3329
+ async has(key) {
3330
+ const result = await this.client.get(this.withPrefix(key));
3331
+ return result !== null && result.value !== null;
3332
+ }
2282
3333
  async delete(key) {
2283
3334
  await this.client.delete(this.withPrefix(key));
2284
3335
  }
@@ -2308,7 +3359,7 @@ var MsgpackSerializer = class {
2308
3359
  };
2309
3360
 
2310
3361
  // src/singleflight/RedisSingleFlightCoordinator.ts
2311
- var import_node_crypto3 = require("crypto");
3362
+ var import_node_crypto2 = require("crypto");
2312
3363
  var RELEASE_SCRIPT = `
2313
3364
  if redis.call("get", KEYS[1]) == ARGV[1] then
2314
3365
  return redis.call("del", KEYS[1])
@@ -2324,7 +3375,7 @@ var RedisSingleFlightCoordinator = class {
2324
3375
  }
2325
3376
  async execute(key, options, worker, waiter) {
2326
3377
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2327
- const token = (0, import_node_crypto3.randomUUID)();
3378
+ const token = (0, import_node_crypto2.randomUUID)();
2328
3379
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2329
3380
  if (acquired === "OK") {
2330
3381
  try {
@@ -2372,6 +3423,12 @@ function createPrometheusMetricsExporter(stacks) {
2372
3423
  lines.push("# TYPE layercache_hits_by_layer_total counter");
2373
3424
  lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
2374
3425
  lines.push("# TYPE layercache_misses_by_layer_total counter");
3426
+ lines.push("# HELP layercache_layer_latency_avg_ms Average read latency per layer in milliseconds");
3427
+ lines.push("# TYPE layercache_layer_latency_avg_ms gauge");
3428
+ lines.push("# HELP layercache_layer_latency_max_ms Maximum read latency per layer in milliseconds");
3429
+ lines.push("# TYPE layercache_layer_latency_max_ms gauge");
3430
+ lines.push("# HELP layercache_layer_latency_count Number of read latency samples per layer");
3431
+ lines.push("# TYPE layercache_layer_latency_count counter");
2375
3432
  for (const { stack, name } of entries) {
2376
3433
  const m = stack.getMetrics();
2377
3434
  const hr = stack.getHitRate();
@@ -2395,6 +3452,12 @@ function createPrometheusMetricsExporter(stacks) {
2395
3452
  for (const [layerName, count] of Object.entries(m.missesByLayer)) {
2396
3453
  lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
2397
3454
  }
3455
+ for (const [layerName, latency] of Object.entries(m.latencyByLayer)) {
3456
+ const layerLabel = `${label},layer="${sanitizeLabel(layerName)}"`;
3457
+ lines.push(`layercache_layer_latency_avg_ms{${layerLabel}} ${latency.avgMs.toFixed(4)}`);
3458
+ lines.push(`layercache_layer_latency_max_ms{${layerLabel}} ${latency.maxMs.toFixed(4)}`);
3459
+ lines.push(`layercache_layer_latency_count{${layerLabel}} ${latency.count}`);
3460
+ }
2398
3461
  }
2399
3462
  lines.push("");
2400
3463
  return lines.join("\n");
@@ -2405,6 +3468,7 @@ function sanitizeLabel(value) {
2405
3468
  }
2406
3469
  // Annotate the CommonJS export names for ESM import in node:
2407
3470
  0 && (module.exports = {
3471
+ CacheMissError,
2408
3472
  CacheNamespace,
2409
3473
  CacheStack,
2410
3474
  DiskLayer,
@@ -2422,7 +3486,10 @@ function sanitizeLabel(value) {
2422
3486
  cacheGraphqlResolver,
2423
3487
  createCacheStatsHandler,
2424
3488
  createCachedMethodDecorator,
3489
+ createExpressCacheMiddleware,
2425
3490
  createFastifyLayercachePlugin,
3491
+ createHonoCacheMiddleware,
3492
+ createOpenTelemetryPlugin,
2426
3493
  createPrometheusMetricsExporter,
2427
3494
  createTrpcCacheMiddleware
2428
3495
  });