layercache 1.2.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,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,95 @@ var CircuitBreakerManager = class {
243
398
  }
244
399
  };
245
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
+
246
490
  // src/internal/MetricsCollector.ts
247
491
  var MetricsCollector = class {
248
492
  data = this.empty();
@@ -439,13 +683,14 @@ var TtlResolver = class {
439
683
  clearProfiles() {
440
684
  this.accessProfiles.clear();
441
685
  }
442
- 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;
443
688
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
444
689
  layerName,
445
690
  options?.negativeTtl,
446
691
  globalNegativeTtl,
447
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
448
- ) : 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);
449
694
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
450
695
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
451
696
  return this.applyJitter(adaptiveTtl, jitter);
@@ -484,6 +729,29 @@ var TtlResolver = class {
484
729
  const delta = (Math.random() * 2 - 1) * jitter;
485
730
  return Math.max(1, Math.round(ttl + delta));
486
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
+ }
487
755
  readLayerNumber(layerName, value) {
488
756
  if (typeof value === "number") {
489
757
  return value;
@@ -511,36 +779,46 @@ var PatternMatcher = class _PatternMatcher {
511
779
  /**
512
780
  * Tests whether a glob-style pattern matches a value.
513
781
  * Supports `*` (any sequence of characters) and `?` (any single character).
514
- * 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.
515
784
  */
516
785
  static matches(pattern, value) {
517
786
  return _PatternMatcher.matchLinear(pattern, value);
518
787
  }
519
788
  /**
520
- * Linear-time glob matching using dynamic programming.
521
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
789
+ * Linear-time glob matching with O(1) extra memory.
522
790
  */
523
791
  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
- }
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;
541
815
  }
816
+ return false;
817
+ }
818
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
819
+ patternIndex += 1;
542
820
  }
543
- return dp[m]?.[n];
821
+ return patternIndex === pattern.length;
544
822
  }
545
823
  };
546
824
 
@@ -578,26 +856,14 @@ var TagIndex = class {
578
856
  }
579
857
  }
580
858
  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);
859
+ this.removeKey(key);
597
860
  }
598
861
  async keysForTag(tag) {
599
862
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
600
863
  }
864
+ async keysForPrefix(prefix) {
865
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
866
+ }
601
867
  async tagsForKey(key) {
602
868
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
603
869
  }
@@ -619,15 +885,32 @@ var TagIndex = class {
619
885
  if (removed >= toRemove) {
620
886
  break;
621
887
  }
622
- this.knownKeys.delete(key);
623
- this.keyToTags.delete(key);
888
+ this.removeKey(key);
624
889
  removed += 1;
625
890
  }
626
891
  }
892
+ removeKey(key) {
893
+ this.knownKeys.delete(key);
894
+ const tags = this.keyToTags.get(key);
895
+ if (!tags) {
896
+ return;
897
+ }
898
+ for (const tag of tags) {
899
+ const keys = this.tagToKeys.get(tag);
900
+ if (!keys) {
901
+ continue;
902
+ }
903
+ keys.delete(key);
904
+ if (keys.size === 0) {
905
+ this.tagToKeys.delete(tag);
906
+ }
907
+ }
908
+ this.keyToTags.delete(key);
909
+ }
627
910
  };
628
911
 
629
912
  // src/stampede/StampedeGuard.ts
