layercache 1.2.0 → 1.2.2

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,6 +17,14 @@ 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
@@ -40,17 +50,18 @@ __export(index_exports, {
40
50
  createCachedMethodDecorator: () => createCachedMethodDecorator,
41
51
  createExpressCacheMiddleware: () => createExpressCacheMiddleware,
42
52
  createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
53
+ createHonoCacheMiddleware: () => createHonoCacheMiddleware,
54
+ createOpenTelemetryPlugin: () => createOpenTelemetryPlugin,
43
55
  createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
44
56
  createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
45
57
  });
46
58
  module.exports = __toCommonJS(index_exports);
47
59
 
48
60
  // src/CacheStack.ts
49
- var import_node_crypto = require("crypto");
50
61
  var import_node_events = require("events");
51
- var import_node_fs = require("fs");
52
62
 
53
63
  // src/CacheNamespace.ts
64
+ var import_async_mutex = require("async-mutex");
54
65
  var CacheNamespace = class _CacheNamespace {
55
66
  constructor(cache, prefix) {
56
67
  this.cache = cache;
@@ -58,57 +69,69 @@ var CacheNamespace = class _CacheNamespace {
58
69
  }
59
70
  cache;
60
71
  prefix;
72
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
73
+ metrics = emptyMetrics();
61
74
  async get(key, fetcher, options) {
62
- return this.cache.get(this.qualify(key), fetcher, options);
75
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
63
76
  }
64
77
  async getOrSet(key, fetcher, options) {
65
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
78
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
66
79
  }
67
80
  /**
68
81
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
69
82
  */
70
83
  async getOrThrow(key, fetcher, options) {
71
- return this.cache.getOrThrow(this.qualify(key), fetcher, options);
84
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
72
85
  }
73
86
  async has(key) {
74
- return this.cache.has(this.qualify(key));
87
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
75
88
  }
76
89
  async ttl(key) {
77
- return this.cache.ttl(this.qualify(key));
90
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
78
91
  }
79
92
  async set(key, value, options) {
80
- await this.cache.set(this.qualify(key), value, options);
93
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
81
94
  }
82
95
  async delete(key) {
83
- await this.cache.delete(this.qualify(key));
96
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
84
97
  }
85
98
  async mdelete(keys) {
86
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
99
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
87
100
  }
88
101
  async clear() {
89
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
102
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
90
103
  }
91
104
  async mget(entries) {
92
- return this.cache.mget(
93
- entries.map((entry) => ({
94
- ...entry,
95
- key: this.qualify(entry.key)
96
- }))
105
+ return this.trackMetrics(
106
+ () => this.cache.mget(
107
+ entries.map((entry) => ({
108
+ ...entry,
109
+ key: this.qualify(entry.key)
110
+ }))
111
+ )
97
112
  );
98
113
  }
99
114
  async mset(entries) {
100
- await this.cache.mset(
101
- entries.map((entry) => ({
102
- ...entry,
103
- key: this.qualify(entry.key)
104
- }))
115
+ await this.trackMetrics(
116
+ () => this.cache.mset(
117
+ entries.map((entry) => ({
118
+ ...entry,
119
+ key: this.qualify(entry.key)
120
+ }))
121
+ )
105
122
  );
106
123
  }
107
124
  async invalidateByTag(tag) {
108
- 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));
109
129
  }
110
130
  async invalidateByPattern(pattern) {
111
- 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)));
112
135
  }
113
136
  /**
114
137
  * Returns detailed metadata about a single cache key within this namespace.
@@ -129,10 +152,19 @@ var CacheNamespace = class _CacheNamespace {
129
152
  );
130
153
  }
131
154
  getMetrics() {
132
- return this.cache.getMetrics();
155
+ return cloneMetrics(this.metrics);
133
156
  }
134
157
  getHitRate() {
135
- 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 };
136
168
  }
137
169
  /**
138
170
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -149,7 +181,130 @@ var CacheNamespace = class _CacheNamespace {
149
181
  qualify(key) {
150
182
  return `${this.prefix}:${key}`;
151
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
+ }
152
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
+ }
153
308
 
154
309
  // src/internal/CircuitBreakerManager.ts
155
310
  var CircuitBreakerManager = class {
@@ -243,6 +398,148 @@ var CircuitBreakerManager = class {
243
398
  }
244
399
  };
245
400
 
401
+ // src/internal/FetchRateLimiter.ts
402
+ var FetchRateLimiter = class {
403
+ queue = [];
404
+ buckets = /* @__PURE__ */ new Map();
405
+ fetcherBuckets = /* @__PURE__ */ new WeakMap();
406
+ nextFetcherBucketId = 0;
407
+ drainTimer;
408
+ async schedule(options, context, task) {
409
+ if (!options) {
410
+ return task();
411
+ }
412
+ const normalized = this.normalize(options);
413
+ if (!normalized) {
414
+ return task();
415
+ }
416
+ return new Promise((resolve2, reject) => {
417
+ this.queue.push({
418
+ bucketKey: this.resolveBucketKey(normalized, context),
419
+ options: normalized,
420
+ task,
421
+ resolve: resolve2,
422
+ reject
423
+ });
424
+ this.drain();
425
+ });
426
+ }
427
+ normalize(options) {
428
+ const maxConcurrent = options.maxConcurrent;
429
+ const intervalMs = options.intervalMs;
430
+ const maxPerInterval = options.maxPerInterval;
431
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
432
+ return void 0;
433
+ }
434
+ return {
435
+ maxConcurrent,
436
+ intervalMs,
437
+ maxPerInterval,
438
+ scope: options.scope ?? "global",
439
+ bucketKey: options.bucketKey
440
+ };
441
+ }
442
+ resolveBucketKey(options, context) {
443
+ if (options.bucketKey) {
444
+ return `custom:${options.bucketKey}`;
445
+ }
446
+ if (options.scope === "key") {
447
+ return `key:${context.key}`;
448
+ }
449
+ if (options.scope === "fetcher") {
450
+ const existing = this.fetcherBuckets.get(context.fetcher);
451
+ if (existing) {
452
+ return existing;
453
+ }
454
+ const bucket = `fetcher:${this.nextFetcherBucketId}`;
455
+ this.nextFetcherBucketId += 1;
456
+ this.fetcherBuckets.set(context.fetcher, bucket);
457
+ return bucket;
458
+ }
459
+ return "global";
460
+ }
461
+ drain() {
462
+ if (this.drainTimer) {
463
+ clearTimeout(this.drainTimer);
464
+ this.drainTimer = void 0;
465
+ }
466
+ while (this.queue.length > 0) {
467
+ let nextIndex = -1;
468
+ let nextWaitMs = Number.POSITIVE_INFINITY;
469
+ for (let index = 0; index < this.queue.length; index += 1) {
470
+ const next2 = this.queue[index];
471
+ if (!next2) {
472
+ continue;
473
+ }
474
+ const waitMs = this.waitTime(next2.bucketKey, next2.options);
475
+ if (waitMs <= 0) {
476
+ nextIndex = index;
477
+ break;
478
+ }
479
+ nextWaitMs = Math.min(nextWaitMs, waitMs);
480
+ }
481
+ if (nextIndex < 0) {
482
+ if (Number.isFinite(nextWaitMs)) {
483
+ this.drainTimer = setTimeout(() => {
484
+ this.drainTimer = void 0;
485
+ this.drain();
486
+ }, nextWaitMs);
487
+ this.drainTimer.unref?.();
488
+ }
489
+ return;
490
+ }
491
+ const next = this.queue.splice(nextIndex, 1)[0];
492
+ if (!next) {
493
+ return;
494
+ }
495
+ const bucket = this.bucketState(next.bucketKey);
496
+ bucket.active += 1;
497
+ bucket.startedAt.push(Date.now());
498
+ void next.task().then(next.resolve, next.reject).finally(() => {
499
+ bucket.active -= 1;
500
+ this.drain();
501
+ });
502
+ }
503
+ }
504
+ waitTime(bucketKey, options) {
505
+ const bucket = this.bucketState(bucketKey);
506
+ const now = Date.now();
507
+ if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
508
+ return 1;
509
+ }
510
+ if (!options.intervalMs || !options.maxPerInterval) {
511
+ return 0;
512
+ }
513
+ this.prune(bucket, now, options.intervalMs);
514
+ if (bucket.startedAt.length < options.maxPerInterval) {
515
+ return 0;
516
+ }
517
+ const oldest = bucket.startedAt[0];
518
+ if (!oldest) {
519
+ return 0;
520
+ }
521
+ return Math.max(1, options.intervalMs - (now - oldest));
522
+ }
523
+ prune(bucket, now, intervalMs) {
524
+ while (bucket.startedAt.length > 0) {
525
+ const startedAt = bucket.startedAt[0];
526
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
527
+ break;
528
+ }
529
+ bucket.startedAt.shift();
530
+ }
531
+ }
532
+ bucketState(bucketKey) {
533
+ const existing = this.buckets.get(bucketKey);
534
+ if (existing) {
535
+ return existing;
536
+ }
537
+ const bucket = { active: 0, startedAt: [] };
538
+ this.buckets.set(bucketKey, bucket);
539
+ return bucket;
540
+ }
541
+ };
542
+
246
543
  // src/internal/MetricsCollector.ts
