layercache 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,66 +1,103 @@
1
1
  import {
2
- PatternMatcher,
3
2
  RedisTagIndex
4
- } from "./chunk-QUB5VZFZ.js";
3
+ } from "./chunk-GF47Y3XR.js";
4
+ import {
5
+ MemoryLayer,
6
+ TagIndex,
7
+ createHonoCacheMiddleware
8
+ } from "./chunk-46UH7LNM.js";
9
+ import {
10
+ PatternMatcher,
11
+ createStoredValueEnvelope,
12
+ isStoredValueEnvelope,
13
+ refreshStoredEnvelope,
14
+ remainingFreshTtlSeconds,
15
+ remainingStoredTtlSeconds,
16
+ resolveStoredValue,
17
+ unwrapStoredValue
18
+ } from "./chunk-ZMDB5KOK.js";
5
19
 
6
20
  // src/CacheStack.ts
7
- import { randomUUID } from "crypto";
8
21
  import { EventEmitter } from "events";
9
- import { promises as fs } from "fs";
10
22
 
11
23
  // src/CacheNamespace.ts
12
- var CacheNamespace = class {
24
+ import { Mutex } from "async-mutex";
25
+ var CacheNamespace = class _CacheNamespace {
13
26
  constructor(cache, prefix) {
14
27
  this.cache = cache;
15
28
  this.prefix = prefix;
16
29
  }
17
30
  cache;
18
31
  prefix;
32
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
33
+ metrics = emptyMetrics();
19
34
  async get(key, fetcher, options) {
20
- return this.cache.get(this.qualify(key), fetcher, options);
35
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
21
36
  }
22
37
  async getOrSet(key, fetcher, options) {
23
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
38
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
39
+ }
40
+ /**
41
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
42
+ */
43
+ async getOrThrow(key, fetcher, options) {
44
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
24
45
  }
25
46
  async has(key) {
26
- return this.cache.has(this.qualify(key));
47
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
27
48
  }
28
49
  async ttl(key) {
29
- return this.cache.ttl(this.qualify(key));
50
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
30
51
  }
31
52
  async set(key, value, options) {
32
- await this.cache.set(this.qualify(key), value, options);
53
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
33
54
  }
34
55
  async delete(key) {
35
- await this.cache.delete(this.qualify(key));
56
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
36
57
  }
37
58
  async mdelete(keys) {
38
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
59
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
39
60
  }
40
61
  async clear() {
41
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
62
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
42
63
  }
43
64
  async mget(entries) {
44
- return this.cache.mget(
45
- entries.map((entry) => ({
46
- ...entry,
47
- key: this.qualify(entry.key)
48
- }))
65
+ return this.trackMetrics(
66
+ () => this.cache.mget(
67
+ entries.map((entry) => ({
68
+ ...entry,
69
+ key: this.qualify(entry.key)
70
+ }))
71
+ )
49
72
  );
50
73
  }
51
74
  async mset(entries) {
52
- await this.cache.mset(
53
- entries.map((entry) => ({
54
- ...entry,
55
- key: this.qualify(entry.key)
56
- }))
75
+ await this.trackMetrics(
76
+ () => this.cache.mset(
77
+ entries.map((entry) => ({
78
+ ...entry,
79
+ key: this.qualify(entry.key)
80
+ }))
81
+ )
57
82
  );
58
83
  }
59
84
  async invalidateByTag(tag) {
60
- await this.cache.invalidateByTag(tag);
85
+ await this.trackMetrics(() => this.cache.invalidateByTag(tag));
86
+ }
87
+ async invalidateByTags(tags, mode = "any") {
88
+ await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
61
89
  }
62
90
  async invalidateByPattern(pattern) {
63
- await this.cache.invalidateByPattern(this.qualify(pattern));
91
+ await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
92
+ }
93
+ async invalidateByPrefix(prefix) {
94
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
95
+ }
96
+ /**
97
+ * Returns detailed metadata about a single cache key within this namespace.
98
+ */
99
+ async inspect(key) {
100
+ return this.cache.inspect(this.qualify(key));
64
101
  }
65
102
  wrap(keyPrefix, fetcher, options) {
66
103
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
@@ -75,15 +112,159 @@ var CacheNamespace = class {
75
112
  );
76
113
  }
77
114
  getMetrics() {
78
- return this.cache.getMetrics();
115
+ return cloneMetrics(this.metrics);
79
116
  }
80
117
  getHitRate() {
81
- return this.cache.getHitRate();
118
+ const total = this.metrics.hits + this.metrics.misses;
119
+ const overall = total === 0 ? 0 : this.metrics.hits / total;
120
+ const byLayer = {};
121
+ const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
122
+ for (const layer of layers) {
123
+ const hits = this.metrics.hitsByLayer[layer] ?? 0;
124
+ const misses = this.metrics.missesByLayer[layer] ?? 0;
125
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
126
+ }
127
+ return { overall, byLayer };
128
+ }
129
+ /**
130
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
131
+ *
132
+ * ```ts
133
+ * const tenant = cache.namespace('tenant:abc')
134
+ * const posts = tenant.namespace('posts')
135
+ * // keys become: "tenant:abc:posts:mykey"
136
+ * ```
137
+ */
138
+ namespace(childPrefix) {
139
+ return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
82
140
  }
83
141
  qualify(key) {
84
142
  return `${this.prefix}:${key}`;
85
143
  }
144
+ async trackMetrics(operation) {
145
+ return this.getMetricsMutex().runExclusive(async () => {
146
+ const before = this.cache.getMetrics();
147
+ const result = await operation();
148
+ const after = this.cache.getMetrics();
149
+ this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
150
+ return result;
151
+ });
152
+ }
153
+ getMetricsMutex() {
154
+ const existing = _CacheNamespace.metricsMutexes.get(this.cache);
155
+ if (existing) {
156
+ return existing;
157
+ }
158
+ const mutex = new Mutex();
159
+ _CacheNamespace.metricsMutexes.set(this.cache, mutex);
160
+ return mutex;
161
+ }
86
162
  };
163
+ function emptyMetrics() {
164
+ return {
165
+ hits: 0,
166
+ misses: 0,
167
+ fetches: 0,
168
+ sets: 0,
169
+ deletes: 0,
170
+ backfills: 0,
171
+ invalidations: 0,
172
+ staleHits: 0,
173
+ refreshes: 0,
174
+ refreshErrors: 0,
175
+ writeFailures: 0,
176
+ singleFlightWaits: 0,
177
+ negativeCacheHits: 0,
178
+ circuitBreakerTrips: 0,
179
+ degradedOperations: 0,
180
+ hitsByLayer: {},
181
+ missesByLayer: {},
182
+ latencyByLayer: {},
183
+ resetAt: Date.now()
184
+ };
185
+ }
186
+ function cloneMetrics(metrics) {
187
+ return {
188
+ ...metrics,
189
+ hitsByLayer: { ...metrics.hitsByLayer },
190
+ missesByLayer: { ...metrics.missesByLayer },
191
+ latencyByLayer: Object.fromEntries(
192
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
193
+ )
194
+ };
195
+ }
196
+ function diffMetrics(before, after) {
197
+ const latencyByLayer = Object.fromEntries(
198
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
199
+ layer,
200
+ {
201
+ avgMs: value.avgMs,
202
+ maxMs: value.maxMs,
203
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
204
+ }
205
+ ])
206
+ );
207
+ return {
208
+ hits: after.hits - before.hits,
209
+ misses: after.misses - before.misses,
210
+ fetches: after.fetches - before.fetches,
211
+ sets: after.sets - before.sets,
212
+ deletes: after.deletes - before.deletes,
213
+ backfills: after.backfills - before.backfills,
214
+ invalidations: after.invalidations - before.invalidations,
215
+ staleHits: after.staleHits - before.staleHits,
216
+ refreshes: after.refreshes - before.refreshes,
217
+ refreshErrors: after.refreshErrors - before.refreshErrors,
218
+ writeFailures: after.writeFailures - before.writeFailures,
219
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
220
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
221
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
222
+ degradedOperations: after.degradedOperations - before.degradedOperations,
223
+ hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
224
+ missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
225
+ latencyByLayer,
226
+ resetAt: after.resetAt
227
+ };
228
+ }
229
+ function addMetrics(base, delta) {
230
+ return {
231
+ hits: base.hits + delta.hits,
232
+ misses: base.misses + delta.misses,
233
+ fetches: base.fetches + delta.fetches,
234
+ sets: base.sets + delta.sets,
235
+ deletes: base.deletes + delta.deletes,
236
+ backfills: base.backfills + delta.backfills,
237
+ invalidations: base.invalidations + delta.invalidations,
238
+ staleHits: base.staleHits + delta.staleHits,
239
+ refreshes: base.refreshes + delta.refreshes,
240
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
241
+ writeFailures: base.writeFailures + delta.writeFailures,
242
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
243
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
244
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
245
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
246
+ hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
247
+ missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
248
+ latencyByLayer: cloneMetrics(delta).latencyByLayer,
249
+ resetAt: base.resetAt
250
+ };
251
+ }
252
+ function diffMap(before, after) {
253
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
254
+ const result = {};
255
+ for (const key of keys) {
256
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
257
+ }
258
+ return result;
259
+ }
260
+ function addMap(base, delta) {
261
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
262
+ const result = {};
263
+ for (const key of keys) {
264
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
265
+ }
266
+ return result;
267
+ }
87
268
 
88
269
  // src/internal/CircuitBreakerManager.ts
89
270
  var CircuitBreakerManager = class {
@@ -177,11 +358,105 @@ var CircuitBreakerManager = class {
177
358
  }
178
359
  };
179
360
 
361
+ // src/internal/FetchRateLimiter.ts
362
+ var FetchRateLimiter = class {
363
+ active = 0;
364
+ queue = [];
365
+ startedAt = [];
366
+ drainTimer;
367
+ async schedule(options, task) {
368
+ if (!options) {
369
+ return task();
370
+ }
371
+ const normalized = this.normalize(options);
372
+ if (!normalized) {
373
+ return task();
374
+ }
375
+ return new Promise((resolve, reject) => {
376
+ this.queue.push({ options: normalized, task, resolve, reject });
377
+ this.drain();
378
+ });
379
+ }
380
+ normalize(options) {
381
+ const maxConcurrent = options.maxConcurrent;
382
+ const intervalMs = options.intervalMs;
383
+ const maxPerInterval = options.maxPerInterval;
384
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
385
+ return void 0;
386
+ }
387
+ return {
388
+ maxConcurrent,
389
+ intervalMs,
390
+ maxPerInterval
391
+ };
392
+ }
393
+ drain() {
394
+ if (this.drainTimer) {
395
+ clearTimeout(this.drainTimer);
396
+ this.drainTimer = void 0;
397
+ }
398
+ while (this.queue.length > 0) {
399
+ const next = this.queue[0];
400
+ if (!next) {
401
+ return;
402
+ }
403
+ const waitMs = this.waitTime(next.options);
404
+ if (waitMs > 0) {
405
+ this.drainTimer = setTimeout(() => {
406
+ this.drainTimer = void 0;
407
+ this.drain();
408
+ }, waitMs);
409
+ this.drainTimer.unref?.();
410
+ return;
411
+ }
412
+ this.queue.shift();
413
+ this.active += 1;
414
+ this.startedAt.push(Date.now());
415
+ void next.task().then(next.resolve, next.reject).finally(() => {
416
+ this.active -= 1;
417
+ this.drain();
418
+ });
419
+ }
420
+ }
421
+ waitTime(options) {
422
+ const now = Date.now();
423
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
424
+ return 1;
425
+ }
426
+ if (!options.intervalMs || !options.maxPerInterval) {
427
+ return 0;
428
+ }
429
+ this.prune(now, options.intervalMs);
430
+ if (this.startedAt.length < options.maxPerInterval) {
431
+ return 0;
432
+ }
433
+ const oldest = this.startedAt[0];
434
+ if (!oldest) {
435
+ return 0;
436
+ }
437
+ return Math.max(1, options.intervalMs - (now - oldest));
438
+ }
439
+ prune(now, intervalMs) {
440
+ while (this.startedAt.length > 0) {
441
+ const startedAt = this.startedAt[0];
442
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
443
+ break;
444
+ }
445
+ this.startedAt.shift();
446
+ }
447
+ }
448
+ };
449
+
180
450
  // src/internal/MetricsCollector.ts