630
- var import_async_mutex = require("async-mutex");
913
+ var import_async_mutex2 = require("async-mutex");
631
914
  var StampedeGuard = class {
632
915
  mutexes = /* @__PURE__ */ new Map();
633
916
  async execute(key, task) {
@@ -644,7 +927,7 @@ var StampedeGuard = class {
644
927
  getMutexEntry(key) {
645
928
  let entry = this.mutexes.get(key);
646
929
  if (!entry) {
647
- entry = { mutex: new import_async_mutex.Mutex(), references: 0 };
930
+ entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
648
931
  this.mutexes.set(key, entry);
649
932
  }
650
933
  entry.references += 1;
@@ -705,6 +988,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
705
988
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
706
989
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
707
990
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
991
+ this.currentGeneration = options.generation;
708
992
  if (options.publishSetInvalidation !== void 0) {
709
993
  console.warn(
710
994
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -713,21 +997,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
713
997
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
714
998
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
715
999
  this.tagIndex = options.tagIndex ?? new TagIndex();
1000
+ this.initializeWriteBehind(options.writeBehind);
716
1001
  this.startup = this.initialize();
717
1002
  }
718
1003
  layers;
719
1004
  options;
720
1005
  stampedeGuard = new StampedeGuard();
721
1006
  metricsCollector = new MetricsCollector();
722
- instanceId = (0, import_node_crypto.randomUUID)();
1007
+ instanceId = createInstanceId();
723
1008
  startup;
724
1009
  unsubscribeInvalidation;
725
1010
  logger;
726
1011
  tagIndex;
1012
+ fetchRateLimiter = new FetchRateLimiter();
727
1013
  backgroundRefreshes = /* @__PURE__ */ new Map();
728
1014
  layerDegradedUntil = /* @__PURE__ */ new Map();
729
1015
  ttlResolver;
730
1016
  circuitBreakerManager;
1017
+ currentGeneration;
1018
+ writeBehindQueue = [];
1019
+ writeBehindTimer;
1020
+ writeBehindFlushPromise;
731
1021
  isDisconnecting = false;
732
1022
  disconnectPromise;
733
1023
  /**
@@ -737,9 +1027,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
737
1027
  * and no `fetcher` is provided.
738
1028
  */
739
1029
  async get(key, fetcher, options) {
740
- const normalizedKey = this.validateCacheKey(key);
1030
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
741
1031
  this.validateWriteOptions(options);
742
- await this.startup;
1032
+ await this.awaitStartup("get");
743
1033
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
744
1034
  if (hit.found) {
745
1035
  this.ttlResolver.recordAccess(normalizedKey);
@@ -804,8 +1094,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
804
1094
  * Returns true if the given key exists and is not expired in any layer.
805
1095
  */
806
1096
  async has(key) {
807
- const normalizedKey = this.validateCacheKey(key);
808
- await this.startup;
1097
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1098
+ await this.awaitStartup("has");
809
1099
  for (const layer of this.layers) {
810
1100
  if (this.shouldSkipLayer(layer)) {
811
1101
  continue;
@@ -835,8 +1125,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
835
1125
  * that has it, or null if the key is not found / has no TTL.
836
1126
  */
837
1127
  async ttl(key) {
838
- const normalizedKey = this.validateCacheKey(key);
839
- await this.startup;
1128
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1129
+ await this.awaitStartup("ttl");
840
1130
  for (const layer of this.layers) {
841
1131
  if (this.shouldSkipLayer(layer)) {
842
1132
  continue;
@@ -857,17 +1147,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
857
1147
  * Stores a value in all cache layers. Overwrites any existing value.
858
1148
  */
859
1149
  async set(key, value, options) {
860
- const normalizedKey = this.validateCacheKey(key);
1150
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
861
1151
  this.validateWriteOptions(options);
862
- await this.startup;
1152
+ await this.awaitStartup("set");
863
1153
  await this.storeEntry(normalizedKey, "value", value, options);
864
1154
  }
865
1155
  /**
866
1156
  * Deletes the key from all layers and publishes an invalidation message.
867
1157
  */
868
1158
  async delete(key) {
869
- const normalizedKey = this.validateCacheKey(key);
870
- await this.startup;
1159
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1160
+ await this.awaitStartup("delete");
871
1161
  await this.deleteKeys([normalizedKey]);
872
1162
  await this.publishInvalidation({
873
1163
  scope: "key",
@@ -877,7 +1167,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
877
1167
  });
878
1168
  }
879
1169
  async clear() {
880
- await this.startup;
1170
+ await this.awaitStartup("clear");
881
1171
  await Promise.all(this.layers.map((layer) => layer.clear()));
882
1172
  await this.tagIndex.clear();
883
1173
  this.ttlResolver.clearProfiles();
@@ -893,23 +1183,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
893
1183
  if (keys.length === 0) {
894
1184
  return;
895
1185
  }
896
- await this.startup;
1186
+ await this.awaitStartup("mdelete");
897
1187
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
898
- await this.deleteKeys(normalizedKeys);
1188
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1189
+ await this.deleteKeys(cacheKeys);
899
1190
  await this.publishInvalidation({
900
1191
  scope: "keys",
901
- keys: normalizedKeys,
1192
+ keys: cacheKeys,
902
1193
  sourceId: this.instanceId,
903
1194
  operation: "delete"
904
1195
  });
905
1196
  }
906
1197
  async mget(entries) {
1198
+ this.assertActive("mget");
907
1199
  if (entries.length === 0) {
908
1200
  return [];
909
1201
  }
910
1202
  const normalizedEntries = entries.map((entry) => ({
911
1203
  ...entry,
912
- key: this.validateCacheKey(entry.key)
1204
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
913
1205
  }));
914
1206
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
915
1207
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -935,7 +1227,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
935
1227
  })
936
1228
  );
937
1229
  }
938
- await this.startup;
1230
+ await this.awaitStartup("mget");
939
1231
  const pending = /* @__PURE__ */ new Set();
940
1232
  const indexesByKey = /* @__PURE__ */ new Map();
941
1233
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -983,14 +1275,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
983
1275
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
984
1276
  }
985
1277
  async mset(entries) {
1278
+ this.assertActive("mset");
986
1279
  const normalizedEntries = entries.map((entry) => ({
987
1280
  ...entry,
988
- key: this.validateCacheKey(entry.key)
1281
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
989
1282
  }));
990
1283
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
991
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1284
+ await this.awaitStartup("mset");
1285
+ await this.writeBatch(normalizedEntries);
992
1286
  }
993
1287
  async warm(entries, options = {}) {
1288
+ this.assertActive("warm");
994
1289
  const concurrency = Math.max(1, options.concurrency ?? 4);
995
1290
  const total = entries.length;
996
1291
  let completed = 0;
@@ -1039,14 +1334,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
1039
1334
  return new CacheNamespace(this, prefix);
1040
1335
  }
1041
1336
  async invalidateByTag(tag) {
1042
- await this.startup;
1337
+ await this.awaitStartup("invalidateByTag");
1043
1338
  const keys = await this.tagIndex.keysForTag(tag);
1044
1339
  await this.deleteKeys(keys);
1045
1340
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1046
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
+ }
1047
1352
  async invalidateByPattern(pattern) {
1048
- await this.startup;
1049
- 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}*`);
1050
1362
  await this.deleteKeys(keys);
1051
1363
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1052
1364
  }
@@ -1073,14 +1385,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
1073
1385
  getHitRate() {
1074
1386
  return this.metricsCollector.hitRate();
1075
1387
  }
1388
+ async healthCheck() {
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
+ }
1076
1416
  /**
1077
1417
  * Returns detailed metadata about a single cache key: which layers contain it,
1078
1418
  * remaining fresh/stale/error TTLs, and associated tags.
1079
1419
  * Returns `null` if the key does not exist in any layer.
1080
1420
  */
1081
1421
  async inspect(key) {
1082
- const normalizedKey = this.validateCacheKey(key);
1083
- await this.startup;
1422
+ const userKey = this.validateCacheKey(key);
1423
+ const normalizedKey = this.qualifyKey(userKey);
1424
+ await this.awaitStartup("inspect");
1084
1425
  const foundInLayers = [];
1085
1426
  let freshTtlSeconds = null;
1086
1427
  let staleTtlSeconds = null;
@@ -1111,10 +1452,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1111
1452
  return null;
1112
1453
  }
1113
1454
  const tags = await this.getTagsForKey(normalizedKey);
1114
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1455
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1115
1456
  }
1116
1457
  async exportState() {
1117
- await this.startup;
1458
+ await this.awaitStartup("exportState");
1118
1459
  const exported = /* @__PURE__ */ new Map();
1119
1460
  for (const layer of this.layers) {
1120
1461
  if (!layer.keys) {
@@ -1122,15 +1463,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1122
1463
  }
1123
1464
  const keys = await layer.keys();
1124
1465
  for (const key of keys) {
1125
- if (exported.has(key)) {
1466
+ const exportedKey = this.stripQualifiedKey(key);
1467
+ if (exported.has(exportedKey)) {
1126
1468
  continue;
1127
1469
  }
1128
1470
  const stored = await this.readLayerEntry(layer, key);
1129
1471
  if (stored === null) {
1130
1472
  continue;
1131
1473
  }
1132
- exported.set(key, {
1133
- key,
1474
+ exported.set(exportedKey, {
1475
+ key: exportedKey,
1134
1476
  value: stored,
1135
1477
  ttl: remainingStoredTtlSeconds(stored)
1136
1478
  });
@@ -1139,20 +1481,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1139
1481
  return [...exported.values()];
1140
1482
  }
1141
1483
  async importState(entries) {
1142
- await this.startup;
1484
+ await this.awaitStartup("importState");
1143
1485
  await Promise.all(
1144
1486
  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);
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);
1147
1490
  })
1148
1491
  );
1149
1492
  }
1150
1493
  async persistToFile(filePath) {
1494
+ this.assertActive("persistToFile");
1151
1495
  const snapshot = await this.exportState();
1152
- 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");
1153
1498
  }
1154
1499
  async restoreFromFile(filePath) {
1155
- 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");
1156
1503
  let parsed;
1157
1504
  try {
1158
1505
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1175,7 +1522,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1175
1522
  this.disconnectPromise = (async () => {
1176
1523
  await this.startup;
1177
1524
  await this.unsubscribeInvalidation?.();
1525
+ await this.flushWriteBehindQueue();
1178
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()));
1179
1532
  })();
1180
1533
  }
1181
1534
  await this.disconnectPromise;
@@ -1235,7 +1588,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1235
1588
  const fetchStart = Date.now();
1236
1589
  let fetched;
1237
1590
  try {
1238
- fetched = await fetcher();
1591
+ fetched = await this.fetchRateLimiter.schedule(
1592
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1593
+ fetcher
1594
+ );
1239
1595
  this.circuitBreakerManager.recordSuccess(key);
1240
1596
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1241
1597
  } catch (error) {
@@ -1269,6 +1625,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
1269
1625
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1270
1626
  }
1271
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
+ }
1272
1683
  async readFromLayers(key, options, mode) {
1273
1684
  let sawRetainableValue = false;
1274
1685
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1352,33 +1763,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1352
1763
  }
1353
1764
  async writeAcrossLayers(key, kind, value, options) {
1354
1765
  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);
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);
1379
1784
  }
1380
- });
1381
- 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)));
1382
1788
  }
1383
1789
  async executeLayerOperations(operations, context) {
1384
1790
  if (this.options.writePolicy !== "best-effort") {
@@ -1402,8 +1808,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1402
1808
  );
1403
1809
  }
1404
1810
  }
1405
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1406
- 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
+ );
1407
1822
  }
1408
1823
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1409
1824
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1497,6 +1912,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
1497
1912
  shouldBroadcastL1Invalidation() {
1498
1913
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1499
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
+ }
1500
2014
  async deleteKeysFromLayers(layers, keys) {
1501
2015
  await Promise.all(
1502
2016
  layers.map(async (layer) => {
@@ -1540,6 +2054,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1540
2054
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1541
2055
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1542
2056
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2057
+ if (this.options.generation !== void 0) {
2058
+ this.validateNonNegativeNumber("generation", this.options.generation);
2059
+ }
1543
2060
  }
1544
2061
  validateWriteOptions(options) {
1545
2062
  if (!options) {
@@ -1551,6 +2068,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1551
2068
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1552
2069
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1553
2070
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2071
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1554
2072
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1555
2073
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1556
2074
  }
@@ -1594,6 +2112,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1594
2112
  }
1595
2113
  return key;
1596
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
+ }
1597
2135
  serializeOptions(options) {
1598
2136
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1599
2137
  }
@@ -1699,18 +2237,23 @@ var CacheStack = class extends import_node_events.EventEmitter {
1699
2237
  return value;
1700
2238
  }
1701
2239
  };
2240
+ function createInstanceId() {
2241
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2242
+ }
1702
2243
 
1703
2244
  // src/invalidation/RedisInvalidationBus.ts
1704
2245
  var RedisInvalidationBus = class {
1705
2246
  channel;
1706
2247
  publisher;
1707
2248
  subscriber;
2249
+ logger;
1708
2250
  handlers = /* @__PURE__ */ new Set();
1709
2251
  sharedListener;
1710
2252
  constructor(options) {
1711
2253
  this.publisher = options.publisher;
1712
2254
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1713
2255
  this.channel = options.channel ?? "layercache:invalidation";
2256
+ this.logger = options.logger;
1714
2257
  }
1715
2258
  async subscribe(handler) {
1716
2259
  if (this.handlers.size === 0) {
@@ -1767,6 +2310,10 @@ var RedisInvalidationBus = class {
1767
2310
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1768
2311
  }
1769
2312
  reportError(message, error) {
2313
+ if (this.logger?.error) {
2314
+ this.logger.error(message, { error });
2315
+ return;
2316
+ }
1770
2317
  console.error(`[layercache] ${message}`, error);
1771
2318
  }
1772
2319
  };
@@ -1815,6 +2362,16 @@ var RedisTagIndex = class {
1815
2362
  async keysForTag(tag) {
1816
2363
  return this.client.smembers(this.tagKeysKey(tag));
1817
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
+ }
1818
2375
  async tagsForKey(key) {
1819
2376
  return this.client.smembers(this.keyTagsKey(key));
1820
2377
  }
@@ -1912,32 +2469,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1912
2469
  function createExpressCacheMiddleware(cache, options = {}) {
1913
2470
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
1914
2471
  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));
2472
+ try {
2473
+ const method = (req.method ?? "GET").toUpperCase();
2474
+ if (!allowedMethods.has(method)) {
2475
+ next();
2476
+ return;
1929
2477
  }
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
- };
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);
1939
2501
  }
1940
- next();
1941
2502
  };
1942
2503
  }
1943
2504
 
@@ -1950,6 +2511,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1950
2511
  return (...args) => wrapped(...args);
1951
2512
  }
1952
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
+
1953
2603
  // src/integrations/trpc.ts
1954
2604
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1955
2605
  return async (context) => {
@@ -1982,12 +2632,21 @@ var MemoryLayer = class {
1982
2632
  isLocal = true;
1983
2633
  maxSize;
1984
2634
  evictionPolicy;
2635
+ onEvict;
1985
2636
  entries = /* @__PURE__ */ new Map();
2637
+ cleanupTimer;
1986
2638
  constructor(options = {}) {
1987
2639
  this.name = options.name ?? "memory";
1988
2640
  this.defaultTtl = options.ttl;
1989
2641
  this.maxSize = options.maxSize ?? 1e3;
1990
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
+ }
1991
2650
  }
1992
2651
  async get(key) {
1993
2652
  const value = await this.getEntry(key);
@@ -2018,6 +2677,11 @@ var MemoryLayer = class {
2018
2677
  }
2019
2678
  return values;
2020
2679
  }
2680
+ async setMany(entries) {
2681
+ for (const entry of entries) {
2682
+ await this.set(entry.key, entry.value, entry.ttl);
2683
+ }
2684
+ }
2021
2685
  async set(key, value, ttl = this.defaultTtl) {
2022
2686
  this.entries.delete(key);
2023
2687
  this.entries.set(key, {
@@ -2070,6 +2734,15 @@ var MemoryLayer = class {
2070
2734
  async clear() {
2071
2735
  this.entries.clear();
2072
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
+ }
2073
2746
  async keys() {
2074
2747
  this.pruneExpired();
2075
2748
  return [...this.entries.keys()];
@@ -2102,7 +2775,11 @@ var MemoryLayer = class {
2102
2775
  if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
2103
2776
  const oldestKey = this.entries.keys().next().value;
2104
2777
  if (oldestKey !== void 0) {
2778
+ const entry = this.entries.get(oldestKey);
2105
2779
  this.entries.delete(oldestKey);
2780
+ if (entry) {
2781
+ this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
2782
+ }
2106
2783
  }
2107
2784
  return;
2108
2785
  }
@@ -2117,7 +2794,11 @@ var MemoryLayer = class {
2117
2794
  }
2118
2795
  }
2119
2796
  if (victimKey !== void 0) {
2797
+ const victim = this.entries.get(victimKey);
2120
2798
  this.entries.delete(victimKey);
2799
+ if (victim) {
2800
+ this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
2801
+ }
2121
2802
  }
2122
2803
  }
2123
2804
  pruneExpired() {
@@ -2158,22 +2839,24 @@ var RedisLayer = class {
2158
2839
  defaultTtl;
2159
2840
  isLocal = false;
2160
2841
  client;
2161
- serializer;
2842
+ serializers;
2162
2843
  prefix;
2163
2844
  allowUnprefixedClear;
2164
2845
  scanCount;
2165
2846
  compression;
2166
2847
  compressionThreshold;
2848
+ disconnectOnDispose;
2167
2849
  constructor(options) {
2168
2850
  this.client = options.client;
2169
2851
  this.defaultTtl = options.ttl;
2170
2852
  this.name = options.name ?? "redis";
2171
- this.serializer = options.serializer ?? new JsonSerializer();
2853
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
2172
2854
  this.prefix = options.prefix ?? "";
2173
2855
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
2174
2856
  this.scanCount = options.scanCount ?? 100;
2175
2857
  this.compression = options.compression;
2176
2858
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2859
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
2177
2860
  }
2178
2861
  async get(key) {
2179
2862
  const payload = await this.getEntry(key);
@@ -2208,8 +2891,25 @@ var RedisLayer = class {
2208
2891
  })
2209
2892
  );
2210
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
+ }
2211
2911
  async set(key, value, ttl = this.defaultTtl) {
2212
- const serialized = this.serializer.serialize(value);
2912
+ const serialized = this.primarySerializer().serialize(value);
2213
2913
  const payload = await this.encodePayload(serialized);
2214
2914
  const normalizedKey = this.withPrefix(key);
2215
2915
  if (ttl && ttl > 0) {
@@ -2242,6 +2942,18 @@ var RedisLayer = class {
2242
2942
  const keys = await this.keys();
2243
2943
  return keys.length;
2244
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
+ }
2245
2957
  /**
2246
2958
  * Deletes all keys matching the layer's prefix in batches to avoid
2247
2959
  * loading millions of keys into memory at once.
@@ -2288,12 +3000,39 @@ var RedisLayer = class {
2288
3000
  return `${this.prefix}${key}`;
2289
3001
  }
2290
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
+ }
2291
3014
  try {
2292
- return this.serializer.deserialize(await this.decodePayload(payload));
2293
- } catch {
2294
3015
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
2295
- return null;
3016
+ } catch {
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;
2296
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;
2297
3036
  }
2298
3037
  isSerializablePayload(payload) {
2299
3038
  return typeof payload === "string" || Buffer.isBuffer(payload);
@@ -2332,8 +3071,8 @@ var RedisLayer = class {
2332
3071
  };
2333
3072
 
2334
3073
  // src/layers/DiskLayer.ts
2335
- var import_node_crypto2 = require("crypto");
2336
- var import_node_fs2 = require("fs");
3074
+ var import_node_crypto = require("crypto");
3075
+ var import_node_fs = require("fs");
2337
3076
  var import_node_path = require("path");
2338
3077
  var DiskLayer = class {
2339
3078
  name;
@@ -2342,6 +3081,7 @@ var DiskLayer = class {
2342
3081
  directory;
2343
3082
  serializer;
2344
3083
  maxFiles;
3084
+ writeQueue = Promise.resolve();
2345
3085
  constructor(options) {
2346
3086
  this.directory = options.directory;
2347
3087
  this.defaultTtl = options.ttl;
@@ -2356,7 +3096,7 @@ var DiskLayer = class {
2356
3096
  const filePath = this.keyToPath(key);
2357
3097
  let raw;
2358
3098
  try {
2359
- raw = await import_node_fs2.promises.readFile(filePath);
3099
+ raw = await import_node_fs.promises.readFile(filePath);
2360
3100
  } catch {
2361
3101
  return null;
2362
3102
  }
@@ -2374,16 +3114,29 @@ var DiskLayer = class {
2374
3114
  return entry.value;
2375
3115
  }
2376
3116
  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();
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);
2387
3140
  }
2388
3141
  }
2389
3142
  async has(key) {
@@ -2394,7 +3147,7 @@ var DiskLayer = class {
2394
3147
  const filePath = this.keyToPath(key);
2395
3148
  let raw;
2396
3149
  try {
2397
- raw = await import_node_fs2.promises.readFile(filePath);
3150
+ raw = await import_node_fs.promises.readFile(filePath);
2398
3151
  } catch {
2399
3152
  return null;
2400
3153
  }
@@ -2414,21 +3167,25 @@ var DiskLayer = class {
2414
3167
  return remaining;
2415
3168
  }
2416
3169
  async delete(key) {
2417
- await this.safeDelete(this.keyToPath(key));
3170
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2418
3171
  }
2419
3172
  async deleteMany(keys) {
2420
- 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
+ });
2421
3176
  }
2422
3177
  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
- );
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
+ });
2432
3189
  }
2433
3190
  /**
2434
3191
  * Returns the original cache key strings stored on disk.
@@ -2437,7 +3194,7 @@ var DiskLayer = class {
2437
3194
  async keys() {
2438
3195
  let entries;
2439
3196
  try {
2440
- entries = await import_node_fs2.promises.readdir(this.directory);
3197
+ entries = await import_node_fs.promises.readdir(this.directory);
2441
3198
  } catch {
2442
3199
  return [];
2443
3200
  }
@@ -2448,7 +3205,7 @@ var DiskLayer = class {
2448
3205
  const filePath = (0, import_node_path.join)(this.directory, name);
2449
3206
  let raw;
2450
3207
  try {
2451
- raw = await import_node_fs2.promises.readFile(filePath);
3208
+ raw = await import_node_fs.promises.readFile(filePath);
2452
3209
  } catch {
2453
3210
  return;
2454
3211
  }
@@ -2472,16 +3229,31 @@ var DiskLayer = class {
2472
3229
  const keys = await this.keys();
2473
3230
  return keys.length;
2474
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
+ }
2475
3242
  keyToPath(key) {
2476
- 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");
2477
3244
  return (0, import_node_path.join)(this.directory, `${hash}.lc`);
2478
3245
  }
2479
3246
  async safeDelete(filePath) {
2480
3247
  try {
2481
- await import_node_fs2.promises.unlink(filePath);
3248
+ await import_node_fs.promises.unlink(filePath);
2482
3249
  } catch {
2483
3250
  }
2484
3251
  }
3252
+ enqueueWrite(operation) {
3253
+ const next = this.writeQueue.then(operation, operation);
3254
+ this.writeQueue = next.catch(() => void 0);
3255
+ return next;
3256
+ }
2485
3257
  /**
2486
3258
  * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
2487
3259
  */
@@ -2491,7 +3263,7 @@ var DiskLayer = class {
2491
3263
  }
2492
3264
  let entries;
2493
3265
  try {
2494
- entries = await import_node_fs2.promises.readdir(this.directory);
3266
+ entries = await import_node_fs.promises.readdir(this.directory);
2495
3267
  } catch {
2496
3268
  return;
2497
3269
  }
@@ -2503,7 +3275,7 @@ var DiskLayer = class {
2503
3275
  lcFiles.map(async (name) => {
2504
3276
  const filePath = (0, import_node_path.join)(this.directory, name);
2505
3277
  try {
2506
- const stat = await import_node_fs2.promises.stat(filePath);
3278
+ const stat = await import_node_fs.promises.stat(filePath);
2507
3279
  return { filePath, mtimeMs: stat.mtimeMs };
2508
3280
  } catch {
2509
3281
  return { filePath, mtimeMs: 0 };
@@ -2587,7 +3359,7 @@ var MsgpackSerializer = class {
2587
3359
  };
2588
3360
 
2589
3361
  // src/singleflight/RedisSingleFlightCoordinator.ts
2590
- var import_node_crypto3 = require("crypto");
3362
+ var import_node_crypto2 = require("crypto");
2591
3363
  var RELEASE_SCRIPT = `
2592
3364
  if redis.call("get", KEYS[1]) == ARGV[1] then
2593
3365
  return redis.call("del", KEYS[1])
@@ -2603,7 +3375,7 @@ var RedisSingleFlightCoordinator = class {
2603
3375
  }
2604
3376
  async execute(key, options, worker, waiter) {
2605
3377
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2606
- const token = (0, import_node_crypto3.randomUUID)();
3378
+ const token = (0, import_node_crypto2.randomUUID)();
2607
3379
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2608
3380
  if (acquired === "OK") {
2609
3381
  try {
@@ -2716,6 +3488,8 @@ function sanitizeLabel(value) {
2716
3488
  createCachedMethodDecorator,
2717
3489
  createExpressCacheMiddleware,
2718
3490
  createFastifyLayercachePlugin,
3491
+ createHonoCacheMiddleware,
3492
+ createOpenTelemetryPlugin,
2719
3493
  createPrometheusMetricsExporter,
2720
3494
  createTrpcCacheMiddleware
2721
3495
  });