247
544
  var MetricsCollector = class {
248
545
  data = this.empty();
@@ -439,13 +736,14 @@ var TtlResolver = class {
439
736
  clearProfiles() {
440
737
  this.accessProfiles.clear();
441
738
  }
442
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
739
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
740
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
443
741
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
444
742
  layerName,
445
743
  options?.negativeTtl,
446
744
  globalNegativeTtl,
447
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
448
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
745
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
746
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
449
747
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
450
748
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
451
749
  return this.applyJitter(adaptiveTtl, jitter);
@@ -484,6 +782,29 @@ var TtlResolver = class {
484
782
  const delta = (Math.random() * 2 - 1) * jitter;
485
783
  return Math.max(1, Math.round(ttl + delta));
486
784
  }
785
+ resolvePolicyTtl(key, value, policy) {
786
+ if (!policy) {
787
+ return void 0;
788
+ }
789
+ if (typeof policy === "function") {
790
+ return policy({ key, value });
791
+ }
792
+ const now = /* @__PURE__ */ new Date();
793
+ if (policy === "until-midnight") {
794
+ const nextMidnight = new Date(now);
795
+ nextMidnight.setHours(24, 0, 0, 0);
796
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
797
+ }
798
+ if (policy === "next-hour") {
799
+ const nextHour = new Date(now);
800
+ nextHour.setMinutes(60, 0, 0);
801
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
802
+ }
803
+ const alignToSeconds = policy.alignTo;
804
+ const currentSeconds = Math.floor(Date.now() / 1e3);
805
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
806
+ return Math.max(1, nextBoundary - currentSeconds);
807
+ }
487
808
  readLayerNumber(layerName, value) {
488
809
  if (typeof value === "number") {
489
810
  return value;
@@ -511,36 +832,46 @@ var PatternMatcher = class _PatternMatcher {
511
832
  /**
512
833
  * Tests whether a glob-style pattern matches a value.
513
834
  * Supports `*` (any sequence of characters) and `?` (any single character).
514
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
835
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
836
+ * quadratic memory usage on long patterns/keys.
515
837
  */
516
838
  static matches(pattern, value) {
517
839
  return _PatternMatcher.matchLinear(pattern, value);
518
840
  }
519
841
  /**
520
- * Linear-time glob matching using dynamic programming.
521
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
842
+ * Linear-time glob matching with O(1) extra memory.
522
843
  */
523
844
  static matchLinear(pattern, value) {
524
- const m = pattern.length;
525
- const n = value.length;
526
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
527
- dp[0][0] = true;
528
- for (let i = 1; i <= m; i++) {
529
- if (pattern[i - 1] === "*") {
530
- dp[i][0] = dp[i - 1]?.[0];
531
- }
532
- }
533
- for (let i = 1; i <= m; i++) {
534
- for (let j = 1; j <= n; j++) {
535
- const pc = pattern[i - 1];
536
- if (pc === "*") {
537
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
538
- } else if (pc === "?" || pc === value[j - 1]) {
539
- dp[i][j] = dp[i - 1]?.[j - 1];
540
- }
845
+ let patternIndex = 0;
846
+ let valueIndex = 0;
847
+ let starIndex = -1;
848
+ let backtrackValueIndex = 0;
849
+ while (valueIndex < value.length) {
850
+ const patternChar = pattern[patternIndex];
851
+ const valueChar = value[valueIndex];
852
+ if (patternChar === "*" && patternIndex < pattern.length) {
853
+ starIndex = patternIndex;
854
+ patternIndex += 1;
855
+ backtrackValueIndex = valueIndex;
856
+ continue;
541
857
  }
858
+ if (patternChar === "?" || patternChar === valueChar) {
859
+ patternIndex += 1;
860
+ valueIndex += 1;
861
+ continue;
862
+ }
863
+ if (starIndex !== -1) {
864
+ patternIndex = starIndex + 1;
865
+ backtrackValueIndex += 1;
866
+ valueIndex = backtrackValueIndex;
867
+ continue;
868
+ }
869
+ return false;
542
870
  }
543
- return dp[m]?.[n];
871
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
872
+ patternIndex += 1;
873
+ }
874
+ return patternIndex === pattern.length;
544
875
  }
545
876
  };
546
877
 
@@ -578,26 +909,14 @@ var TagIndex = class {
578
909
  }
579
910
  }
580
911
  async remove(key) {
581
- this.knownKeys.delete(key);
582
- const tags = this.keyToTags.get(key);
583
- if (!tags) {
584
- return;
585
- }
586
- for (const tag of tags) {
587
- const keys = this.tagToKeys.get(tag);
588
- if (!keys) {
589
- continue;
590
- }
591
- keys.delete(key);
592
- if (keys.size === 0) {
593
- this.tagToKeys.delete(tag);
594
- }
595
- }
596
- this.keyToTags.delete(key);
912
+ this.removeKey(key);
597
913
  }
598
914
  async keysForTag(tag) {
599
915
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
600
916
  }
917
+ async keysForPrefix(prefix) {
918
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
919
+ }
601
920
  async tagsForKey(key) {
602
921
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
603
922
  }
@@ -619,15 +938,32 @@ var TagIndex = class {
619
938
  if (removed >= toRemove) {
620
939
  break;
621
940
  }
622
- this.knownKeys.delete(key);
623
- this.keyToTags.delete(key);
941
+ this.removeKey(key);
624
942
  removed += 1;
625
943
  }
626
944
  }
945
+ removeKey(key) {
946
+ this.knownKeys.delete(key);
947
+ const tags = this.keyToTags.get(key);
948
+ if (!tags) {
949
+ return;
950
+ }
951
+ for (const tag of tags) {
952
+ const keys = this.tagToKeys.get(tag);
953
+ if (!keys) {
954
+ continue;
955
+ }
956
+ keys.delete(key);
957
+ if (keys.size === 0) {
958
+ this.tagToKeys.delete(tag);
959
+ }
960
+ }
961
+ this.keyToTags.delete(key);
962
+ }
627
963
  };
628
964
 
629
965
  // src/stampede/StampedeGuard.ts
630
- var import_async_mutex = require("async-mutex");
966
+ var import_async_mutex2 = require("async-mutex");
631
967
  var StampedeGuard = class {
632
968
  mutexes = /* @__PURE__ */ new Map();
633
969
  async execute(key, task) {
@@ -644,7 +980,7 @@ var StampedeGuard = class {
644
980
  getMutexEntry(key) {
645
981
  let entry = this.mutexes.get(key);
646
982
  if (!entry) {
647
- entry = { mutex: new import_async_mutex.Mutex(), references: 0 };
983
+ entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
648
984
  this.mutexes.set(key, entry);
649
985
  }
650
986
  entry.references += 1;
@@ -705,6 +1041,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
705
1041
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
706
1042
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
707
1043
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1044
+ this.currentGeneration = options.generation;
708
1045
  if (options.publishSetInvalidation !== void 0) {
709
1046
  console.warn(
710
1047
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -713,21 +1050,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
713
1050
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
714
1051
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
715
1052
  this.tagIndex = options.tagIndex ?? new TagIndex();
1053
+ this.initializeWriteBehind(options.writeBehind);
716
1054
  this.startup = this.initialize();
717
1055
  }
718
1056
  layers;
719
1057
  options;
720
1058
  stampedeGuard = new StampedeGuard();
721
1059
  metricsCollector = new MetricsCollector();
722
- instanceId = (0, import_node_crypto.randomUUID)();
1060
+ instanceId = createInstanceId();
723
1061
  startup;
724
1062
  unsubscribeInvalidation;
725
1063
  logger;
726
1064
  tagIndex;
1065
+ fetchRateLimiter = new FetchRateLimiter();
727
1066
  backgroundRefreshes = /* @__PURE__ */ new Map();
728
1067
  layerDegradedUntil = /* @__PURE__ */ new Map();
729
1068
  ttlResolver;
730
1069
  circuitBreakerManager;
1070
+ currentGeneration;
1071
+ writeBehindQueue = [];
1072
+ writeBehindTimer;
1073
+ writeBehindFlushPromise;
731
1074
  isDisconnecting = false;
732
1075
  disconnectPromise;
733
1076
  /**
@@ -737,9 +1080,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
737
1080
  * and no `fetcher` is provided.
738
1081
  */
739
1082
  async get(key, fetcher, options) {
740
- const normalizedKey = this.validateCacheKey(key);
1083
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
741
1084
  this.validateWriteOptions(options);
742
- await this.startup;
1085
+ await this.awaitStartup("get");
743
1086
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
744
1087
  if (hit.found) {
745
1088
  this.ttlResolver.recordAccess(normalizedKey);
@@ -804,8 +1147,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
804
1147
  * Returns true if the given key exists and is not expired in any layer.
805
1148
  */
806
1149
  async has(key) {
807
- const normalizedKey = this.validateCacheKey(key);
808
- await this.startup;
1150
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1151
+ await this.awaitStartup("has");
809
1152
  for (const layer of this.layers) {
810
1153
  if (this.shouldSkipLayer(layer)) {
811
1154
  continue;
@@ -835,8 +1178,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
835
1178
  * that has it, or null if the key is not found / has no TTL.
836
1179
  */
837
1180
  async ttl(key) {
838
- const normalizedKey = this.validateCacheKey(key);
839
- await this.startup;
1181
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1182
+ await this.awaitStartup("ttl");
840
1183
  for (const layer of this.layers) {
841
1184
  if (this.shouldSkipLayer(layer)) {
842
1185
  continue;
@@ -857,17 +1200,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
857
1200
  * Stores a value in all cache layers. Overwrites any existing value.
858
1201
  */
859
1202
  async set(key, value, options) {
860
- const normalizedKey = this.validateCacheKey(key);
1203
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
861
1204
  this.validateWriteOptions(options);
862
- await this.startup;
1205
+ await this.awaitStartup("set");
863
1206
  await this.storeEntry(normalizedKey, "value", value, options);
864
1207
  }
865
1208
  /**
866
1209
  * Deletes the key from all layers and publishes an invalidation message.
867
1210
  */
868
1211
  async delete(key) {
869
- const normalizedKey = this.validateCacheKey(key);
870
- await this.startup;
1212
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1213
+ await this.awaitStartup("delete");
871
1214
  await this.deleteKeys([normalizedKey]);
872
1215
  await this.publishInvalidation({
873
1216
  scope: "key",
@@ -877,7 +1220,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
877
1220
  });
878
1221
  }
879
1222
  async clear() {
880
- await this.startup;
1223
+ await this.awaitStartup("clear");
881
1224
  await Promise.all(this.layers.map((layer) => layer.clear()));
882
1225
  await this.tagIndex.clear();
883
1226
  this.ttlResolver.clearProfiles();
@@ -893,23 +1236,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
893
1236
  if (keys.length === 0) {
894
1237
  return;
895
1238
  }
896
- await this.startup;
1239
+ await this.awaitStartup("mdelete");
897
1240
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
898
- await this.deleteKeys(normalizedKeys);
1241
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1242
+ await this.deleteKeys(cacheKeys);
899
1243
  await this.publishInvalidation({
900
1244
  scope: "keys",
901
- keys: normalizedKeys,
1245
+ keys: cacheKeys,
902
1246
  sourceId: this.instanceId,
903
1247
  operation: "delete"
904
1248
  });
905
1249
  }
906
1250
  async mget(entries) {
1251
+ this.assertActive("mget");
907
1252
  if (entries.length === 0) {
908
1253
  return [];
909
1254
  }
910
1255
  const normalizedEntries = entries.map((entry) => ({
911
1256
  ...entry,
912
- key: this.validateCacheKey(entry.key)
1257
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
913
1258
  }));
914
1259
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
915
1260
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -935,7 +1280,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
935
1280
  })
936
1281
  );
937
1282
  }
938
- await this.startup;
1283
+ await this.awaitStartup("mget");
939
1284
  const pending = /* @__PURE__ */ new Set();
940
1285
  const indexesByKey = /* @__PURE__ */ new Map();
941
1286
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -983,14 +1328,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
983
1328
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
984
1329
  }
985
1330
  async mset(entries) {
1331
+ this.assertActive("mset");
986
1332
  const normalizedEntries = entries.map((entry) => ({
987
1333
  ...entry,
988
- key: this.validateCacheKey(entry.key)
1334
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
989
1335
  }));
990
1336
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
991
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1337
+ await this.awaitStartup("mset");
1338
+ await this.writeBatch(normalizedEntries);
992
1339
  }
993
1340
  async warm(entries, options = {}) {
1341
+ this.assertActive("warm");
994
1342
  const concurrency = Math.max(1, options.concurrency ?? 4);
995
1343
  const total = entries.length;
996
1344
  let completed = 0;
@@ -1039,14 +1387,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
1039
1387
  return new CacheNamespace(this, prefix);
1040
1388
  }
1041
1389
  async invalidateByTag(tag) {
1042
- await this.startup;
1390
+ await this.awaitStartup("invalidateByTag");
1043
1391
  const keys = await this.tagIndex.keysForTag(tag);
1044
1392
  await this.deleteKeys(keys);
1045
1393
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1046
1394
  }
1395
+ async invalidateByTags(tags, mode = "any") {
1396
+ if (tags.length === 0) {
1397
+ return;
1398
+ }
1399
+ await this.awaitStartup("invalidateByTags");
1400
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1401
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1402
+ await this.deleteKeys(keys);
1403
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1404
+ }
1047
1405
  async invalidateByPattern(pattern) {
1048
- await this.startup;
1049
- const keys = await this.tagIndex.matchPattern(pattern);
1406
+ await this.awaitStartup("invalidateByPattern");
1407
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1408
+ await this.deleteKeys(keys);
1409
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1410
+ }
1411
+ async invalidateByPrefix(prefix) {
1412
+ await this.awaitStartup("invalidateByPrefix");
1413
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1414
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1050
1415
  await this.deleteKeys(keys);
1051
1416
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1052
1417
  }
@@ -1073,14 +1438,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
1073
1438
  getHitRate() {
1074
1439
  return this.metricsCollector.hitRate();
1075
1440
  }
1441
+ async healthCheck() {
1442
+ await this.startup;
1443
+ return Promise.all(
1444
+ this.layers.map(async (layer) => {
1445
+ const startedAt = performance.now();
1446
+ try {
1447
+ const healthy = layer.ping ? await layer.ping() : true;
1448
+ return {
1449
+ layer: layer.name,
1450
+ healthy,
1451
+ latencyMs: performance.now() - startedAt
1452
+ };
1453
+ } catch (error) {
1454
+ return {
1455
+ layer: layer.name,
1456
+ healthy: false,
1457
+ latencyMs: performance.now() - startedAt,
1458
+ error: this.formatError(error)
1459
+ };
1460
+ }
1461
+ })
1462
+ );
1463
+ }
1464
+ bumpGeneration(nextGeneration) {
1465
+ const current = this.currentGeneration ?? 0;
1466
+ this.currentGeneration = nextGeneration ?? current + 1;
1467
+ return this.currentGeneration;
1468
+ }
1076
1469
  /**
1077
1470
  * Returns detailed metadata about a single cache key: which layers contain it,
1078
1471
  * remaining fresh/stale/error TTLs, and associated tags.
1079
1472
  * Returns `null` if the key does not exist in any layer.
1080
1473
  */
1081
1474
  async inspect(key) {
1082
- const normalizedKey = this.validateCacheKey(key);
1083
- await this.startup;
1475
+ const userKey = this.validateCacheKey(key);
1476
+ const normalizedKey = this.qualifyKey(userKey);
1477
+ await this.awaitStartup("inspect");
1084
1478
  const foundInLayers = [];
1085
1479
  let freshTtlSeconds = null;
1086
1480
  let staleTtlSeconds = null;
@@ -1111,10 +1505,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1111
1505
  return null;
1112
1506
  }
1113
1507
  const tags = await this.getTagsForKey(normalizedKey);
1114
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1508
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1115
1509
  }
1116
1510
  async exportState() {
1117
- await this.startup;
1511
+ await this.awaitStartup("exportState");
1118
1512
  const exported = /* @__PURE__ */ new Map();
1119
1513
  for (const layer of this.layers) {
1120
1514
  if (!layer.keys) {
@@ -1122,15 +1516,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1122
1516
  }
1123
1517
  const keys = await layer.keys();
1124
1518
  for (const key of keys) {
1125
- if (exported.has(key)) {
1519
+ const exportedKey = this.stripQualifiedKey(key);
1520
+ if (exported.has(exportedKey)) {
1126
1521
  continue;
1127
1522
  }
1128
1523
  const stored = await this.readLayerEntry(layer, key);
1129
1524
  if (stored === null) {
1130
1525
  continue;
1131
1526
  }
1132
- exported.set(key, {
1133
- key,
1527
+ exported.set(exportedKey, {
1528
+ key: exportedKey,
1134
1529
  value: stored,
1135
1530
  ttl: remainingStoredTtlSeconds(stored)
1136
1531
  });
@@ -1139,20 +1534,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1139
1534
  return [...exported.values()];
1140
1535
  }
1141
1536
  async importState(entries) {
1142
- await this.startup;
1537
+ await this.awaitStartup("importState");
1143
1538
  await Promise.all(
1144
1539
  entries.map(async (entry) => {
1145
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1146
- await this.tagIndex.touch(entry.key);
1540
+ const qualifiedKey = this.qualifyKey(entry.key);
1541
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1542
+ await this.tagIndex.touch(qualifiedKey);
1147
1543
  })
1148
1544
  );
1149
1545
  }
1150
1546
  async persistToFile(filePath) {
1547
+ this.assertActive("persistToFile");
1151
1548
  const snapshot = await this.exportState();
1152
- await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1549
+ const { promises: fs2 } = await import("fs");
1550
+ await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1153
1551
  }
1154
1552
  async restoreFromFile(filePath) {
1155
- const raw = await import_node_fs.promises.readFile(filePath, "utf8");
1553
+ this.assertActive("restoreFromFile");
1554
+ const { promises: fs2 } = await import("fs");
1555
+ const raw = await fs2.readFile(filePath, "utf8");
1156
1556
  let parsed;
1157
1557
  try {
1158
1558
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1175,7 +1575,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1175
1575
  this.disconnectPromise = (async () => {
1176
1576
  await this.startup;
1177
1577
  await this.unsubscribeInvalidation?.();
1578
+ await this.flushWriteBehindQueue();
1178
1579
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1580
+ if (this.writeBehindTimer) {
1581
+ clearInterval(this.writeBehindTimer);
1582
+ this.writeBehindTimer = void 0;
1583
+ }
1584
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1179
1585
  })();
1180
1586
  }
1181
1587
  await this.disconnectPromise;
@@ -1235,7 +1641,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
1235
1641
  const fetchStart = Date.now();
1236
1642
  let fetched;
1237
1643
  try {
1238
- fetched = await fetcher();
1644
+ fetched = await this.fetchRateLimiter.schedule(
1645
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1646
+ { key, fetcher },
1647
+ fetcher
1648
+ );
1239
1649
  this.circuitBreakerManager.recordSuccess(key);
1240
1650
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1241
1651
  } catch (error) {
@@ -1269,6 +1679,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
1269
1679
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1270
1680
  }
1271
1681
  }
1682
+ async writeBatch(entries) {
1683
+ const now = Date.now();
1684
+ const entriesByLayer = /* @__PURE__ */ new Map();
1685
+ const immediateOperations = [];
1686
+ const deferredOperations = [];
1687
+ for (const entry of entries) {
1688
+ for (const layer of this.layers) {
1689
+ if (this.shouldSkipLayer(layer)) {
1690
+ continue;
1691
+ }
1692
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1693
+ const bucket = entriesByLayer.get(layer) ?? [];
1694
+ bucket.push(layerEntry);
1695
+ entriesByLayer.set(layer, bucket);
1696
+ }
1697
+ }
1698
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1699
+ const operation = async () => {
1700
+ try {
1701
+ if (layer.setMany) {
1702
+ await layer.setMany(layerEntries);
1703
+ return;
1704
+ }
1705
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1706
+ } catch (error) {
1707
+ await this.handleLayerFailure(layer, "write", error);
1708
+ }
1709
+ };
1710
+ if (this.shouldWriteBehind(layer)) {
1711
+ deferredOperations.push(operation);
1712
+ } else {
1713
+ immediateOperations.push(operation);
1714
+ }
1715
+ }
1716
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1717
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1718
+ for (const entry of entries) {
1719
+ if (entry.options?.tags) {
1720
+ await this.tagIndex.track(entry.key, entry.options.tags);
1721
+ } else {
1722
+ await this.tagIndex.touch(entry.key);
1723
+ }
1724
+ this.metricsCollector.increment("sets");
1725
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1726
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1727
+ }
1728
+ if (this.shouldBroadcastL1Invalidation()) {
1729
+ await this.publishInvalidation({
1730
+ scope: "keys",
1731
+ keys: entries.map((entry) => entry.key),
1732
+ sourceId: this.instanceId,
1733
+ operation: "write"
1734
+ });
1735
+ }
1736
+ }
1272
1737
  async readFromLayers(key, options, mode) {
1273
1738
  let sawRetainableValue = false;
1274
1739
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1352,33 +1817,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1352
1817
  }
1353
1818
  async writeAcrossLayers(key, kind, value, options) {
1354
1819
  const now = Date.now();
1355
- const operations = this.layers.map((layer) => async () => {
1356
- if (this.shouldSkipLayer(layer)) {
1357
- return;
1358
- }
1359
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1360
- const staleWhileRevalidate = this.resolveLayerSeconds(
1361
- layer.name,
1362
- options?.staleWhileRevalidate,
1363
- this.options.staleWhileRevalidate
1364
- );
1365
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1366
- const payload = createStoredValueEnvelope({
1367
- kind,
1368
- value,
1369
- freshTtlSeconds: freshTtl,
1370
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1371
- staleIfErrorSeconds: staleIfError,
1372
- now
1373
- });
1374
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1375
- try {
1376
- await layer.set(key, payload, ttl);
1377
- } catch (error) {
1378
- await this.handleLayerFailure(layer, "write", error);
1820
+ const immediateOperations = [];
1821
+ const deferredOperations = [];
1822
+ for (const layer of this.layers) {
1823
+ const operation = async () => {
1824
+ if (this.shouldSkipLayer(layer)) {
1825
+ return;
1826
+ }
1827
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1828
+ try {
1829
+ await layer.set(entry.key, entry.value, entry.ttl);
1830
+ } catch (error) {
1831
+ await this.handleLayerFailure(layer, "write", error);
1832
+ }
1833
+ };
1834
+ if (this.shouldWriteBehind(layer)) {
1835
+ deferredOperations.push(operation);
1836
+ } else {
1837
+ immediateOperations.push(operation);
1379
1838
  }
1380
- });
1381
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1839
+ }
1840
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1841
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1382
1842
  }
1383
1843
  async executeLayerOperations(operations, context) {
1384
1844
  if (this.options.writePolicy !== "best-effort") {
@@ -1402,8 +1862,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1402
1862
  );
1403
1863
  }
1404
1864
  }
1405
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1406
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1865
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1866
+ return this.ttlResolver.resolveFreshTtl(
1867
+ key,
1868
+ layerName,
1869
+ kind,
1870
+ options,
1871
+ fallbackTtl,
1872
+ this.options.negativeTtl,
1873
+ void 0,
1874
+ value
1875
+ );
1407
1876
  }
1408
1877
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1409
1878
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1432,7 +1901,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1432
1901
  return {
1433
1902
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1434
1903
  waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1435
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
1904
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1905
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1436
1906
  };
1437
1907
  }
1438
1908
  async deleteKeys(keys) {
@@ -1492,11 +1962,110 @@ var CacheStack = class extends import_node_events.EventEmitter {
1492
1962
  return String(error);
1493
1963
  }
1494
1964
  sleep(ms) {
1495
- return new Promise((resolve) => setTimeout(resolve, ms));
1965
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1496
1966
  }
1497
1967
  shouldBroadcastL1Invalidation() {
1498
1968
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1499
1969
  }
1970
+ initializeWriteBehind(options) {
1971
+ if (this.options.writeStrategy !== "write-behind") {
1972
+ return;
1973
+ }
1974
+ const flushIntervalMs = options?.flushIntervalMs;
1975
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1976
+ return;
1977
+ }
1978
+ this.writeBehindTimer = setInterval(() => {
1979
+ void this.flushWriteBehindQueue();
1980
+ }, flushIntervalMs);
1981
+ this.writeBehindTimer.unref?.();
1982
+ }
1983
+ shouldWriteBehind(layer) {
1984
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
1985
+ }
1986
+ async enqueueWriteBehind(operation) {
1987
+ this.writeBehindQueue.push(operation);
1988
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1989
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
1990
+ if (this.writeBehindQueue.length >= batchSize) {
1991
+ await this.flushWriteBehindQueue();
1992
+ return;
1993
+ }
1994
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1995
+ await this.flushWriteBehindQueue();
1996
+ }
1997
+ }
1998
+ async flushWriteBehindQueue() {
1999
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2000
+ await this.writeBehindFlushPromise;
2001
+ return;
2002
+ }
2003
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2004
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2005
+ this.writeBehindFlushPromise = (async () => {
2006
+ await Promise.allSettled(batch.map((operation) => operation()));
2007
+ })();
2008
+ await this.writeBehindFlushPromise;
2009
+ this.writeBehindFlushPromise = void 0;
2010
+ if (this.writeBehindQueue.length > 0) {
2011
+ await this.flushWriteBehindQueue();
2012
+ }
2013
+ }
2014
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2015
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2016
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2017
+ layer.name,
2018
+ options?.staleWhileRevalidate,
2019
+ this.options.staleWhileRevalidate
2020
+ );
2021
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2022
+ const payload = createStoredValueEnvelope({
2023
+ kind,
2024
+ value,
2025
+ freshTtlSeconds: freshTtl,
2026
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2027
+ staleIfErrorSeconds: staleIfError,
2028
+ now
2029
+ });
2030
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2031
+ return {
2032
+ key,
2033
+ value: payload,
2034
+ ttl
2035
+ };
2036
+ }
2037
+ intersectKeys(groups) {
2038
+ if (groups.length === 0) {
2039
+ return [];
2040
+ }
2041
+ const [firstGroup, ...rest] = groups;
2042
+ if (!firstGroup) {
2043
+ return [];
2044
+ }
2045
+ const restSets = rest.map((group) => new Set(group));
2046
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2047
+ }
2048
+ qualifyKey(key) {
2049
+ const prefix = this.generationPrefix();
2050
+ return prefix ? `${prefix}${key}` : key;
2051
+ }
2052
+ qualifyPattern(pattern) {
2053
+ const prefix = this.generationPrefix();
2054
+ return prefix ? `${prefix}${pattern}` : pattern;
2055
+ }
2056
+ stripQualifiedKey(key) {
2057
+ const prefix = this.generationPrefix();
2058
+ if (!prefix || !key.startsWith(prefix)) {
2059
+ return key;
2060
+ }
2061
+ return key.slice(prefix.length);
2062
+ }
2063
+ generationPrefix() {
2064
+ if (this.currentGeneration === void 0) {
2065
+ return "";
2066
+ }
2067
+ return `v${this.currentGeneration}:`;
2068
+ }
1500
2069
  async deleteKeysFromLayers(layers, keys) {
1501
2070
  await Promise.all(
1502
2071
  layers.map(async (layer) => {
@@ -1538,8 +2107,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1538
2107
  this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1539
2108
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1540
2109
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2110
+ this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2111
+ this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
1541
2112
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1542
2113
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2114
+ if (this.options.generation !== void 0) {
2115
+ this.validateNonNegativeNumber("generation", this.options.generation);
2116
+ }
1543
2117
  }
1544
2118
  validateWriteOptions(options) {
1545
2119
  if (!options) {
@@ -1551,8 +2125,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1551
2125
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1552
2126
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1553
2127
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2128
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1554
2129
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1555
2130
  this.validateCircuitBreakerOptions(options.circuitBreaker);
2131
+ this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
1556
2132
  }
1557
2133
  validateLayerNumberOption(name, value) {
1558
2134
  if (value === void 0) {
@@ -1577,6 +2153,20 @@ var CacheStack = class extends import_node_events.EventEmitter {
1577
2153
  throw new Error(`${name} must be a positive finite number.`);
1578
2154
  }
1579
2155
  }
2156
+ validateRateLimitOptions(name, options) {
2157
+ if (!options) {
2158
+ return;
2159
+ }
2160
+ this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2161
+ this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2162
+ this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2163
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2164
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2165
+ }
2166
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2167
+ throw new Error(`${name}.bucketKey must not be empty.`);
2168
+ }
2169
+ }
1580
2170
  validateNonNegativeNumber(name, value) {
1581
2171
  if (!Number.isFinite(value) || value < 0) {
1582
2172
  throw new Error(`${name} must be a non-negative finite number.`);
@@ -1594,6 +2184,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1594
2184
  }
1595
2185
  return key;
1596
2186
  }
2187
+ validateTtlPolicy(name, policy) {
2188
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2189
+ return;
2190
+ }
2191
+ if ("alignTo" in policy) {
2192
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2193
+ return;
2194
+ }
2195
+ throw new Error(`${name} is invalid.`);
2196
+ }
2197
+ assertActive(operation) {
2198
+ if (this.isDisconnecting) {
2199
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2200
+ }
2201
+ }
2202
+ async awaitStartup(operation) {
2203
+ this.assertActive(operation);
2204
+ await this.startup;
2205
+ this.assertActive(operation);
2206
+ }
1597
2207
  serializeOptions(options) {
1598
2208
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1599
2209
  }
@@ -1699,18 +2309,23 @@ var CacheStack = class extends import_node_events.EventEmitter {
1699
2309
  return value;
1700
2310
  }
1701
2311
  };
2312
+ function createInstanceId() {
2313
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2314
+ }
1702
2315
 
1703
2316
  // src/invalidation/RedisInvalidationBus.ts
1704
2317
  var RedisInvalidationBus = class {
1705
2318
  channel;
1706
2319
  publisher;
1707
2320
  subscriber;
2321
+ logger;
1708
2322
  handlers = /* @__PURE__ */ new Set();
1709
2323
  sharedListener;
1710
2324
  constructor(options) {
1711
2325
  this.publisher = options.publisher;
1712
2326
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1713
2327
  this.channel = options.channel ?? "layercache:invalidation";
2328
+ this.logger = options.logger;
1714
2329
  }
1715
2330
  async subscribe(handler) {
1716
2331
  if (this.handlers.size === 0) {
@@ -1767,6 +2382,10 @@ var RedisInvalidationBus = class {
1767
2382
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1768
2383
  }
1769
2384
  reportError(message, error) {
2385
+ if (this.logger?.error) {
2386
+ this.logger.error(message, { error });
2387
+ return;
2388
+ }
1770
2389
  console.error(`[layercache] ${message}`, error);
1771
2390
  }
1772
2391
  };
@@ -1776,19 +2395,21 @@ var RedisTagIndex = class {
1776
2395
  client;
1777
2396
  prefix;
1778
2397
  scanCount;
2398
+ knownKeysShards;
1779
2399
  constructor(options) {
1780
2400
  this.client = options.client;
1781
2401
  this.prefix = options.prefix ?? "layercache:tag-index";
1782
2402
  this.scanCount = options.scanCount ?? 100;
2403
+ this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
1783
2404
  }
1784
2405
  async touch(key) {
1785
- await this.client.sadd(this.knownKeysKey(), key);
2406
+ await this.client.sadd(this.knownKeysKeyFor(key), key);
1786
2407
  }
1787
2408
  async track(key, tags) {
1788
2409
  const keyTagsKey = this.keyTagsKey(key);
1789
2410
  const existingTags = await this.client.smembers(keyTagsKey);
1790
2411
  const pipeline = this.client.pipeline();
1791
- pipeline.sadd(this.knownKeysKey(), key);
2412
+ pipeline.sadd(this.knownKeysKeyFor(key), key);
1792
2413
  for (const tag of existingTags) {
1793
2414
  pipeline.srem(this.tagKeysKey(tag), key);
1794
2415
  }
@@ -1805,7 +2426,7 @@ var RedisTagIndex = class {
1805
2426
  const keyTagsKey = this.keyTagsKey(key);
1806
2427
  const existingTags = await this.client.smembers(keyTagsKey);
1807
2428
  const pipeline = this.client.pipeline();
1808
- pipeline.srem(this.knownKeysKey(), key);
2429
+ pipeline.srem(this.knownKeysKeyFor(key), key);
1809
2430
  pipeline.del(keyTagsKey);
1810
2431
  for (const tag of existingTags) {
1811
2432
  pipeline.srem(this.tagKeysKey(tag), key);
@@ -1815,24 +2436,38 @@ var RedisTagIndex = class {
1815
2436
  async keysForTag(tag) {
1816
2437
  return this.client.smembers(this.tagKeysKey(tag));
1817
2438
  }
2439
+ async keysForPrefix(prefix) {
2440
+ const matches = [];
2441
+ for (const knownKeysKey of this.knownKeysKeys()) {
2442
+ let cursor = "0";
2443
+ do {
2444
+ const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
2445
+ cursor = nextCursor;
2446
+ matches.push(...keys.filter((key) => key.startsWith(prefix)));
2447
+ } while (cursor !== "0");
2448
+ }
2449
+ return matches;
2450
+ }
1818
2451
  async tagsForKey(key) {
1819
2452
  return this.client.smembers(this.keyTagsKey(key));
1820
2453
  }
1821
2454
  async matchPattern(pattern) {
1822
2455
  const matches = [];
1823
- let cursor = "0";
1824
- do {
1825
- const [nextCursor, keys] = await this.client.sscan(
1826
- this.knownKeysKey(),
1827
- cursor,
1828
- "MATCH",
1829
- pattern,
1830
- "COUNT",
1831
- this.scanCount
1832
- );
1833
- cursor = nextCursor;
1834
- matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
1835
- } while (cursor !== "0");
2456
+ for (const knownKeysKey of this.knownKeysKeys()) {
2457
+ let cursor = "0";
2458
+ do {
2459
+ const [nextCursor, keys] = await this.client.sscan(
2460
+ knownKeysKey,
2461
+ cursor,
2462
+ "MATCH",
2463
+ pattern,
2464
+ "COUNT",
2465
+ this.scanCount
2466
+ );
2467
+ cursor = nextCursor;
2468
+ matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
2469
+ } while (cursor !== "0");
2470
+ }
1836
2471
  return matches;
1837
2472
  }
1838
2473
  async clear() {
@@ -1853,8 +2488,17 @@ var RedisTagIndex = class {
1853
2488
  } while (cursor !== "0");
1854
2489
  return matches;
1855
2490
  }
1856
- knownKeysKey() {
1857
- return `${this.prefix}:keys`;
2491
+ knownKeysKeyFor(key) {
2492
+ if (this.knownKeysShards === 1) {
2493
+ return `${this.prefix}:keys`;
2494
+ }
2495
+ return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
2496
+ }
2497
+ knownKeysKeys() {
2498
+ if (this.knownKeysShards === 1) {
2499
+ return [`${this.prefix}:keys`];
2500
+ }
2501
+ return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
1858
2502
  }
1859
2503
  keyTagsKey(key) {
1860
2504
  return `${this.prefix}:key:${encodeURIComponent(key)}`;
@@ -1863,6 +2507,22 @@ var RedisTagIndex = class {
1863
2507
  return `${this.prefix}:tag:${encodeURIComponent(tag)}`;
1864
2508
  }
1865
2509
  };
2510
+ function normalizeKnownKeysShards(value) {
2511
+ if (value === void 0) {
2512
+ return 1;
2513
+ }
2514
+ if (!Number.isInteger(value) || value <= 0) {
2515
+ throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
2516
+ }
2517
+ return value;
2518
+ }
2519
+ function simpleHash(value) {
2520
+ let hash = 0;
2521
+ for (let index = 0; index < value.length; index += 1) {
2522
+ hash = hash * 31 + value.charCodeAt(index) >>> 0;
2523
+ }
2524
+ return hash;
2525
+ }
1866
2526
 
1867
2527
  // src/http/createCacheStatsHandler.ts
1868
2528
  function createCacheStatsHandler(cache) {
@@ -1912,32 +2572,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1912
2572
  function createExpressCacheMiddleware(cache, options = {}) {
1913
2573
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
1914
2574
  return async (req, res, next) => {
1915
- const method = (req.method ?? "GET").toUpperCase();
1916
- if (!allowedMethods.has(method)) {
1917
- next();
1918
- return;
1919
- }
1920
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
1921
- const cached = await cache.get(key, void 0, options);
1922
- if (cached !== null) {
1923
- res.setHeader?.("content-type", "application/json; charset=utf-8");
1924
- res.setHeader?.("x-cache", "HIT");
1925
- if (res.json) {
1926
- res.json(cached);
1927
- } else {
1928
- res.end?.(JSON.stringify(cached));
2575
+ try {
2576
+ const method = (req.method ?? "GET").toUpperCase();
2577
+ if (!allowedMethods.has(method)) {
2578
+ next();
2579
+ return;
1929
2580
  }
1930
- return;
1931
- }
1932
- const originalJson = res.json?.bind(res);
1933
- if (originalJson) {
1934
- res.json = (body) => {
1935
- res.setHeader?.("x-cache", "MISS");
1936
- void cache.set(key, body, options);
1937
- return originalJson(body);
1938
- };
2581
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
2582
+ const cached = await cache.get(key, void 0, options);
2583
+ if (cached !== null) {
2584
+ res.setHeader?.("content-type", "application/json; charset=utf-8");
2585
+ res.setHeader?.("x-cache", "HIT");
2586
+ if (res.json) {
2587
+ res.json(cached);
2588
+ } else {
2589
+ res.end?.(JSON.stringify(cached));
2590
+ }
2591
+ return;
2592
+ }
2593
+ const originalJson = res.json?.bind(res);
2594
+ if (originalJson) {
2595
+ res.json = (body) => {
2596
+ res.setHeader?.("x-cache", "MISS");
2597
+ void cache.set(key, body, options);
2598
+ return originalJson(body);
2599
+ };
2600
+ }
2601
+ next();
2602
+ } catch (error) {
2603
+ next(error);
1939
2604
  }
1940
- next();
1941
2605
  };
1942
2606
  }
1943
2607
 
@@ -1950,6 +2614,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1950
2614
  return (...args) => wrapped(...args);
1951
2615
  }
1952
2616
 
2617
+ // src/integrations/hono.ts
2618
+ function createHonoCacheMiddleware(cache, options = {}) {
2619
+ const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
2620
+ return async (context, next) => {
2621
+ const method = (context.req.method ?? "GET").toUpperCase();
2622
+ if (!allowedMethods.has(method)) {
2623
+ await next();
2624
+ return;
2625
+ }
2626
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
2627
+ const cached = await cache.get(key, void 0, options);
2628
+ if (cached !== null) {
2629
+ context.header?.("x-cache", "HIT");
2630
+ context.header?.("content-type", "application/json; charset=utf-8");
2631
+ context.json(cached);
2632
+ return;
2633
+ }
2634
+ const originalJson = context.json.bind(context);
2635
+ context.json = (body, status) => {
2636
+ context.header?.("x-cache", "MISS");
2637
+ void cache.set(key, body, options);
2638
+ return originalJson(body, status);
2639
+ };
2640
+ await next();
2641
+ };
2642
+ }
2643
+
2644
+ // src/integrations/opentelemetry.ts
2645
+ function createOpenTelemetryPlugin(cache, tracer) {
2646
+ const originals = {
2647
+ get: cache.get.bind(cache),
2648
+ set: cache.set.bind(cache),
2649
+ delete: cache.delete.bind(cache),
2650
+ mget: cache.mget.bind(cache),
2651
+ mset: cache.mset.bind(cache),
2652
+ invalidateByTag: cache.invalidateByTag.bind(cache),
2653
+ invalidateByTags: cache.invalidateByTags.bind(cache),
2654
+ invalidateByPattern: cache.invalidateByPattern.bind(cache),
2655
+ invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
2656
+ };
2657
+ cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
2658
+ "layercache.key": String(args[0] ?? "")
2659
+ }));
2660
+ cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
2661
+ "layercache.key": String(args[0] ?? "")
2662
+ }));
2663
+ cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
2664
+ "layercache.key": String(args[0] ?? "")
2665
+ }));
2666
+ cache.mget = instrument("layercache.mget", tracer, originals.mget);
2667
+ cache.mset = instrument("layercache.mset", tracer, originals.mset);
2668
+ cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
2669
+ cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
2670
+ cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
2671
+ cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
2672
+ return {
2673
+ uninstall() {
2674
+ cache.get = originals.get;
2675
+ cache.set = originals.set;
2676
+ cache.delete = originals.delete;
2677
+ cache.mget = originals.mget;
2678
+ cache.mset = originals.mset;
2679
+ cache.invalidateByTag = originals.invalidateByTag;
2680
+ cache.invalidateByTags = originals.invalidateByTags;
2681
+ cache.invalidateByPattern = originals.invalidateByPattern;
2682
+ cache.invalidateByPrefix = originals.invalidateByPrefix;
2683
+ }
2684
+ };
2685
+ }
2686
+ function instrument(name, tracer, method, attributes) {
2687
+ return (async (...args) => {
2688
+ const span = tracer.startSpan(name, { attributes: attributes?.(args) });
2689
+ try {
2690
+ const result = await method(...args);
2691
+ span.setAttribute?.("layercache.success", true);
2692
+ if (result === null) {
2693
+ span.setAttribute?.("layercache.result", "null");
2694
+ }
2695
+ return result;
2696
+ } catch (error) {
2697
+ span.setAttribute?.("layercache.success", false);
2698
+ span.recordException?.(error);
2699
+ throw error;
2700
+ } finally {
2701
+ span.end();
2702
+ }
2703
+ });
2704
+ }
2705
+
1953
2706
  // src/integrations/trpc.ts
1954
2707
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1955
2708
  return async (context) => {
@@ -1982,12 +2735,21 @@ var MemoryLayer = class {
1982
2735
  isLocal = true;
1983
2736
  maxSize;
1984
2737
  evictionPolicy;
2738
+ onEvict;
1985
2739
  entries = /* @__PURE__ */ new Map();
2740
+ cleanupTimer;
1986
2741
  constructor(options = {}) {
1987
2742
  this.name = options.name ?? "memory";
1988
2743
  this.defaultTtl = options.ttl;
1989
2744
  this.maxSize = options.maxSize ?? 1e3;
1990
2745
  this.evictionPolicy = options.evictionPolicy ?? "lru";
2746
+ this.onEvict = options.onEvict;
2747
+ if (options.cleanupIntervalMs && options.cleanupIntervalMs > 0) {
2748
+ this.cleanupTimer = setInterval(() => {
2749
+ this.pruneExpired();
2750
+ }, options.cleanupIntervalMs);
2751
+ this.cleanupTimer.unref?.();
2752
+ }
1991
2753
  }
1992
2754
  async get(key) {
1993
2755
  const value = await this.getEntry(key);
@@ -2018,6 +2780,11 @@ var MemoryLayer = class {
2018
2780
  }
2019
2781
  return values;
2020
2782
  }
2783
+ async setMany(entries) {
2784
+ for (const entry of entries) {
2785
+ await this.set(entry.key, entry.value, entry.ttl);
2786
+ }
2787
+ }
2021
2788
  async set(key, value, ttl = this.defaultTtl) {
2022
2789
  this.entries.delete(key);
2023
2790
  this.entries.set(key, {
@@ -2070,6 +2837,15 @@ var MemoryLayer = class {
2070
2837
  async clear() {
2071
2838
  this.entries.clear();
2072
2839
  }
2840
+ async ping() {
2841
+ return true;
2842
+ }
2843
+ async dispose() {
2844
+ if (this.cleanupTimer) {
2845
+ clearInterval(this.cleanupTimer);
2846
+ this.cleanupTimer = void 0;
2847
+ }
2848
+ }
2073
2849
  async keys() {
2074
2850
  this.pruneExpired();
2075
2851
  return [...this.entries.keys()];
@@ -2102,7 +2878,11 @@ var MemoryLayer = class {
2102
2878
  if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
2103
2879
  const oldestKey = this.entries.keys().next().value;
2104
2880
  if (oldestKey !== void 0) {
2881
+ const entry = this.entries.get(oldestKey);
2105
2882
  this.entries.delete(oldestKey);
2883
+ if (entry) {
2884
+ this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
2885
+ }
2106
2886
  }
2107
2887
  return;
2108
2888
  }
@@ -2117,7 +2897,11 @@ var MemoryLayer = class {
2117
2897
  }
2118
2898
  }
2119
2899
  if (victimKey !== void 0) {
2900
+ const victim = this.entries.get(victimKey);
2120
2901
  this.entries.delete(victimKey);
2902
+ if (victim) {
2903
+ this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
2904
+ }
2121
2905
  }
2122
2906
  }
2123
2907
  pruneExpired() {
@@ -2137,15 +2921,35 @@ var import_node_util = require("util");
2137
2921
  var import_node_zlib = require("zlib");
2138
2922
 
2139
2923
  // src/serialization/JsonSerializer.ts
2924
+ var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2140
2925
  var JsonSerializer = class {
2141
2926
  serialize(value) {
2142
2927
  return JSON.stringify(value);
2143
2928
  }
2144
2929
  deserialize(payload) {
2145
2930
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2146
- return JSON.parse(normalized);
2931
+ return sanitizeJsonValue(JSON.parse(normalized));
2147
2932
  }
2148
2933
  };
2934
+ function sanitizeJsonValue(value) {
2935
+ if (Array.isArray(value)) {
2936
+ return value.map((entry) => sanitizeJsonValue(entry));
2937
+ }
2938
+ if (!isPlainObject(value)) {
2939
+ return value;
2940
+ }
2941
+ const sanitized = {};
2942
+ for (const [key, entry] of Object.entries(value)) {
2943
+ if (DANGEROUS_JSON_KEYS.has(key)) {
2944
+ continue;
2945
+ }
2946
+ sanitized[key] = sanitizeJsonValue(entry);
2947
+ }
2948
+ return sanitized;
2949
+ }
2950
+ function isPlainObject(value) {
2951
+ return Object.prototype.toString.call(value) === "[object Object]";
2952
+ }
2149
2953
 
2150
2954
  // src/layers/RedisLayer.ts
2151
2955
  var BATCH_DELETE_SIZE = 500;
@@ -2158,22 +2962,24 @@ var RedisLayer = class {
2158
2962
  defaultTtl;
2159
2963
  isLocal = false;
2160
2964
  client;
2161
- serializer;
2965
+ serializers;
2162
2966
  prefix;
2163
2967
  allowUnprefixedClear;
2164
2968
  scanCount;
2165
2969
  compression;
2166
2970
  compressionThreshold;
2971
+ disconnectOnDispose;
2167
2972
  constructor(options) {
2168
2973
  this.client = options.client;
2169
2974
  this.defaultTtl = options.ttl;
2170
2975
  this.name = options.name ?? "redis";
2171
- this.serializer = options.serializer ?? new JsonSerializer();
2976
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
2172
2977
  this.prefix = options.prefix ?? "";
2173
2978
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
2174
2979
  this.scanCount = options.scanCount ?? 100;
2175
2980
  this.compression = options.compression;
2176
2981
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2982
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
2177
2983
  }
2178
2984
  async get(key) {
2179
2985
  const payload = await this.getEntry(key);
@@ -2208,8 +3014,25 @@ var RedisLayer = class {
2208
3014
  })
2209
3015
  );
2210
3016
  }
3017
+ async setMany(entries) {
3018
+ if (entries.length === 0) {
3019
+ return;
3020
+ }
3021
+ const pipeline = this.client.pipeline();
3022
+ for (const entry of entries) {
3023
+ const serialized = this.primarySerializer().serialize(entry.value);
3024
+ const payload = await this.encodePayload(serialized);
3025
+ const normalizedKey = this.withPrefix(entry.key);
3026
+ if (entry.ttl && entry.ttl > 0) {
3027
+ pipeline.set(normalizedKey, payload, "EX", entry.ttl);
3028
+ } else {
3029
+ pipeline.set(normalizedKey, payload);
3030
+ }
3031
+ }
3032
+ await pipeline.exec();
3033
+ }
2211
3034
  async set(key, value, ttl = this.defaultTtl) {
2212
- const serialized = this.serializer.serialize(value);
3035
+ const serialized = this.primarySerializer().serialize(value);
2213
3036
  const payload = await this.encodePayload(serialized);
2214
3037
  const normalizedKey = this.withPrefix(key);
2215
3038
  if (ttl && ttl > 0) {
@@ -2242,6 +3065,18 @@ var RedisLayer = class {
2242
3065
  const keys = await this.keys();
2243
3066
  return keys.length;
2244
3067
  }
3068
+ async ping() {
3069
+ try {
3070
+ return await this.client.ping() === "PONG";
3071
+ } catch {
3072
+ return false;
3073
+ }
3074
+ }
3075
+ async dispose() {
3076
+ if (this.disconnectOnDispose) {
3077
+ this.client.disconnect();
3078
+ }
3079
+ }
2245
3080
  /**
2246
3081
  * Deletes all keys matching the layer's prefix in batches to avoid
2247
3082
  * loading millions of keys into memory at once.
@@ -2288,12 +3123,39 @@ var RedisLayer = class {
2288
3123
  return `${this.prefix}${key}`;
2289
3124
  }
2290
3125
  async deserializeOrDelete(key, payload) {
3126
+ const decodedPayload = await this.decodePayload(payload);
3127
+ for (const serializer of this.serializers) {
3128
+ try {
3129
+ const value = serializer.deserialize(decodedPayload);
3130
+ if (serializer !== this.primarySerializer()) {
3131
+ await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
3132
+ }
3133
+ return value;
3134
+ } catch {
3135
+ }
3136
+ }
2291
3137
  try {
2292
- return this.serializer.deserialize(await this.decodePayload(payload));
2293
- } catch {
2294
3138
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
2295
- return null;
3139
+ } catch {
2296
3140
  }
3141
+ return null;
3142
+ }
3143
+ async rewriteWithPrimarySerializer(key, value) {
3144
+ const serialized = this.primarySerializer().serialize(value);
3145
+ const payload = await this.encodePayload(serialized);
3146
+ const ttl = await this.client.ttl(this.withPrefix(key));
3147
+ if (ttl > 0) {
3148
+ await this.client.set(this.withPrefix(key), payload, "EX", ttl);
3149
+ return;
3150
+ }
3151
+ await this.client.set(this.withPrefix(key), payload);
3152
+ }
3153
+ primarySerializer() {
3154
+ const serializer = this.serializers[0];
3155
+ if (!serializer) {
3156
+ throw new Error("RedisLayer requires at least one serializer.");
3157
+ }
3158
+ return serializer;
2297
3159
  }
2298
3160
  isSerializablePayload(payload) {
2299
3161
  return typeof payload === "string" || Buffer.isBuffer(payload);
@@ -2332,8 +3194,8 @@ var RedisLayer = class {
2332
3194
  };
2333
3195
 
2334
3196
  // src/layers/DiskLayer.ts
2335
- var import_node_crypto2 = require("crypto");
2336
- var import_node_fs2 = require("fs");
3197
+ var import_node_crypto = require("crypto");
3198
+ var import_node_fs = require("fs");
2337
3199
  var import_node_path = require("path");
2338
3200
  var DiskLayer = class {
2339
3201
  name;
@@ -2342,12 +3204,13 @@ var DiskLayer = class {
2342
3204
  directory;
2343
3205
  serializer;
2344
3206
  maxFiles;
3207
+ writeQueue = Promise.resolve();
2345
3208
  constructor(options) {
2346
- this.directory = options.directory;
3209
+ this.directory = this.resolveDirectory(options.directory);
2347
3210
  this.defaultTtl = options.ttl;
2348
3211
  this.name = options.name ?? "disk";
2349
3212
  this.serializer = options.serializer ?? new JsonSerializer();
2350
- this.maxFiles = options.maxFiles;
3213
+ this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
2351
3214
  }
2352
3215
  async get(key) {
2353
3216
  return unwrapStoredValue(await this.getEntry(key));
@@ -2356,13 +3219,13 @@ var DiskLayer = class {
2356
3219
  const filePath = this.keyToPath(key);
2357
3220
  let raw;
2358
3221
  try {
2359
- raw = await import_node_fs2.promises.readFile(filePath);
3222
+ raw = await import_node_fs.promises.readFile(filePath);
2360
3223
  } catch {
2361
3224
  return null;
2362
3225
  }
2363
3226
  let entry;
2364
3227
  try {
2365
- entry = this.serializer.deserialize(raw);
3228
+ entry = this.deserializeEntry(raw);
2366
3229
  } catch {
2367
3230
  await this.safeDelete(filePath);
2368
3231
  return null;
@@ -2374,16 +3237,29 @@ var DiskLayer = class {
2374
3237
  return entry.value;
2375
3238
  }
2376
3239
  async set(key, value, ttl = this.defaultTtl) {
2377
- await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
2378
- const entry = {
2379
- key,
2380
- value,
2381
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2382
- };
2383
- const payload = this.serializer.serialize(entry);
2384
- await import_node_fs2.promises.writeFile(this.keyToPath(key), payload);
2385
- if (this.maxFiles !== void 0) {
2386
- await this.enforceMaxFiles();
3240
+ await this.enqueueWrite(async () => {
3241
+ await import_node_fs.promises.mkdir(this.directory, { recursive: true });
3242
+ const entry = {
3243
+ key,
3244
+ value,
3245
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
3246
+ };
3247
+ const payload = this.serializer.serialize(entry);
3248
+ const targetPath = this.keyToPath(key);
3249
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3250
+ await import_node_fs.promises.writeFile(tempPath, payload);
3251
+ await import_node_fs.promises.rename(tempPath, targetPath);
3252
+ if (this.maxFiles !== void 0) {
3253
+ await this.enforceMaxFiles();
3254
+ }
3255
+ });
3256
+ }
3257
+ async getMany(keys) {
3258
+ return Promise.all(keys.map((key) => this.getEntry(key)));
3259
+ }
3260
+ async setMany(entries) {
3261
+ for (const entry of entries) {
3262
+ await this.set(entry.key, entry.value, entry.ttl);
2387
3263
  }
2388
3264
  }
2389
3265
  async has(key) {
@@ -2394,14 +3270,15 @@ var DiskLayer = class {
2394
3270
  const filePath = this.keyToPath(key);
2395
3271
  let raw;
2396
3272
  try {
2397
- raw = await import_node_fs2.promises.readFile(filePath);
3273
+ raw = await import_node_fs.promises.readFile(filePath);
2398
3274
  } catch {
2399
3275
  return null;
2400
3276
  }
2401
3277
  let entry;
2402
3278
  try {
2403
- entry = this.serializer.deserialize(raw);
3279
+ entry = this.deserializeEntry(raw);
2404
3280
  } catch {
3281
+ await this.safeDelete(filePath);
2405
3282
  return null;
2406
3283
  }
2407
3284
  if (entry.expiresAt === null) {
@@ -2414,21 +3291,25 @@ var DiskLayer = class {
2414
3291
  return remaining;
2415
3292
  }
2416
3293
  async delete(key) {
2417
- await this.safeDelete(this.keyToPath(key));
3294
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2418
3295
  }
2419
3296
  async deleteMany(keys) {
2420
- await Promise.all(keys.map((key) => this.delete(key)));
3297
+ await this.enqueueWrite(async () => {
3298
+ await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
3299
+ });
2421
3300
  }
2422
3301
  async clear() {
2423
- let entries;
2424
- try {
2425
- entries = await import_node_fs2.promises.readdir(this.directory);
2426
- } catch {
2427
- return;
2428
- }
2429
- await Promise.all(
2430
- entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
2431
- );
3302
+ await this.enqueueWrite(async () => {
3303
+ let entries;
3304
+ try {
3305
+ entries = await import_node_fs.promises.readdir(this.directory);
3306
+ } catch {
3307
+ return;
3308
+ }
3309
+ await Promise.all(
3310
+ entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
3311
+ );
3312
+ });
2432
3313
  }
2433
3314
  /**
2434
3315
  * Returns the original cache key strings stored on disk.
@@ -2437,7 +3318,7 @@ var DiskLayer = class {
2437
3318
  async keys() {
2438
3319
  let entries;
2439
3320
  try {
2440
- entries = await import_node_fs2.promises.readdir(this.directory);
3321
+ entries = await import_node_fs.promises.readdir(this.directory);
2441
3322
  } catch {
2442
3323
  return [];
2443
3324
  }
@@ -2448,13 +3329,13 @@ var DiskLayer = class {
2448
3329
  const filePath = (0, import_node_path.join)(this.directory, name);
2449
3330
  let raw;
2450
3331
  try {
2451
- raw = await import_node_fs2.promises.readFile(filePath);
3332
+ raw = await import_node_fs.promises.readFile(filePath);
2452
3333
  } catch {
2453
3334
  return;
2454
3335
  }
2455
3336
  let entry;
2456
3337
  try {
2457
- entry = this.serializer.deserialize(raw);
3338
+ entry = this.deserializeEntry(raw);
2458
3339
  } catch {
2459
3340
  await this.safeDelete(filePath);
2460
3341
  return;
@@ -2472,16 +3353,56 @@ var DiskLayer = class {
2472
3353
  const keys = await this.keys();
2473
3354
  return keys.length;
2474
3355
  }
3356
+ async ping() {
3357
+ try {
3358
+ await import_node_fs.promises.mkdir(this.directory, { recursive: true });
3359
+ return true;
3360
+ } catch {
3361
+ return false;
3362
+ }
3363
+ }
3364
+ async dispose() {
3365
+ }
2475
3366
  keyToPath(key) {
2476
- const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
3367
+ const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
2477
3368
  return (0, import_node_path.join)(this.directory, `${hash}.lc`);
2478
3369
  }
3370
+ resolveDirectory(directory) {
3371
+ if (typeof directory !== "string" || directory.trim().length === 0) {
3372
+ throw new Error("DiskLayer.directory must be a non-empty path.");
3373
+ }
3374
+ if (directory.includes("\0")) {
3375
+ throw new Error("DiskLayer.directory must not contain null bytes.");
3376
+ }
3377
+ return (0, import_node_path.resolve)(directory);
3378
+ }
3379
+ normalizeMaxFiles(maxFiles) {
3380
+ if (maxFiles === void 0) {
3381
+ return void 0;
3382
+ }
3383
+ if (!Number.isInteger(maxFiles) || maxFiles <= 0) {
3384
+ throw new Error("DiskLayer.maxFiles must be a positive integer.");
3385
+ }
3386
+ return maxFiles;
3387
+ }
3388
+ deserializeEntry(raw) {
3389
+ const entry = this.serializer.deserialize(raw);
3390
+ if (!isDiskEntry(entry)) {
3391
+ throw new Error("Invalid disk cache entry.");
3392
+ }
3393
+ return entry;
3394
+ }
2479
3395
  async safeDelete(filePath) {
2480
3396
  try {
2481
- await import_node_fs2.promises.unlink(filePath);
3397
+ await import_node_fs.promises.unlink(filePath);
2482
3398
  } catch {
2483
3399
  }
2484
3400
  }
3401
+ enqueueWrite(operation) {
3402
+ const next = this.writeQueue.then(operation, operation);
3403
+ this.writeQueue = next.catch(() => void 0);
3404
+ return next;
3405
+ }
2485
3406
  /**
2486
3407
  * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
2487
3408
  */
@@ -2491,7 +3412,7 @@ var DiskLayer = class {
2491
3412
  }
2492
3413
  let entries;
2493
3414
  try {
2494
- entries = await import_node_fs2.promises.readdir(this.directory);
3415
+ entries = await import_node_fs.promises.readdir(this.directory);
2495
3416
  } catch {
2496
3417
  return;
2497
3418
  }
@@ -2503,7 +3424,7 @@ var DiskLayer = class {
2503
3424
  lcFiles.map(async (name) => {
2504
3425
  const filePath = (0, import_node_path.join)(this.directory, name);
2505
3426
  try {
2506
- const stat = await import_node_fs2.promises.stat(filePath);
3427
+ const stat = await import_node_fs.promises.stat(filePath);
2507
3428
  return { filePath, mtimeMs: stat.mtimeMs };
2508
3429
  } catch {
2509
3430
  return { filePath, mtimeMs: 0 };
@@ -2515,6 +3436,14 @@ var DiskLayer = class {
2515
3436
  await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
2516
3437
  }
2517
3438
  };
3439
+ function isDiskEntry(value) {
3440
+ if (!value || typeof value !== "object") {
3441
+ return false;
3442
+ }
3443
+ const candidate = value;
3444
+ const validExpiry = candidate.expiresAt === null || typeof candidate.expiresAt === "number";
3445
+ return typeof candidate.key === "string" && validExpiry && "value" in candidate;
3446
+ }
2518
3447
 
2519
3448
  // src/layers/MemcachedLayer.ts
2520
3449
  var MemcachedLayer = class {
@@ -2587,13 +3516,19 @@ var MsgpackSerializer = class {
2587
3516
  };
2588
3517
 
2589
3518
  // src/singleflight/RedisSingleFlightCoordinator.ts
2590
- var import_node_crypto3 = require("crypto");
3519
+ var import_node_crypto2 = require("crypto");
2591
3520
  var RELEASE_SCRIPT = `
2592
3521
  if redis.call("get", KEYS[1]) == ARGV[1] then
2593
3522
  return redis.call("del", KEYS[1])
2594
3523
  end
2595
3524
  return 0
2596
3525
  `;
3526
+ var RENEW_SCRIPT = `
3527
+ if redis.call("get", KEYS[1]) == ARGV[1] then
3528
+ return redis.call("pexpire", KEYS[1], ARGV[2])
3529
+ end
3530
+ return 0
3531
+ `;
2597
3532
  var RedisSingleFlightCoordinator = class {
2598
3533
  client;
2599
3534
  prefix;
@@ -2603,17 +3538,32 @@ var RedisSingleFlightCoordinator = class {
2603
3538
  }
2604
3539
  async execute(key, options, worker, waiter) {
2605
3540
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2606
- const token = (0, import_node_crypto3.randomUUID)();
3541
+ const token = (0, import_node_crypto2.randomUUID)();
2607
3542
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2608
3543
  if (acquired === "OK") {
3544
+ const renewTimer = this.startLeaseRenewal(lockKey, token, options);
2609
3545
  try {
2610
3546
  return await worker();
2611
3547
  } finally {
3548
+ if (renewTimer) {
3549
+ clearInterval(renewTimer);
3550
+ }
2612
3551
  await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
2613
3552
  }
2614
3553
  }
2615
3554
  return waiter();
2616
3555
  }
3556
+ startLeaseRenewal(lockKey, token, options) {
3557
+ const renewIntervalMs = options.renewIntervalMs ?? Math.max(100, Math.floor(options.leaseMs / 2));
3558
+ if (renewIntervalMs <= 0 || renewIntervalMs >= options.leaseMs) {
3559
+ return void 0;
3560
+ }
3561
+ const timer = setInterval(() => {
3562
+ void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
3563
+ }, renewIntervalMs);
3564
+ timer.unref?.();
3565
+ return timer;
3566
+ }
2617
3567
  };
2618
3568
 
2619
3569
  // src/metrics/PrometheusExporter.ts
@@ -2716,6 +3666,8 @@ function sanitizeLabel(value) {
2716
3666
  createCachedMethodDecorator,
2717
3667
  createExpressCacheMiddleware,
2718
3668
  createFastifyLayercachePlugin,
3669
+ createHonoCacheMiddleware,
3670
+ createOpenTelemetryPlugin,
2719
3671
  createPrometheusMetricsExporter,
2720
3672
  createTrpcCacheMiddleware
2721
3673
  });