181
451
  var MetricsCollector = class {
182
452
  data = this.empty();
183
453
  get snapshot() {
184
- return { ...this.data };
454
+ return {
455
+ ...this.data,
456
+ hitsByLayer: { ...this.data.hitsByLayer },
457
+ missesByLayer: { ...this.data.missesByLayer },
458
+ latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
459
+ };
185
460
  }
186
461
  increment(field, amount = 1) {
187
462
  ;
@@ -190,6 +465,22 @@ var MetricsCollector = class {
190
465
  incrementLayer(map, layerName) {
191
466
  this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
192
467
  }
468
+ /**
469
+ * Records a read latency sample for the given layer.
470
+ * Maintains a rolling average and max using Welford's online algorithm.
471
+ */
472
+ recordLatency(layerName, durationMs) {
473
+ const existing = this.data.latencyByLayer[layerName];
474
+ if (!existing) {
475
+ this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
476
+ return;
477
+ }
478
+ existing.count += 1;
479
+ existing.avgMs += (durationMs - existing.avgMs) / existing.count;
480
+ if (durationMs > existing.maxMs) {
481
+ existing.maxMs = durationMs;
482
+ }
483
+ }
193
484
  reset() {
194
485
  this.data = this.empty();
195
486
  }
@@ -224,112 +515,12 @@ var MetricsCollector = class {
224
515
  degradedOperations: 0,
225
516
  hitsByLayer: {},
226
517
  missesByLayer: {},
518
+ latencyByLayer: {},
227
519
  resetAt: Date.now()
228
520
  };
229
521
  }
230
522
  };
231
523
 
232
- // src/internal/StoredValue.ts
233
- function isStoredValueEnvelope(value) {
234
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
235
- }
236
- function createStoredValueEnvelope(options) {
237
- const now = options.now ?? Date.now();
238
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
239
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
240
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
241
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
242
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
243
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
244
- return {
245
- __layercache: 1,
246
- kind: options.kind,
247
- value: options.value,
248
- freshUntil,
249
- staleUntil,
250
- errorUntil,
251
- freshTtlSeconds: freshTtlSeconds ?? null,
252
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
253
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
254
- };
255
- }
256
- function resolveStoredValue(stored, now = Date.now()) {
257
- if (!isStoredValueEnvelope(stored)) {
258
- return { state: "fresh", value: stored, stored };
259
- }
260
- if (stored.freshUntil === null || stored.freshUntil > now) {
261
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
262
- }
263
- if (stored.staleUntil !== null && stored.staleUntil > now) {
264
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
265
- }
266
- if (stored.errorUntil !== null && stored.errorUntil > now) {
267
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
268
- }
269
- return { state: "expired", value: null, stored, envelope: stored };
270
- }
271
- function unwrapStoredValue(stored) {
272
- if (!isStoredValueEnvelope(stored)) {
273
- return stored;
274
- }
275
- if (stored.kind === "empty") {
276
- return null;
277
- }
278
- return stored.value ?? null;
279
- }
280
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
281
- if (!isStoredValueEnvelope(stored)) {
282
- return void 0;
283
- }
284
- const expiry = maxExpiry(stored);
285
- if (expiry === null) {
286
- return void 0;
287
- }
288
- const remainingMs = expiry - now;
289
- if (remainingMs <= 0) {
290
- return 1;
291
- }
292
- return Math.max(1, Math.ceil(remainingMs / 1e3));
293
- }
294
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
295
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
296
- return void 0;
297
- }
298
- const remainingMs = stored.freshUntil - now;
299
- if (remainingMs <= 0) {
300
- return 0;
301
- }
302
- return Math.max(1, Math.ceil(remainingMs / 1e3));
303
- }
304
- function refreshStoredEnvelope(stored, now = Date.now()) {
305
- if (!isStoredValueEnvelope(stored)) {
306
- return stored;
307
- }
308
- return createStoredValueEnvelope({
309
- kind: stored.kind,
310
- value: stored.value,
311
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
312
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
313
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
314
- now
315
- });
316
- }
317
- function maxExpiry(stored) {
318
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
319
- (value) => value !== null
320
- );
321
- if (values.length === 0) {
322
- return null;
323
- }
324
- return Math.max(...values);
325
- }
326
- function normalizePositiveSeconds(value) {
327
- if (!value || value <= 0) {
328
- return void 0;
329
- }
330
- return value;
331
- }
332
-
333
524
  // src/internal/TtlResolver.ts
334
525
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
335
526
  var TtlResolver = class {
@@ -351,13 +542,14 @@ var TtlResolver = class {
351
542
  clearProfiles() {
352
543
  this.accessProfiles.clear();
353
544
  }
354
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
545
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
546
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
355
547
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
356
548
  layerName,
357
549
  options?.negativeTtl,
358
550
  globalNegativeTtl,
359
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
360
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
551
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
552
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
361
553
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
362
554
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
363
555
  return this.applyJitter(adaptiveTtl, jitter);
@@ -396,6 +588,29 @@ var TtlResolver = class {
396
588
  const delta = (Math.random() * 2 - 1) * jitter;
397
589
  return Math.max(1, Math.round(ttl + delta));
398
590
  }
591
+ resolvePolicyTtl(key, value, policy) {
592
+ if (!policy) {
593
+ return void 0;
594
+ }
595
+ if (typeof policy === "function") {
596
+ return policy({ key, value });
597
+ }
598
+ const now = /* @__PURE__ */ new Date();
599
+ if (policy === "until-midnight") {
600
+ const nextMidnight = new Date(now);
601
+ nextMidnight.setHours(24, 0, 0, 0);
602
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
603
+ }
604
+ if (policy === "next-hour") {
605
+ const nextHour = new Date(now);
606
+ nextHour.setMinutes(60, 0, 0);
607
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
608
+ }
609
+ const alignToSeconds = policy.alignTo;
610
+ const currentSeconds = Math.floor(Date.now() / 1e3);
611
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
612
+ return Math.max(1, nextBoundary - currentSeconds);
613
+ }
399
614
  readLayerNumber(layerName, value) {
400
615
  if (typeof value === "number") {
401
616
  return value;
@@ -418,66 +633,8 @@ var TtlResolver = class {
418
633
  }
419
634
  };
420
635
 
421
- // src/invalidation/TagIndex.ts
422
- var TagIndex = class {
423
- tagToKeys = /* @__PURE__ */ new Map();
424
- keyToTags = /* @__PURE__ */ new Map();
425
- knownKeys = /* @__PURE__ */ new Set();
426
- async touch(key) {
427
- this.knownKeys.add(key);
428
- }
429
- async track(key, tags) {
430
- this.knownKeys.add(key);
431
- if (tags.length === 0) {
432
- return;
433
- }
434
- const existingTags = this.keyToTags.get(key);
435
- if (existingTags) {
436
- for (const tag of existingTags) {
437
- this.tagToKeys.get(tag)?.delete(key);
438
- }
439
- }
440
- const tagSet = new Set(tags);
441
- this.keyToTags.set(key, tagSet);
442
- for (const tag of tagSet) {
443
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
444
- keys.add(key);
445
- this.tagToKeys.set(tag, keys);
446
- }
447
- }
448
- async remove(key) {
449
- this.knownKeys.delete(key);
450
- const tags = this.keyToTags.get(key);
451
- if (!tags) {
452
- return;
453
- }
454
- for (const tag of tags) {
455
- const keys = this.tagToKeys.get(tag);
456
- if (!keys) {
457
- continue;
458
- }
459
- keys.delete(key);
460
- if (keys.size === 0) {
461
- this.tagToKeys.delete(tag);
462
- }
463
- }
464
- this.keyToTags.delete(key);
465
- }
466
- async keysForTag(tag) {
467
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
468
- }
469
- async matchPattern(pattern) {
470
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
471
- }
472
- async clear() {
473
- this.tagToKeys.clear();
474
- this.keyToTags.clear();
475
- this.knownKeys.clear();
476
- }
477
- };
478
-
479
636
  // src/stampede/StampedeGuard.ts
480
- import { Mutex } from "async-mutex";
637
+ import { Mutex as Mutex2 } from "async-mutex";
481
638
  var StampedeGuard = class {
482
639
  mutexes = /* @__PURE__ */ new Map();
483
640
  async execute(key, task) {
@@ -494,7 +651,7 @@ var StampedeGuard = class {
494
651
  getMutexEntry(key) {
495
652
  let entry = this.mutexes.get(key);
496
653
  if (!entry) {
497
- entry = { mutex: new Mutex(), references: 0 };
654
+ entry = { mutex: new Mutex2(), references: 0 };
498
655
  this.mutexes.set(key, entry);
499
656
  }
500
657
  entry.references += 1;
@@ -502,6 +659,16 @@ var StampedeGuard = class {
502
659
  }
503
660
  };
504
661
 
662
+ // src/types.ts
663
+ var CacheMissError = class extends Error {
664
+ key;
665
+ constructor(key) {
666
+ super(`Cache miss for key "${key}".`);
667
+ this.name = "CacheMissError";
668
+ this.key = key;
669
+ }
670
+ };
671
+
505
672
  // src/CacheStack.ts
506
673
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
507
674
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
@@ -545,6 +712,7 @@ var CacheStack = class extends EventEmitter {
545
712
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
546
713
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
547
714
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
715
+ this.currentGeneration = options.generation;
548
716
  if (options.publishSetInvalidation !== void 0) {
549
717
  console.warn(
550
718
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -553,21 +721,27 @@ var CacheStack = class extends EventEmitter {
553
721
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
554
722
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
555
723
  this.tagIndex = options.tagIndex ?? new TagIndex();
724
+ this.initializeWriteBehind(options.writeBehind);
556
725
  this.startup = this.initialize();
557
726
  }
558
727
  layers;
559
728
  options;
560
729
  stampedeGuard = new StampedeGuard();
561
730
  metricsCollector = new MetricsCollector();
562
- instanceId = randomUUID();
731
+ instanceId = createInstanceId();
563
732
  startup;
564
733
  unsubscribeInvalidation;
565
734
  logger;
566
735
  tagIndex;
736
+ fetchRateLimiter = new FetchRateLimiter();
567
737
  backgroundRefreshes = /* @__PURE__ */ new Map();
568
738
  layerDegradedUntil = /* @__PURE__ */ new Map();
569
739
  ttlResolver;
570
740
  circuitBreakerManager;
741
+ currentGeneration;
742
+ writeBehindQueue = [];
743
+ writeBehindTimer;
744
+ writeBehindFlushPromise;
571
745
  isDisconnecting = false;
572
746
  disconnectPromise;
573
747
  /**
@@ -577,9 +751,9 @@ var CacheStack = class extends EventEmitter {
577
751
  * and no `fetcher` is provided.
578
752
  */
579
753
  async get(key, fetcher, options) {
580
- const normalizedKey = this.validateCacheKey(key);
754
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
581
755
  this.validateWriteOptions(options);
582
- await this.startup;
756
+ await this.awaitStartup("get");
583
757
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
584
758
  if (hit.found) {
585
759
  this.ttlResolver.recordAccess(normalizedKey);
@@ -628,12 +802,24 @@ var CacheStack = class extends EventEmitter {
628
802
  async getOrSet(key, fetcher, options) {
629
803
  return this.get(key, fetcher, options);
630
804
  }
805
+ /**
806
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
807
+ * Useful when the value is expected to exist or the fetcher is expected to
808
+ * return non-null.
809
+ */
810
+ async getOrThrow(key, fetcher, options) {
811
+ const value = await this.get(key, fetcher, options);
812
+ if (value === null) {
813
+ throw new CacheMissError(key);
814
+ }
815
+ return value;
816
+ }
631
817
  /**
632
818
  * Returns true if the given key exists and is not expired in any layer.
633
819
  */
634
820
  async has(key) {
635
- const normalizedKey = this.validateCacheKey(key);
636
- await this.startup;
821
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
822
+ await this.awaitStartup("has");
637
823
  for (const layer of this.layers) {
638
824
  if (this.shouldSkipLayer(layer)) {
639
825
  continue;
@@ -663,8 +849,8 @@ var CacheStack = class extends EventEmitter {
663
849
  * that has it, or null if the key is not found / has no TTL.
664
850
  */
665
851
  async ttl(key) {
666
- const normalizedKey = this.validateCacheKey(key);
667
- await this.startup;
852
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
853
+ await this.awaitStartup("ttl");
668
854
  for (const layer of this.layers) {
669
855
  if (this.shouldSkipLayer(layer)) {
670
856
  continue;
@@ -685,17 +871,17 @@ var CacheStack = class extends EventEmitter {
685
871
  * Stores a value in all cache layers. Overwrites any existing value.
686
872
  */
687
873
  async set(key, value, options) {
688
- const normalizedKey = this.validateCacheKey(key);
874
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
689
875
  this.validateWriteOptions(options);
690
- await this.startup;
876
+ await this.awaitStartup("set");
691
877
  await this.storeEntry(normalizedKey, "value", value, options);
692
878
  }
693
879
  /**
694
880
  * Deletes the key from all layers and publishes an invalidation message.
695
881
  */
696
882
  async delete(key) {
697
- const normalizedKey = this.validateCacheKey(key);
698
- await this.startup;
883
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
884
+ await this.awaitStartup("delete");
699
885
  await this.deleteKeys([normalizedKey]);
700
886
  await this.publishInvalidation({
701
887
  scope: "key",
@@ -705,7 +891,7 @@ var CacheStack = class extends EventEmitter {
705
891
  });
706
892
  }
707
893
  async clear() {
708
- await this.startup;
894
+ await this.awaitStartup("clear");
709
895
  await Promise.all(this.layers.map((layer) => layer.clear()));
710
896
  await this.tagIndex.clear();
711
897
  this.ttlResolver.clearProfiles();
@@ -721,23 +907,25 @@ var CacheStack = class extends EventEmitter {
721
907
  if (keys.length === 0) {
722
908
  return;
723
909
  }
724
- await this.startup;
910
+ await this.awaitStartup("mdelete");
725
911
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
726
- await this.deleteKeys(normalizedKeys);
912
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
913
+ await this.deleteKeys(cacheKeys);
727
914
  await this.publishInvalidation({
728
915
  scope: "keys",
729
- keys: normalizedKeys,
916
+ keys: cacheKeys,
730
917
  sourceId: this.instanceId,
731
918
  operation: "delete"
732
919
  });
733
920
  }
734
921
  async mget(entries) {
922
+ this.assertActive("mget");
735
923
  if (entries.length === 0) {
736
924
  return [];
737
925
  }
738
926
  const normalizedEntries = entries.map((entry) => ({
739
927
  ...entry,
740
- key: this.validateCacheKey(entry.key)
928
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
741
929
  }));
742
930
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
743
931
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -763,7 +951,7 @@ var CacheStack = class extends EventEmitter {
763
951
  })
764
952
  );
765
953
  }
766
- await this.startup;
954
+ await this.awaitStartup("mget");
767
955
  const pending = /* @__PURE__ */ new Set();
768
956
  const indexesByKey = /* @__PURE__ */ new Map();
769
957
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -811,14 +999,17 @@ var CacheStack = class extends EventEmitter {
811
999
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
812
1000
  }
813
1001
  async mset(entries) {
1002
+ this.assertActive("mset");
814
1003
  const normalizedEntries = entries.map((entry) => ({
815
1004
  ...entry,
816
- key: this.validateCacheKey(entry.key)
1005
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
817
1006
  }));
818
1007
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
819
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1008
+ await this.awaitStartup("mset");
1009
+ await this.writeBatch(normalizedEntries);
820
1010
  }
821
1011
  async warm(entries, options = {}) {
1012
+ this.assertActive("warm");
822
1013
  const concurrency = Math.max(1, options.concurrency ?? 4);
823
1014
  const total = entries.length;
824
1015
  let completed = 0;
@@ -867,14 +1058,31 @@ var CacheStack = class extends EventEmitter {
867
1058
  return new CacheNamespace(this, prefix);
868
1059
  }
869
1060
  async invalidateByTag(tag) {
870
- await this.startup;
1061
+ await this.awaitStartup("invalidateByTag");
871
1062
  const keys = await this.tagIndex.keysForTag(tag);
872
1063
  await this.deleteKeys(keys);
873
1064
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
874
1065
  }
1066
+ async invalidateByTags(tags, mode = "any") {
1067
+ if (tags.length === 0) {
1068
+ return;
1069
+ }
1070
+ await this.awaitStartup("invalidateByTags");
1071
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1072
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1073
+ await this.deleteKeys(keys);
1074
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1075
+ }
875
1076
  async invalidateByPattern(pattern) {
876
- await this.startup;
877
- const keys = await this.tagIndex.matchPattern(pattern);
1077
+ await this.awaitStartup("invalidateByPattern");
1078
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1079
+ await this.deleteKeys(keys);
1080
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1081
+ }
1082
+ async invalidateByPrefix(prefix) {
1083
+ await this.awaitStartup("invalidateByPrefix");
1084
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1085
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
878
1086
  await this.deleteKeys(keys);
879
1087
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
880
1088
  }
@@ -901,8 +1109,77 @@ var CacheStack = class extends EventEmitter {
901
1109
  getHitRate() {
902
1110
  return this.metricsCollector.hitRate();
903
1111
  }
904
- async exportState() {
1112
+ async healthCheck() {
905
1113
  await this.startup;
1114
+ return Promise.all(
1115
+ this.layers.map(async (layer) => {
1116
+ const startedAt = performance.now();
1117
+ try {
1118
+ const healthy = layer.ping ? await layer.ping() : true;
1119
+ return {
1120
+ layer: layer.name,
1121
+ healthy,
1122
+ latencyMs: performance.now() - startedAt
1123
+ };
1124
+ } catch (error) {
1125
+ return {
1126
+ layer: layer.name,
1127
+ healthy: false,
1128
+ latencyMs: performance.now() - startedAt,
1129
+ error: this.formatError(error)
1130
+ };
1131
+ }
1132
+ })
1133
+ );
1134
+ }
1135
+ bumpGeneration(nextGeneration) {
1136
+ const current = this.currentGeneration ?? 0;
1137
+ this.currentGeneration = nextGeneration ?? current + 1;
1138
+ return this.currentGeneration;
1139
+ }
1140
+ /**
1141
+ * Returns detailed metadata about a single cache key: which layers contain it,
1142
+ * remaining fresh/stale/error TTLs, and associated tags.
1143
+ * Returns `null` if the key does not exist in any layer.
1144
+ */
1145
+ async inspect(key) {
1146
+ const userKey = this.validateCacheKey(key);
1147
+ const normalizedKey = this.qualifyKey(userKey);
1148
+ await this.awaitStartup("inspect");
1149
+ const foundInLayers = [];
1150
+ let freshTtlSeconds = null;
1151
+ let staleTtlSeconds = null;
1152
+ let errorTtlSeconds = null;
1153
+ let isStale = false;
1154
+ for (const layer of this.layers) {
1155
+ if (this.shouldSkipLayer(layer)) {
1156
+ continue;
1157
+ }
1158
+ const stored = await this.readLayerEntry(layer, normalizedKey);
1159
+ if (stored === null) {
1160
+ continue;
1161
+ }
1162
+ const resolved = resolveStoredValue(stored);
1163
+ if (resolved.state === "expired") {
1164
+ continue;
1165
+ }
1166
+ foundInLayers.push(layer.name);
1167
+ if (foundInLayers.length === 1 && resolved.envelope) {
1168
+ const now = Date.now();
1169
+ freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
1170
+ staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
1171
+ errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
1172
+ isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
1173
+ }
1174
+ }
1175
+ if (foundInLayers.length === 0) {
1176
+ return null;
1177
+ }
1178
+ const tags = await this.getTagsForKey(normalizedKey);
1179
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1180
+ }
1181
+ async exportState() {
1182
+ await this.awaitStartup("exportState");
906
1183
  const exported = /* @__PURE__ */ new Map();
907
1184
  for (const layer of this.layers) {
908
1185
  if (!layer.keys) {
@@ -910,15 +1187,16 @@ var CacheStack = class extends EventEmitter {
910
1187
  }
911
1188
  const keys = await layer.keys();
912
1189
  for (const key of keys) {
913
- if (exported.has(key)) {
1190
+ const exportedKey = this.stripQualifiedKey(key);
1191
+ if (exported.has(exportedKey)) {
914
1192
  continue;
915
1193
  }
916
1194
  const stored = await this.readLayerEntry(layer, key);
917
1195
  if (stored === null) {
918
1196
  continue;
919
1197
  }
920
- exported.set(key, {
921
- key,
1198
+ exported.set(exportedKey, {
1199
+ key: exportedKey,
922
1200
  value: stored,
923
1201
  ttl: remainingStoredTtlSeconds(stored)
924
1202
  });
@@ -927,20 +1205,25 @@ var CacheStack = class extends EventEmitter {
927
1205
  return [...exported.values()];
928
1206
  }
929
1207
  async importState(entries) {
930
- await this.startup;
1208
+ await this.awaitStartup("importState");
931
1209
  await Promise.all(
932
1210
  entries.map(async (entry) => {
933
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
934
- await this.tagIndex.touch(entry.key);
1211
+ const qualifiedKey = this.qualifyKey(entry.key);
1212
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1213
+ await this.tagIndex.touch(qualifiedKey);
935
1214
  })
936
1215
  );
937
1216
  }
938
1217
  async persistToFile(filePath) {
1218
+ this.assertActive("persistToFile");
939
1219
  const snapshot = await this.exportState();
940
- await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1220
+ const { promises: fs2 } = await import("fs");
1221
+ await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
941
1222
  }
942
1223
  async restoreFromFile(filePath) {
943
- const raw = await fs.readFile(filePath, "utf8");
1224
+ this.assertActive("restoreFromFile");
1225
+ const { promises: fs2 } = await import("fs");
1226
+ const raw = await fs2.readFile(filePath, "utf8");
944
1227
  let parsed;
945
1228
  try {
946
1229
  parsed = JSON.parse(raw, (_key, value) => {
@@ -963,7 +1246,13 @@ var CacheStack = class extends EventEmitter {
963
1246
  this.disconnectPromise = (async () => {
964
1247
  await this.startup;
965
1248
  await this.unsubscribeInvalidation?.();
1249
+ await this.flushWriteBehindQueue();
966
1250
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1251
+ if (this.writeBehindTimer) {
1252
+ clearInterval(this.writeBehindTimer);
1253
+ this.writeBehindTimer = void 0;
1254
+ }
1255
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
967
1256
  })();
968
1257
  }
969
1258
  await this.disconnectPromise;
@@ -1023,7 +1312,10 @@ var CacheStack = class extends EventEmitter {
1023
1312
  const fetchStart = Date.now();
1024
1313
  let fetched;
1025
1314
  try {
1026
- fetched = await fetcher();
1315
+ fetched = await this.fetchRateLimiter.schedule(
1316
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1317
+ fetcher
1318
+ );
1027
1319
  this.circuitBreakerManager.recordSuccess(key);
1028
1320
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1029
1321
  } catch (error) {
@@ -1037,6 +1329,9 @@ var CacheStack = class extends EventEmitter {
1037
1329
  await this.storeEntry(key, "empty", null, options);
1038
1330
  return null;
1039
1331
  }
1332
+ if (options?.shouldCache && !options.shouldCache(fetched)) {
1333
+ return fetched;
1334
+ }
1040
1335
  await this.storeEntry(key, "value", fetched, options);
1041
1336
  return fetched;
1042
1337
  }
@@ -1054,12 +1349,70 @@ var CacheStack = class extends EventEmitter {
1054
1349
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1055
1350
  }
1056
1351
  }
1352
+ async writeBatch(entries) {
1353
+ const now = Date.now();
1354
+ const entriesByLayer = /* @__PURE__ */ new Map();
1355
+ const immediateOperations = [];
1356
+ const deferredOperations = [];
1357
+ for (const entry of entries) {
1358
+ for (const layer of this.layers) {
1359
+ if (this.shouldSkipLayer(layer)) {
1360
+ continue;
1361
+ }
1362
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1363
+ const bucket = entriesByLayer.get(layer) ?? [];
1364
+ bucket.push(layerEntry);
1365
+ entriesByLayer.set(layer, bucket);
1366
+ }
1367
+ }
1368
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1369
+ const operation = async () => {
1370
+ try {
1371
+ if (layer.setMany) {
1372
+ await layer.setMany(layerEntries);
1373
+ return;
1374
+ }
1375
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1376
+ } catch (error) {
1377
+ await this.handleLayerFailure(layer, "write", error);
1378
+ }
1379
+ };
1380
+ if (this.shouldWriteBehind(layer)) {
1381
+ deferredOperations.push(operation);
1382
+ } else {
1383
+ immediateOperations.push(operation);
1384
+ }
1385
+ }
1386
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1387
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1388
+ for (const entry of entries) {
1389
+ if (entry.options?.tags) {
1390
+ await this.tagIndex.track(entry.key, entry.options.tags);
1391
+ } else {
1392
+ await this.tagIndex.touch(entry.key);
1393
+ }
1394
+ this.metricsCollector.increment("sets");
1395
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1396
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1397
+ }
1398
+ if (this.shouldBroadcastL1Invalidation()) {
1399
+ await this.publishInvalidation({
1400
+ scope: "keys",
1401
+ keys: entries.map((entry) => entry.key),
1402
+ sourceId: this.instanceId,
1403
+ operation: "write"
1404
+ });
1405
+ }
1406
+ }
1057
1407
  async readFromLayers(key, options, mode) {
1058
1408
  let sawRetainableValue = false;
1059
1409
  for (let index = 0; index < this.layers.length; index += 1) {
1060
1410
  const layer = this.layers[index];
1061
1411
  if (!layer) continue;
1412
+ const readStart = performance.now();
1062
1413
  const stored = await this.readLayerEntry(layer, key);
1414
+ const readDuration = performance.now() - readStart;
1415
+ this.metricsCollector.recordLatency(layer.name, readDuration);
1063
1416
  if (stored === null) {
1064
1417
  this.metricsCollector.incrementLayer("missesByLayer", layer.name);
1065
1418
  continue;
@@ -1134,33 +1487,28 @@ var CacheStack = class extends EventEmitter {
1134
1487
  }
1135
1488
  async writeAcrossLayers(key, kind, value, options) {
1136
1489
  const now = Date.now();
1137
- const operations = this.layers.map((layer) => async () => {
1138
- if (this.shouldSkipLayer(layer)) {
1139
- return;
1140
- }
1141
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1142
- const staleWhileRevalidate = this.resolveLayerSeconds(
1143
- layer.name,
1144
- options?.staleWhileRevalidate,
1145
- this.options.staleWhileRevalidate
1146
- );
1147
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1148
- const payload = createStoredValueEnvelope({
1149
- kind,
1150
- value,
1151
- freshTtlSeconds: freshTtl,
1152
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1153
- staleIfErrorSeconds: staleIfError,
1154
- now
1155
- });
1156
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1157
- try {
1158
- await layer.set(key, payload, ttl);
1159
- } catch (error) {
1160
- await this.handleLayerFailure(layer, "write", error);
1490
+ const immediateOperations = [];
1491
+ const deferredOperations = [];
1492
+ for (const layer of this.layers) {
1493
+ const operation = async () => {
1494
+ if (this.shouldSkipLayer(layer)) {
1495
+ return;
1496
+ }
1497
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1498
+ try {
1499
+ await layer.set(entry.key, entry.value, entry.ttl);
1500
+ } catch (error) {
1501
+ await this.handleLayerFailure(layer, "write", error);
1502
+ }
1503
+ };
1504
+ if (this.shouldWriteBehind(layer)) {
1505
+ deferredOperations.push(operation);
1506
+ } else {
1507
+ immediateOperations.push(operation);
1161
1508
  }
1162
- });
1163
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1509
+ }
1510
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1511
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1164
1512
  }
1165
1513
  async executeLayerOperations(operations, context) {
1166
1514
  if (this.options.writePolicy !== "best-effort") {
@@ -1184,8 +1532,17 @@ var CacheStack = class extends EventEmitter {
1184
1532
  );
1185
1533
  }
1186
1534
  }
1187
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1188
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1535
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1536
+ return this.ttlResolver.resolveFreshTtl(
1537
+ key,
1538
+ layerName,
1539
+ kind,
1540
+ options,
1541
+ fallbackTtl,
1542
+ this.options.negativeTtl,
1543
+ void 0,
1544
+ value
1545
+ );
1189
1546
  }
1190
1547
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1191
1548
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1261,6 +1618,12 @@ var CacheStack = class extends EventEmitter {
1261
1618
  }
1262
1619
  }
1263
1620
  }
1621
+ async getTagsForKey(key) {
1622
+ if (this.tagIndex.tagsForKey) {
1623
+ return this.tagIndex.tagsForKey(key);
1624
+ }
1625
+ return [];
1626
+ }
1264
1627
  formatError(error) {
1265
1628
  if (error instanceof Error) {
1266
1629
  return error.message;
@@ -1273,11 +1636,110 @@ var CacheStack = class extends EventEmitter {
1273
1636
  shouldBroadcastL1Invalidation() {
1274
1637
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1275
1638
  }
1276
- async deleteKeysFromLayers(layers, keys) {
1277
- await Promise.all(
1278
- layers.map(async (layer) => {
1279
- if (this.shouldSkipLayer(layer)) {
1280
- return;
1639
+ initializeWriteBehind(options) {
1640
+ if (this.options.writeStrategy !== "write-behind") {
1641
+ return;
1642
+ }
1643
+ const flushIntervalMs = options?.flushIntervalMs;
1644
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1645
+ return;
1646
+ }
1647
+ this.writeBehindTimer = setInterval(() => {
1648
+ void this.flushWriteBehindQueue();
1649
+ }, flushIntervalMs);
1650
+ this.writeBehindTimer.unref?.();
1651
+ }
1652
+ shouldWriteBehind(layer) {
1653
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
1654
+ }
1655
+ async enqueueWriteBehind(operation) {
1656
+ this.writeBehindQueue.push(operation);
1657
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1658
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
1659
+ if (this.writeBehindQueue.length >= batchSize) {
1660
+ await this.flushWriteBehindQueue();
1661
+ return;
1662
+ }
1663
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1664
+ await this.flushWriteBehindQueue();
1665
+ }
1666
+ }
1667
+ async flushWriteBehindQueue() {
1668
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1669
+ await this.writeBehindFlushPromise;
1670
+ return;
1671
+ }
1672
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1673
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1674
+ this.writeBehindFlushPromise = (async () => {
1675
+ await Promise.allSettled(batch.map((operation) => operation()));
1676
+ })();
1677
+ await this.writeBehindFlushPromise;
1678
+ this.writeBehindFlushPromise = void 0;
1679
+ if (this.writeBehindQueue.length > 0) {
1680
+ await this.flushWriteBehindQueue();
1681
+ }
1682
+ }
1683
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
1684
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
1685
+ const staleWhileRevalidate = this.resolveLayerSeconds(
1686
+ layer.name,
1687
+ options?.staleWhileRevalidate,
1688
+ this.options.staleWhileRevalidate
1689
+ );
1690
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1691
+ const payload = createStoredValueEnvelope({
1692
+ kind,
1693
+ value,
1694
+ freshTtlSeconds: freshTtl,
1695
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
1696
+ staleIfErrorSeconds: staleIfError,
1697
+ now
1698
+ });
1699
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1700
+ return {
1701
+ key,
1702
+ value: payload,
1703
+ ttl
1704
+ };
1705
+ }
1706
+ intersectKeys(groups) {
1707
+ if (groups.length === 0) {
1708
+ return [];
1709
+ }
1710
+ const [firstGroup, ...rest] = groups;
1711
+ if (!firstGroup) {
1712
+ return [];
1713
+ }
1714
+ const restSets = rest.map((group) => new Set(group));
1715
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
1716
+ }
1717
+ qualifyKey(key) {
1718
+ const prefix = this.generationPrefix();
1719
+ return prefix ? `${prefix}${key}` : key;
1720
+ }
1721
+ qualifyPattern(pattern) {
1722
+ const prefix = this.generationPrefix();
1723
+ return prefix ? `${prefix}${pattern}` : pattern;
1724
+ }
1725
+ stripQualifiedKey(key) {
1726
+ const prefix = this.generationPrefix();
1727
+ if (!prefix || !key.startsWith(prefix)) {
1728
+ return key;
1729
+ }
1730
+ return key.slice(prefix.length);
1731
+ }
1732
+ generationPrefix() {
1733
+ if (this.currentGeneration === void 0) {
1734
+ return "";
1735
+ }
1736
+ return `v${this.currentGeneration}:`;
1737
+ }
1738
+ async deleteKeysFromLayers(layers, keys) {
1739
+ await Promise.all(
1740
+ layers.map(async (layer) => {
1741
+ if (this.shouldSkipLayer(layer)) {
1742
+ return;
1281
1743
  }
1282
1744
  if (layer.deleteMany) {
1283
1745
  try {
@@ -1316,6 +1778,9 @@ var CacheStack = class extends EventEmitter {
1316
1778
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1317
1779
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1318
1780
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
1781
+ if (this.options.generation !== void 0) {
1782
+ this.validateNonNegativeNumber("generation", this.options.generation);
1783
+ }
1319
1784
  }
1320
1785
  validateWriteOptions(options) {
1321
1786
  if (!options) {
@@ -1327,6 +1792,7 @@ var CacheStack = class extends EventEmitter {
1327
1792
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1328
1793
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1329
1794
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
1795
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1330
1796
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1331
1797
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1332
1798
  }
@@ -1370,6 +1836,26 @@ var CacheStack = class extends EventEmitter {
1370
1836
  }
1371
1837
  return key;
1372
1838
  }
1839
+ validateTtlPolicy(name, policy) {
1840
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
1841
+ return;
1842
+ }
1843
+ if ("alignTo" in policy) {
1844
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
1845
+ return;
1846
+ }
1847
+ throw new Error(`${name} is invalid.`);
1848
+ }
1849
+ assertActive(operation) {
1850
+ if (this.isDisconnecting) {
1851
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
1852
+ }
1853
+ }
1854
+ async awaitStartup(operation) {
1855
+ this.assertActive(operation);
1856
+ await this.startup;
1857
+ this.assertActive(operation);
1858
+ }
1373
1859
  serializeOptions(options) {
1374
1860
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1375
1861
  }
@@ -1475,41 +1961,47 @@ var CacheStack = class extends EventEmitter {
1475
1961
  return value;
1476
1962
  }
1477
1963
  };
1964
+ function createInstanceId() {
1965
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1966
+ }
1478
1967
 
1479
1968
  // src/invalidation/RedisInvalidationBus.ts
1480
1969
  var RedisInvalidationBus = class {
1481
1970
  channel;
1482
1971
  publisher;
1483
1972
  subscriber;
1484
- activeListener;
1973
+ logger;
1974
+ handlers = /* @__PURE__ */ new Set();
1975
+ sharedListener;
1485
1976
  constructor(options) {
1486
1977
  this.publisher = options.publisher;
1487
1978
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1488
1979
  this.channel = options.channel ?? "layercache:invalidation";
1980
+ this.logger = options.logger;
1489
1981
  }
1490
1982
  async subscribe(handler) {
1491
- if (this.activeListener) {
1492
- throw new Error("RedisInvalidationBus already has an active subscription.");
1983
+ if (this.handlers.size === 0) {
1984
+ const listener = (_channel, payload) => {
1985
+ void this.dispatchToHandlers(payload);
1986
+ };
1987
+ this.sharedListener = listener;
1988
+ this.subscriber.on("message", listener);
1989
+ await this.subscriber.subscribe(this.channel);
1493
1990
  }
1494
- const listener = (_channel, payload) => {
1495
- void this.handleMessage(payload, handler);
1496
- };
1497
- this.activeListener = listener;
1498
- this.subscriber.on("message", listener);
1499
- await this.subscriber.subscribe(this.channel);
1991
+ this.handlers.add(handler);
1500
1992
  return async () => {
1501
- if (this.activeListener !== listener) {
1502
- return;
1993
+ this.handlers.delete(handler);
1994
+ if (this.handlers.size === 0 && this.sharedListener) {
1995
+ this.subscriber.off("message", this.sharedListener);
1996
+ this.sharedListener = void 0;
1997
+ await this.subscriber.unsubscribe(this.channel);
1503
1998
  }
1504
- this.activeListener = void 0;
1505
- this.subscriber.off("message", listener);
1506
- await this.subscriber.unsubscribe(this.channel);
1507
1999
  };
1508
2000
  }
1509
2001
  async publish(message) {
1510
2002
  await this.publisher.publish(this.channel, JSON.stringify(message));
1511
2003
  }
1512
- async handleMessage(payload, handler) {
2004
+ async dispatchToHandlers(payload) {
1513
2005
  let message;
1514
2006
  try {
1515
2007
  const parsed = JSON.parse(payload);
@@ -1521,11 +2013,15 @@ var RedisInvalidationBus = class {
1521
2013
  this.reportError("invalid invalidation payload", error);
1522
2014
  return;
1523
2015
  }
1524
- try {
1525
- await handler(message);
1526
- } catch (error) {
1527
- this.reportError("invalidation handler failed", error);
1528
- }
2016
+ await Promise.all(
2017
+ [...this.handlers].map(async (handler) => {
2018
+ try {
2019
+ await handler(message);
2020
+ } catch (error) {
2021
+ this.reportError("invalidation handler failed", error);
2022
+ }
2023
+ })
2024
+ );
1529
2025
  }
1530
2026
  isInvalidationMessage(value) {
1531
2027
  if (!value || typeof value !== "object") {
@@ -1538,6 +2034,10 @@ var RedisInvalidationBus = class {
1538
2034
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1539
2035
  }
1540
2036
  reportError(message, error) {
2037
+ if (this.logger?.error) {
2038
+ this.logger.error(message, { error });
2039
+ return;
2040
+ }
1541
2041
  console.error(`[layercache] ${message}`, error);
1542
2042
  }
1543
2043
  };
@@ -1586,6 +2086,43 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1586
2086
  };
1587
2087
  }
1588
2088
 
2089
+ // src/integrations/express.ts
2090
+ function createExpressCacheMiddleware(cache, options = {}) {
2091
+ const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
2092
+ return async (req, res, next) => {
2093
+ try {
2094
+ const method = (req.method ?? "GET").toUpperCase();
2095
+ if (!allowedMethods.has(method)) {
2096
+ next();
2097
+ return;
2098
+ }
2099
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
2100
+ const cached = await cache.get(key, void 0, options);
2101
+ if (cached !== null) {
2102
+ res.setHeader?.("content-type", "application/json; charset=utf-8");
2103
+ res.setHeader?.("x-cache", "HIT");
2104
+ if (res.json) {
2105
+ res.json(cached);
2106
+ } else {
2107
+ res.end?.(JSON.stringify(cached));
2108
+ }
2109
+ return;
2110
+ }
2111
+ const originalJson = res.json?.bind(res);
2112
+ if (originalJson) {
2113
+ res.json = (body) => {
2114
+ res.setHeader?.("x-cache", "MISS");
2115
+ void cache.set(key, body, options);
2116
+ return originalJson(body);
2117
+ };
2118
+ }
2119
+ next();
2120
+ } catch (error) {
2121
+ next(error);
2122
+ }
2123
+ };
2124
+ }
2125
+
1589
2126
  // src/integrations/graphql.ts
1590
2127
  function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1591
2128
  const wrapped = cache.wrap(prefix, resolver, {
@@ -1595,6 +2132,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1595
2132
  return (...args) => wrapped(...args);
1596
2133
  }
1597
2134
 
2135
+ // src/integrations/opentelemetry.ts
2136
+ function createOpenTelemetryPlugin(cache, tracer) {
2137
+ const originals = {
2138
+ get: cache.get.bind(cache),
2139
+ set: cache.set.bind(cache),
2140
+ delete: cache.delete.bind(cache),
2141
+ mget: cache.mget.bind(cache),
2142
+ mset: cache.mset.bind(cache),
2143
+ invalidateByTag: cache.invalidateByTag.bind(cache),
2144
+ invalidateByTags: cache.invalidateByTags.bind(cache),
2145
+ invalidateByPattern: cache.invalidateByPattern.bind(cache),
2146
+ invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
2147
+ };
2148
+ cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
2149
+ "layercache.key": String(args[0] ?? "")
2150
+ }));
2151
+ cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
2152
+ "layercache.key": String(args[0] ?? "")
2153
+ }));
2154
+ cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
2155
+ "layercache.key": String(args[0] ?? "")
2156
+ }));
2157
+ cache.mget = instrument("layercache.mget", tracer, originals.mget);
2158
+ cache.mset = instrument("layercache.mset", tracer, originals.mset);
2159
+ cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
2160
+ cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
2161
+ cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
2162
+ cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
2163
+ return {
2164
+ uninstall() {
2165
+ cache.get = originals.get;
2166
+ cache.set = originals.set;
2167
+ cache.delete = originals.delete;
2168
+ cache.mget = originals.mget;
2169
+ cache.mset = originals.mset;
2170
+ cache.invalidateByTag = originals.invalidateByTag;
2171
+ cache.invalidateByTags = originals.invalidateByTags;
2172
+ cache.invalidateByPattern = originals.invalidateByPattern;
2173
+ cache.invalidateByPrefix = originals.invalidateByPrefix;
2174
+ }
2175
+ };
2176
+ }
2177
+ function instrument(name, tracer, method, attributes) {
2178
+ return (async (...args) => {
2179
+ const span = tracer.startSpan(name, { attributes: attributes?.(args) });
2180
+ try {
2181
+ const result = await method(...args);
2182
+ span.setAttribute?.("layercache.success", true);
2183
+ if (result === null) {
2184
+ span.setAttribute?.("layercache.result", "null");
2185
+ }
2186
+ return result;
2187
+ } catch (error) {
2188
+ span.setAttribute?.("layercache.success", false);
2189
+ span.recordException?.(error);
2190
+ throw error;
2191
+ } finally {
2192
+ span.end();
2193
+ }
2194
+ });
2195
+ }
2196
+
1598
2197
  // src/integrations/trpc.ts
1599
2198
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1600
2199
  return async (context) => {
@@ -1620,165 +2219,9 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1620
2219
  };
1621
2220
  }
1622
2221
 
1623
- // src/layers/MemoryLayer.ts
1624
- var MemoryLayer = class {
1625
- name;
1626
- defaultTtl;
1627
- isLocal = true;
1628
- maxSize;
1629
- evictionPolicy;
1630
- entries = /* @__PURE__ */ new Map();
1631
- constructor(options = {}) {
1632
- this.name = options.name ?? "memory";
1633
- this.defaultTtl = options.ttl;
1634
- this.maxSize = options.maxSize ?? 1e3;
1635
- this.evictionPolicy = options.evictionPolicy ?? "lru";
1636
- }
1637
- async get(key) {
1638
- const value = await this.getEntry(key);
1639
- return unwrapStoredValue(value);
1640
- }
1641
- async getEntry(key) {
1642
- const entry = this.entries.get(key);
1643
- if (!entry) {
1644
- return null;
1645
- }
1646
- if (this.isExpired(entry)) {
1647
- this.entries.delete(key);
1648
- return null;
1649
- }
1650
- if (this.evictionPolicy === "lru") {
1651
- this.entries.delete(key);
1652
- entry.frequency += 1;
1653
- this.entries.set(key, entry);
1654
- } else {
1655
- entry.frequency += 1;
1656
- }
1657
- return entry.value;
1658
- }
1659
- async getMany(keys) {
1660
- const values = [];
1661
- for (const key of keys) {
1662
- values.push(await this.getEntry(key));
1663
- }
1664
- return values;
1665
- }
1666
- async set(key, value, ttl = this.defaultTtl) {
1667
- this.entries.delete(key);
1668
- this.entries.set(key, {
1669
- value,
1670
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
1671
- frequency: 0,
1672
- insertedAt: Date.now()
1673
- });
1674
- while (this.entries.size > this.maxSize) {
1675
- this.evict();
1676
- }
1677
- }
1678
- async has(key) {
1679
- const entry = this.entries.get(key);
1680
- if (!entry) {
1681
- return false;
1682
- }
1683
- if (this.isExpired(entry)) {
1684
- this.entries.delete(key);
1685
- return false;
1686
- }
1687
- return true;
1688
- }
1689
- async ttl(key) {
1690
- const entry = this.entries.get(key);
1691
- if (!entry) {
1692
- return null;
1693
- }
1694
- if (this.isExpired(entry)) {
1695
- this.entries.delete(key);
1696
- return null;
1697
- }
1698
- if (entry.expiresAt === null) {
1699
- return null;
1700
- }
1701
- return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
1702
- }
1703
- async size() {
1704
- this.pruneExpired();
1705
- return this.entries.size;
1706
- }
1707
- async delete(key) {
1708
- this.entries.delete(key);
1709
- }
1710
- async deleteMany(keys) {
1711
- for (const key of keys) {
1712
- this.entries.delete(key);
1713
- }
1714
- }
1715
- async clear() {
1716
- this.entries.clear();
1717
- }
1718
- async keys() {
1719
- this.pruneExpired();
1720
- return [...this.entries.keys()];
1721
- }
1722
- exportState() {
1723
- this.pruneExpired();
1724
- return [...this.entries.entries()].map(([key, entry]) => ({
1725
- key,
1726
- value: entry.value,
1727
- expiresAt: entry.expiresAt
1728
- }));
1729
- }
1730
- importState(entries) {
1731
- for (const entry of entries) {
1732
- if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
1733
- continue;
1734
- }
1735
- this.entries.set(entry.key, {
1736
- value: entry.value,
1737
- expiresAt: entry.expiresAt,
1738
- frequency: 0,
1739
- insertedAt: Date.now()
1740
- });
1741
- }
1742
- while (this.entries.size > this.maxSize) {
1743
- this.evict();
1744
- }
1745
- }
1746
- evict() {
1747
- if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
1748
- const oldestKey = this.entries.keys().next().value;
1749
- if (oldestKey !== void 0) {
1750
- this.entries.delete(oldestKey);
1751
- }
1752
- return;
1753
- }
1754
- let victimKey;
1755
- let minFreq = Number.POSITIVE_INFINITY;
1756
- let minInsertedAt = Number.POSITIVE_INFINITY;
1757
- for (const [key, entry] of this.entries.entries()) {
1758
- if (entry.frequency < minFreq || entry.frequency === minFreq && entry.insertedAt < minInsertedAt) {
1759
- minFreq = entry.frequency;
1760
- minInsertedAt = entry.insertedAt;
1761
- victimKey = key;
1762
- }
1763
- }
1764
- if (victimKey !== void 0) {
1765
- this.entries.delete(victimKey);
1766
- }
1767
- }
1768
- pruneExpired() {
1769
- for (const [key, entry] of this.entries.entries()) {
1770
- if (this.isExpired(entry)) {
1771
- this.entries.delete(key);
1772
- }
1773
- }
1774
- }
1775
- isExpired(entry) {
1776
- return entry.expiresAt !== null && entry.expiresAt <= Date.now();
1777
- }
1778
- };
1779
-
1780
2222
  // src/layers/RedisLayer.ts
1781
- import { brotliCompressSync, brotliDecompressSync, gunzipSync, gzipSync } from "zlib";
2223
+ import { promisify } from "util";
2224
+ import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
1782
2225
 
1783
2226
  // src/serialization/JsonSerializer.ts
1784
2227
  var JsonSerializer = class {
@@ -1793,27 +2236,33 @@ var JsonSerializer = class {
1793
2236
 
1794
2237
  // src/layers/RedisLayer.ts
1795
2238
  var BATCH_DELETE_SIZE = 500;
2239
+ var gzipAsync = promisify(gzip);
2240
+ var gunzipAsync = promisify(gunzip);
2241
+ var brotliCompressAsync = promisify(brotliCompress);
2242
+ var brotliDecompressAsync = promisify(brotliDecompress);
1796
2243
  var RedisLayer = class {
1797
2244
  name;
1798
2245
  defaultTtl;
1799
2246
  isLocal = false;
1800
2247
  client;
1801
- serializer;
2248
+ serializers;
1802
2249
  prefix;
1803
2250
  allowUnprefixedClear;
1804
2251
  scanCount;
1805
2252
  compression;
1806
2253
  compressionThreshold;
2254
+ disconnectOnDispose;
1807
2255
  constructor(options) {
1808
2256
  this.client = options.client;
1809
2257
  this.defaultTtl = options.ttl;
1810
2258
  this.name = options.name ?? "redis";
1811
- this.serializer = options.serializer ?? new JsonSerializer();
2259
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
1812
2260
  this.prefix = options.prefix ?? "";
1813
2261
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
1814
2262
  this.scanCount = options.scanCount ?? 100;
1815
2263
  this.compression = options.compression;
1816
2264
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2265
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
1817
2266
  }
1818
2267
  async get(key) {
1819
2268
  const payload = await this.getEntry(key);
@@ -1848,8 +2297,26 @@ var RedisLayer = class {
1848
2297
  })
1849
2298
  );
1850
2299
  }
2300
+ async setMany(entries) {
2301
+ if (entries.length === 0) {
2302
+ return;
2303
+ }
2304
+ const pipeline = this.client.pipeline();
2305
+ for (const entry of entries) {
2306
+ const serialized = this.primarySerializer().serialize(entry.value);
2307
+ const payload = await this.encodePayload(serialized);
2308
+ const normalizedKey = this.withPrefix(entry.key);
2309
+ if (entry.ttl && entry.ttl > 0) {
2310
+ pipeline.set(normalizedKey, payload, "EX", entry.ttl);
2311
+ } else {
2312
+ pipeline.set(normalizedKey, payload);
2313
+ }
2314
+ }
2315
+ await pipeline.exec();
2316
+ }
1851
2317
  async set(key, value, ttl = this.defaultTtl) {
1852
- const payload = this.encodePayload(this.serializer.serialize(value));
2318
+ const serialized = this.primarySerializer().serialize(value);
2319
+ const payload = await this.encodePayload(serialized);
1853
2320
  const normalizedKey = this.withPrefix(key);
1854
2321
  if (ttl && ttl > 0) {
1855
2322
  await this.client.set(normalizedKey, payload, "EX", ttl);
@@ -1881,6 +2348,18 @@ var RedisLayer = class {
1881
2348
  const keys = await this.keys();
1882
2349
  return keys.length;
1883
2350
  }
2351
+ async ping() {
2352
+ try {
2353
+ return await this.client.ping() === "PONG";
2354
+ } catch {
2355
+ return false;
2356
+ }
2357
+ }
2358
+ async dispose() {
2359
+ if (this.disconnectOnDispose) {
2360
+ this.client.disconnect();
2361
+ }
2362
+ }
1884
2363
  /**
1885
2364
  * Deletes all keys matching the layer's prefix in batches to avoid
1886
2365
  * loading millions of keys into memory at once.
@@ -1927,17 +2406,48 @@ var RedisLayer = class {
1927
2406
  return `${this.prefix}${key}`;
1928
2407
  }
1929
2408
  async deserializeOrDelete(key, payload) {
2409
+ const decodedPayload = await this.decodePayload(payload);
2410
+ for (const serializer of this.serializers) {
2411
+ try {
2412
+ const value = serializer.deserialize(decodedPayload);
2413
+ if (serializer !== this.primarySerializer()) {
2414
+ await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
2415
+ }
2416
+ return value;
2417
+ } catch {
2418
+ }
2419
+ }
1930
2420
  try {
1931
- return this.serializer.deserialize(this.decodePayload(payload));
1932
- } catch {
1933
2421
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
1934
- return null;
2422
+ } catch {
1935
2423
  }
2424
+ return null;
2425
+ }
2426
+ async rewriteWithPrimarySerializer(key, value) {
2427
+ const serialized = this.primarySerializer().serialize(value);
2428
+ const payload = await this.encodePayload(serialized);
2429
+ const ttl = await this.client.ttl(this.withPrefix(key));
2430
+ if (ttl > 0) {
2431
+ await this.client.set(this.withPrefix(key), payload, "EX", ttl);
2432
+ return;
2433
+ }
2434
+ await this.client.set(this.withPrefix(key), payload);
2435
+ }
2436
+ primarySerializer() {
2437
+ const serializer = this.serializers[0];
2438
+ if (!serializer) {
2439
+ throw new Error("RedisLayer requires at least one serializer.");
2440
+ }
2441
+ return serializer;
1936
2442
  }
1937
2443
  isSerializablePayload(payload) {
1938
2444
  return typeof payload === "string" || Buffer.isBuffer(payload);
1939
2445
  }
1940
- encodePayload(payload) {
2446
+ /**
2447
+ * Compresses the payload asynchronously if compression is enabled and the
2448
+ * payload exceeds the threshold. This avoids blocking the event loop.
2449
+ */
2450
+ async encodePayload(payload) {
1941
2451
  if (!this.compression) {
1942
2452
  return payload;
1943
2453
  }
@@ -1946,18 +2456,21 @@ var RedisLayer = class {
1946
2456
  return payload;
1947
2457
  }
1948
2458
  const header = Buffer.from(`LCZ1:${this.compression}:`);
1949
- const compressed = this.compression === "gzip" ? gzipSync(source) : brotliCompressSync(source);
2459
+ const compressed = this.compression === "gzip" ? await gzipAsync(source) : await brotliCompressAsync(source);
1950
2460
  return Buffer.concat([header, compressed]);
1951
2461
  }
1952
- decodePayload(payload) {
2462
+ /**
2463
+ * Decompresses the payload asynchronously if a compression header is present.
2464
+ */
2465
+ async decodePayload(payload) {
1953
2466
  if (!Buffer.isBuffer(payload)) {
1954
2467
  return payload;
1955
2468
  }
1956
2469
  if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
1957
- return gunzipSync(payload.subarray(10));
2470
+ return gunzipAsync(payload.subarray(10));
1958
2471
  }
1959
2472
  if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
1960
- return brotliDecompressSync(payload.subarray(12));
2473
+ return brotliDecompressAsync(payload.subarray(12));
1961
2474
  }
1962
2475
  return payload;
1963
2476
  }
@@ -1965,7 +2478,7 @@ var RedisLayer = class {
1965
2478
 
1966
2479
  // src/layers/DiskLayer.ts
1967
2480
  import { createHash } from "crypto";
1968
- import { promises as fs2 } from "fs";
2481
+ import { promises as fs } from "fs";
1969
2482
  import { join } from "path";
1970
2483
  var DiskLayer = class {
1971
2484
  name;
@@ -1973,11 +2486,14 @@ var DiskLayer = class {
1973
2486
  isLocal = true;
1974
2487
  directory;
1975
2488
  serializer;
2489
+ maxFiles;
2490
+ writeQueue = Promise.resolve();
1976
2491
  constructor(options) {
1977
2492
  this.directory = options.directory;
1978
2493
  this.defaultTtl = options.ttl;
1979
2494
  this.name = options.name ?? "disk";
1980
2495
  this.serializer = options.serializer ?? new JsonSerializer();
2496
+ this.maxFiles = options.maxFiles;
1981
2497
  }
1982
2498
  async get(key) {
1983
2499
  return unwrapStoredValue(await this.getEntry(key));
@@ -1986,7 +2502,7 @@ var DiskLayer = class {
1986
2502
  const filePath = this.keyToPath(key);
1987
2503
  let raw;
1988
2504
  try {
1989
- raw = await fs2.readFile(filePath);
2505
+ raw = await fs.readFile(filePath);
1990
2506
  } catch {
1991
2507
  return null;
1992
2508
  }
@@ -2004,13 +2520,30 @@ var DiskLayer = class {
2004
2520
  return entry.value;
2005
2521
  }
2006
2522
  async set(key, value, ttl = this.defaultTtl) {
2007
- await fs2.mkdir(this.directory, { recursive: true });
2008
- const entry = {
2009
- value,
2010
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2011
- };
2012
- const payload = this.serializer.serialize(entry);
2013
- await fs2.writeFile(this.keyToPath(key), payload);
2523
+ await this.enqueueWrite(async () => {
2524
+ await fs.mkdir(this.directory, { recursive: true });
2525
+ const entry = {
2526
+ key,
2527
+ value,
2528
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2529
+ };
2530
+ const payload = this.serializer.serialize(entry);
2531
+ const targetPath = this.keyToPath(key);
2532
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
2533
+ await fs.writeFile(tempPath, payload);
2534
+ await fs.rename(tempPath, targetPath);
2535
+ if (this.maxFiles !== void 0) {
2536
+ await this.enforceMaxFiles();
2537
+ }
2538
+ });
2539
+ }
2540
+ async getMany(keys) {
2541
+ return Promise.all(keys.map((key) => this.getEntry(key)));
2542
+ }
2543
+ async setMany(entries) {
2544
+ for (const entry of entries) {
2545
+ await this.set(entry.key, entry.value, entry.ttl);
2546
+ }
2014
2547
  }
2015
2548
  async has(key) {
2016
2549
  const value = await this.getEntry(key);
@@ -2020,7 +2553,7 @@ var DiskLayer = class {
2020
2553
  const filePath = this.keyToPath(key);
2021
2554
  let raw;
2022
2555
  try {
2023
- raw = await fs2.readFile(filePath);
2556
+ raw = await fs.readFile(filePath);
2024
2557
  } catch {
2025
2558
  return null;
2026
2559
  }
@@ -2040,45 +2573,125 @@ var DiskLayer = class {
2040
2573
  return remaining;
2041
2574
  }
2042
2575
  async delete(key) {
2043
- await this.safeDelete(this.keyToPath(key));
2576
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2044
2577
  }
2045
2578
  async deleteMany(keys) {
2046
- await Promise.all(keys.map((key) => this.delete(key)));
2579
+ await this.enqueueWrite(async () => {
2580
+ await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
2581
+ });
2047
2582
  }
2048
2583
  async clear() {
2049
- let entries;
2050
- try {
2051
- entries = await fs2.readdir(this.directory);
2052
- } catch {
2053
- return;
2054
- }
2055
- await Promise.all(
2056
- entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
2057
- );
2584
+ await this.enqueueWrite(async () => {
2585
+ let entries;
2586
+ try {
2587
+ entries = await fs.readdir(this.directory);
2588
+ } catch {
2589
+ return;
2590
+ }
2591
+ await Promise.all(
2592
+ entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
2593
+ );
2594
+ });
2058
2595
  }
2596
+ /**
2597
+ * Returns the original cache key strings stored on disk.
2598
+ * Expired entries are skipped and cleaned up during the scan.
2599
+ */
2059
2600
  async keys() {
2060
2601
  let entries;
2061
2602
  try {
2062
- entries = await fs2.readdir(this.directory);
2603
+ entries = await fs.readdir(this.directory);
2063
2604
  } catch {
2064
2605
  return [];
2065
2606
  }
2066
- return entries.filter((name) => name.endsWith(".lc")).map((name) => name.slice(0, -3));
2607
+ const lcFiles = entries.filter((name) => name.endsWith(".lc"));
2608
+ const keys = [];
2609
+ await Promise.all(
2610
+ lcFiles.map(async (name) => {
2611
+ const filePath = join(this.directory, name);
2612
+ let raw;
2613
+ try {
2614
+ raw = await fs.readFile(filePath);
2615
+ } catch {
2616
+ return;
2617
+ }
2618
+ let entry;
2619
+ try {
2620
+ entry = this.serializer.deserialize(raw);
2621
+ } catch {
2622
+ await this.safeDelete(filePath);
2623
+ return;
2624
+ }
2625
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
2626
+ await this.safeDelete(filePath);
2627
+ return;
2628
+ }
2629
+ keys.push(entry.key);
2630
+ })
2631
+ );
2632
+ return keys;
2067
2633
  }
2068
2634
  async size() {
2069
2635
  const keys = await this.keys();
2070
2636
  return keys.length;
2071
2637
  }
2638
+ async ping() {
2639
+ try {
2640
+ await fs.mkdir(this.directory, { recursive: true });
2641
+ return true;
2642
+ } catch {
2643
+ return false;
2644
+ }
2645
+ }
2646
+ async dispose() {
2647
+ }
2072
2648
  keyToPath(key) {
2073
2649
  const hash = createHash("sha256").update(key).digest("hex");
2074
2650
  return join(this.directory, `${hash}.lc`);
2075
2651
  }
2076
2652
  async safeDelete(filePath) {
2077
2653
  try {
2078
- await fs2.unlink(filePath);
2654
+ await fs.unlink(filePath);
2079
2655
  } catch {
2080
2656
  }
2081
2657
  }
2658
+ enqueueWrite(operation) {
2659
+ const next = this.writeQueue.then(operation, operation);
2660
+ this.writeQueue = next.catch(() => void 0);
2661
+ return next;
2662
+ }
2663
+ /**
2664
+ * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
2665
+ */
2666
+ async enforceMaxFiles() {
2667
+ if (this.maxFiles === void 0) {
2668
+ return;
2669
+ }
2670
+ let entries;
2671
+ try {
2672
+ entries = await fs.readdir(this.directory);
2673
+ } catch {
2674
+ return;
2675
+ }
2676
+ const lcFiles = entries.filter((name) => name.endsWith(".lc"));
2677
+ if (lcFiles.length <= this.maxFiles) {
2678
+ return;
2679
+ }
2680
+ const withStats = await Promise.all(
2681
+ lcFiles.map(async (name) => {
2682
+ const filePath = join(this.directory, name);
2683
+ try {
2684
+ const stat = await fs.stat(filePath);
2685
+ return { filePath, mtimeMs: stat.mtimeMs };
2686
+ } catch {
2687
+ return { filePath, mtimeMs: 0 };
2688
+ }
2689
+ })
2690
+ );
2691
+ withStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
2692
+ const toEvict = withStats.slice(0, lcFiles.length - this.maxFiles);
2693
+ await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
2694
+ }
2082
2695
  };
2083
2696
 
2084
2697
  // src/layers/MemcachedLayer.ts
@@ -2088,29 +2701,41 @@ var MemcachedLayer = class {
2088
2701
  isLocal = false;
2089
2702
  client;
2090
2703
  keyPrefix;
2704
+ serializer;
2091
2705
  constructor(options) {
2092
2706
  this.client = options.client;
2093
2707
  this.defaultTtl = options.ttl;
2094
2708
  this.name = options.name ?? "memcached";
2095
2709
  this.keyPrefix = options.keyPrefix ?? "";
2710
+ this.serializer = options.serializer ?? new JsonSerializer();
2096
2711
  }
2097
2712
  async get(key) {
2713
+ return unwrapStoredValue(await this.getEntry(key));
2714
+ }
2715
+ async getEntry(key) {
2098
2716
  const result = await this.client.get(this.withPrefix(key));
2099
2717
  if (!result || result.value === null) {
2100
2718
  return null;
2101
2719
  }
2102
2720
  try {
2103
- return JSON.parse(result.value.toString("utf8"));
2721
+ return this.serializer.deserialize(result.value);
2104
2722
  } catch {
2105
2723
  return null;
2106
2724
  }
2107
2725
  }
2726
+ async getMany(keys) {
2727
+ return Promise.all(keys.map((key) => this.getEntry(key)));
2728
+ }
2108
2729
  async set(key, value, ttl = this.defaultTtl) {
2109
- const payload = JSON.stringify(value);
2730
+ const payload = this.serializer.serialize(value);
2110
2731
  await this.client.set(this.withPrefix(key), payload, {
2111
2732
  expires: ttl && ttl > 0 ? ttl : void 0
2112
2733
  });
2113
2734
  }
2735
+ async has(key) {
2736
+ const result = await this.client.get(this.withPrefix(key));
2737
+ return result !== null && result.value !== null;
2738
+ }
2114
2739
  async delete(key) {
2115
2740
  await this.client.delete(this.withPrefix(key));
2116
2741
  }
@@ -2140,7 +2765,7 @@ var MsgpackSerializer = class {
2140
2765
  };
2141
2766
 
2142
2767
  // src/singleflight/RedisSingleFlightCoordinator.ts
2143
- import { randomUUID as randomUUID2 } from "crypto";
2768
+ import { randomUUID } from "crypto";
2144
2769
  var RELEASE_SCRIPT = `
2145
2770
  if redis.call("get", KEYS[1]) == ARGV[1] then
2146
2771
  return redis.call("del", KEYS[1])
@@ -2156,7 +2781,7 @@ var RedisSingleFlightCoordinator = class {
2156
2781
  }
2157
2782
  async execute(key, options, worker, waiter) {
2158
2783
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2159
- const token = randomUUID2();
2784
+ const token = randomUUID();
2160
2785
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2161
2786
  if (acquired === "OK") {
2162
2787
  try {
@@ -2204,6 +2829,12 @@ function createPrometheusMetricsExporter(stacks) {
2204
2829
  lines.push("# TYPE layercache_hits_by_layer_total counter");
2205
2830
  lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
2206
2831
  lines.push("# TYPE layercache_misses_by_layer_total counter");
2832
+ lines.push("# HELP layercache_layer_latency_avg_ms Average read latency per layer in milliseconds");
2833
+ lines.push("# TYPE layercache_layer_latency_avg_ms gauge");
2834
+ lines.push("# HELP layercache_layer_latency_max_ms Maximum read latency per layer in milliseconds");
2835
+ lines.push("# TYPE layercache_layer_latency_max_ms gauge");
2836
+ lines.push("# HELP layercache_layer_latency_count Number of read latency samples per layer");
2837
+ lines.push("# TYPE layercache_layer_latency_count counter");
2207
2838
  for (const { stack, name } of entries) {
2208
2839
  const m = stack.getMetrics();
2209
2840
  const hr = stack.getHitRate();
@@ -2227,6 +2858,12 @@ function createPrometheusMetricsExporter(stacks) {
2227
2858
  for (const [layerName, count] of Object.entries(m.missesByLayer)) {
2228
2859
  lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
2229
2860
  }
2861
+ for (const [layerName, latency] of Object.entries(m.latencyByLayer)) {
2862
+ const layerLabel = `${label},layer="${sanitizeLabel(layerName)}"`;
2863
+ lines.push(`layercache_layer_latency_avg_ms{${layerLabel}} ${latency.avgMs.toFixed(4)}`);
2864
+ lines.push(`layercache_layer_latency_max_ms{${layerLabel}} ${latency.maxMs.toFixed(4)}`);
2865
+ lines.push(`layercache_layer_latency_count{${layerLabel}} ${latency.count}`);
2866
+ }
2230
2867
  }
2231
2868
  lines.push("");
2232
2869
  return lines.join("\n");
@@ -2236,6 +2873,7 @@ function sanitizeLabel(value) {
2236
2873
  return value.replace(/["\\\n]/g, "_");
2237
2874
  }
2238
2875
  export {
2876
+ CacheMissError,
2239
2877
  CacheNamespace,
2240
2878
  CacheStack,
2241
2879
  DiskLayer,
@@ -2253,7 +2891,10 @@ export {
2253
2891
  cacheGraphqlResolver,
2254
2892
  createCacheStatsHandler,
2255
2893
  createCachedMethodDecorator,
2894
+ createExpressCacheMiddleware,
2256
2895
  createFastifyLayercachePlugin,
2896
+ createHonoCacheMiddleware,
2897
+ createOpenTelemetryPlugin,
2257
2898
  createPrometheusMetricsExporter,
2258
2899
  createTrpcCacheMiddleware
2259
2900
  };