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.js CHANGED
@@ -1,14 +1,27 @@
1
1
  import {
2
- PatternMatcher,
3
2
  RedisTagIndex
4
- } from "./chunk-BWM4MU2X.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
24
+ import { Mutex } from "async-mutex";
12
25
  var CacheNamespace = class _CacheNamespace {
13
26
  constructor(cache, prefix) {
14
27
  this.cache = cache;
@@ -16,57 +29,69 @@ var CacheNamespace = class _CacheNamespace {
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));
24
39
  }
25
40
  /**
26
41
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
27
42
  */
28
43
  async getOrThrow(key, fetcher, options) {
29
- return this.cache.getOrThrow(this.qualify(key), fetcher, options);
44
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
30
45
  }
31
46
  async has(key) {
32
- return this.cache.has(this.qualify(key));
47
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
33
48
  }
34
49
  async ttl(key) {
35
- return this.cache.ttl(this.qualify(key));
50
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
36
51
  }
37
52
  async set(key, value, options) {
38
- await this.cache.set(this.qualify(key), value, options);
53
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
39
54
  }
40
55
  async delete(key) {
41
- await this.cache.delete(this.qualify(key));
56
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
42
57
  }
43
58
  async mdelete(keys) {
44
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
59
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
45
60
  }
46
61
  async clear() {
47
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
62
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
48
63
  }
49
64
  async mget(entries) {
50
- return this.cache.mget(
51
- entries.map((entry) => ({
52
- ...entry,
53
- key: this.qualify(entry.key)
54
- }))
65
+ return this.trackMetrics(
66
+ () => this.cache.mget(
67
+ entries.map((entry) => ({
68
+ ...entry,
69
+ key: this.qualify(entry.key)
70
+ }))
71
+ )
55
72
  );
56
73
  }
57
74
  async mset(entries) {
58
- await this.cache.mset(
59
- entries.map((entry) => ({
60
- ...entry,
61
- key: this.qualify(entry.key)
62
- }))
75
+ await this.trackMetrics(
76
+ () => this.cache.mset(
77
+ entries.map((entry) => ({
78
+ ...entry,
79
+ key: this.qualify(entry.key)
80
+ }))
81
+ )
63
82
  );
64
83
  }
65
84
  async invalidateByTag(tag) {
66
- 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));
67
89
  }
68
90
  async invalidateByPattern(pattern) {
69
- 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)));
70
95
  }
71
96
  /**
72
97
  * Returns detailed metadata about a single cache key within this namespace.
@@ -87,10 +112,19 @@ var CacheNamespace = class _CacheNamespace {
87
112
  );
88
113
  }
89
114
  getMetrics() {
90
- return this.cache.getMetrics();
115
+ return cloneMetrics(this.metrics);
91
116
  }
92
117
  getHitRate() {
93
- 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 };
94
128
  }
95
129
  /**
96
130
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -107,7 +141,130 @@ var CacheNamespace = class _CacheNamespace {
107
141
  qualify(key) {
108
142
  return `${this.prefix}:${key}`;
109
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
+ }
110
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
+ }
111
268
 
112
269
  // src/internal/CircuitBreakerManager.ts
113
270
  var CircuitBreakerManager = class {
@@ -201,6 +358,95 @@ var CircuitBreakerManager = class {
201
358
  }
202
359
  };
203
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
+
204
450
  // src/internal/MetricsCollector.ts
205
451
  var MetricsCollector = class {
206
452
  data = this.empty();
@@ -275,107 +521,6 @@ var MetricsCollector = class {
275
521
  }
276
522
  };
277
523
 
278
- // src/internal/StoredValue.ts
279
- function isStoredValueEnvelope(value) {
280
- return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
281
- }
282
- function createStoredValueEnvelope(options) {
283
- const now = options.now ?? Date.now();
284
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
285
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
286
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
287
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
288
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
289
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
290
- return {
291
- __layercache: 1,
292
- kind: options.kind,
293
- value: options.value,
294
- freshUntil,
295
- staleUntil,
296
- errorUntil,
297
- freshTtlSeconds: freshTtlSeconds ?? null,
298
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
299
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
300
- };
301
- }
302
- function resolveStoredValue(stored, now = Date.now()) {
303
- if (!isStoredValueEnvelope(stored)) {
304
- return { state: "fresh", value: stored, stored };
305
- }
306
- if (stored.freshUntil === null || stored.freshUntil > now) {
307
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
308
- }
309
- if (stored.staleUntil !== null && stored.staleUntil > now) {
310
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
311
- }
312
- if (stored.errorUntil !== null && stored.errorUntil > now) {
313
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
314
- }
315
- return { state: "expired", value: null, stored, envelope: stored };
316
- }
317
- function unwrapStoredValue(stored) {
318
- if (!isStoredValueEnvelope(stored)) {
319
- return stored;
320
- }
321
- if (stored.kind === "empty") {
322
- return null;
323
- }
324
- return stored.value ?? null;
325
- }
326
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
327
- if (!isStoredValueEnvelope(stored)) {
328
- return void 0;
329
- }
330
- const expiry = maxExpiry(stored);
331
- if (expiry === null) {
332
- return void 0;
333
- }
334
- const remainingMs = expiry - now;
335
- if (remainingMs <= 0) {
336
- return 1;
337
- }
338
- return Math.max(1, Math.ceil(remainingMs / 1e3));
339
- }
340
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
341
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
342
- return void 0;
343
- }
344
- const remainingMs = stored.freshUntil - now;
345
- if (remainingMs <= 0) {
346
- return 0;
347
- }
348
- return Math.max(1, Math.ceil(remainingMs / 1e3));
349
- }
350
- function refreshStoredEnvelope(stored, now = Date.now()) {
351
- if (!isStoredValueEnvelope(stored)) {
352
- return stored;
353
- }
354
- return createStoredValueEnvelope({
355
- kind: stored.kind,
356
- value: stored.value,
357
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
358
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
359
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
360
- now
361
- });
362
- }
363
- function maxExpiry(stored) {
364
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
365
- (value) => value !== null
366
- );
367
- if (values.length === 0) {
368
- return null;
369
- }
370
- return Math.max(...values);
371
- }
372
- function normalizePositiveSeconds(value) {
373
- if (!value || value <= 0) {
374
- return void 0;
375
- }
376
- return value;
377
- }
378
-
379
524
  // src/internal/TtlResolver.ts
380
525
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
381
526
  var TtlResolver = class {
@@ -397,13 +542,14 @@ var TtlResolver = class {
397
542
  clearProfiles() {
398
543
  this.accessProfiles.clear();
399
544
  }
400
- 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;
401
547
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
402
548
  layerName,
403
549
  options?.negativeTtl,
404
550
  globalNegativeTtl,
405
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
406
- ) : 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);
407
553
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
408
554
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
409
555
  return this.applyJitter(adaptiveTtl, jitter);
@@ -442,6 +588,29 @@ var TtlResolver = class {
442
588
  const delta = (Math.random() * 2 - 1) * jitter;
443
589
  return Math.max(1, Math.round(ttl + delta));
444
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
+ }
445
614
  readLayerNumber(layerName, value) {
446
615
  if (typeof value === "number") {
447
616
  return value;
@@ -464,90 +633,8 @@ var TtlResolver = class {
464
633
  }
465
634
  };
466
635
 
467
- // src/invalidation/TagIndex.ts
468
- var TagIndex = class {
469
- tagToKeys = /* @__PURE__ */ new Map();
470
- keyToTags = /* @__PURE__ */ new Map();
471
- knownKeys = /* @__PURE__ */ new Set();
472
- maxKnownKeys;
473
- constructor(options = {}) {
474
- this.maxKnownKeys = options.maxKnownKeys;
475
- }
476
- async touch(key) {
477
- this.knownKeys.add(key);
478
- this.pruneKnownKeysIfNeeded();
479
- }
480
- async track(key, tags) {
481
- this.knownKeys.add(key);
482
- this.pruneKnownKeysIfNeeded();
483
- if (tags.length === 0) {
484
- return;
485
- }
486
- const existingTags = this.keyToTags.get(key);
487
- if (existingTags) {
488
- for (const tag of existingTags) {
489
- this.tagToKeys.get(tag)?.delete(key);
490
- }
491
- }
492
- const tagSet = new Set(tags);
493
- this.keyToTags.set(key, tagSet);
494
- for (const tag of tagSet) {
495
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
496
- keys.add(key);
497
- this.tagToKeys.set(tag, keys);
498
- }
499
- }
500
- async remove(key) {
501
- this.knownKeys.delete(key);
502
- const tags = this.keyToTags.get(key);
503
- if (!tags) {
504
- return;
505
- }
506
- for (const tag of tags) {
507
- const keys = this.tagToKeys.get(tag);
508
- if (!keys) {
509
- continue;
510
- }
511
- keys.delete(key);
512
- if (keys.size === 0) {
513
- this.tagToKeys.delete(tag);
514
- }
515
- }
516
- this.keyToTags.delete(key);
517
- }
518
- async keysForTag(tag) {
519
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
520
- }
521
- async tagsForKey(key) {
522
- return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
523
- }
524
- async matchPattern(pattern) {
525
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
526
- }
527
- async clear() {
528
- this.tagToKeys.clear();
529
- this.keyToTags.clear();
530
- this.knownKeys.clear();
531
- }
532
- pruneKnownKeysIfNeeded() {
533
- if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
534
- return;
535
- }
536
- const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
537
- let removed = 0;
538
- for (const key of this.knownKeys) {
539
- if (removed >= toRemove) {
540
- break;
541
- }
542
- this.knownKeys.delete(key);
543
- this.keyToTags.delete(key);
544
- removed += 1;
545
- }
546
- }
547
- };
548
-
549
636
  // src/stampede/StampedeGuard.ts
550
- import { Mutex } from "async-mutex";
637
+ import { Mutex as Mutex2 } from "async-mutex";
551
638
  var StampedeGuard = class {
552
639
  mutexes = /* @__PURE__ */ new Map();
553
640
  async execute(key, task) {
@@ -564,7 +651,7 @@ var StampedeGuard = class {
564
651
  getMutexEntry(key) {
565
652
  let entry = this.mutexes.get(key);
566
653
  if (!entry) {
567
- entry = { mutex: new Mutex(), references: 0 };
654
+ entry = { mutex: new Mutex2(), references: 0 };
568
655
  this.mutexes.set(key, entry);
569
656
  }
570
657
  entry.references += 1;
@@ -625,6 +712,7 @@ var CacheStack = class extends EventEmitter {
625
712
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
626
713
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
627
714
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
715
+ this.currentGeneration = options.generation;
628
716
  if (options.publishSetInvalidation !== void 0) {
629
717
  console.warn(
630
718
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -633,21 +721,27 @@ var CacheStack = class extends EventEmitter {
633
721
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
634
722
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
635
723
  this.tagIndex = options.tagIndex ?? new TagIndex();
724
+ this.initializeWriteBehind(options.writeBehind);
636
725
  this.startup = this.initialize();
637
726
  }
638
727
  layers;
639
728
  options;
640
729
  stampedeGuard = new StampedeGuard();
641
730
  metricsCollector = new MetricsCollector();
642
- instanceId = randomUUID();
731
+ instanceId = createInstanceId();
643
732
  startup;
644
733
  unsubscribeInvalidation;
645
734
  logger;
646
735
  tagIndex;
736
+ fetchRateLimiter = new FetchRateLimiter();
647
737
  backgroundRefreshes = /* @__PURE__ */ new Map();
648
738
  layerDegradedUntil = /* @__PURE__ */ new Map();
649
739
  ttlResolver;
650
740
  circuitBreakerManager;
741
+ currentGeneration;
742
+ writeBehindQueue = [];
743
+ writeBehindTimer;
744
+ writeBehindFlushPromise;
651
745
  isDisconnecting = false;
652
746
  disconnectPromise;
653
747
  /**
@@ -657,9 +751,9 @@ var CacheStack = class extends EventEmitter {
657
751
  * and no `fetcher` is provided.
658
752
  */
659
753
  async get(key, fetcher, options) {
660
- const normalizedKey = this.validateCacheKey(key);
754
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
661
755
  this.validateWriteOptions(options);
662
- await this.startup;
756
+ await this.awaitStartup("get");
663
757
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
664
758
  if (hit.found) {
665
759
  this.ttlResolver.recordAccess(normalizedKey);
@@ -724,8 +818,8 @@ var CacheStack = class extends EventEmitter {
724
818
  * Returns true if the given key exists and is not expired in any layer.
725
819
  */
726
820
  async has(key) {
727
- const normalizedKey = this.validateCacheKey(key);
728
- await this.startup;
821
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
822
+ await this.awaitStartup("has");
729
823
  for (const layer of this.layers) {
730
824
  if (this.shouldSkipLayer(layer)) {
731
825
  continue;
@@ -755,8 +849,8 @@ var CacheStack = class extends EventEmitter {
755
849
  * that has it, or null if the key is not found / has no TTL.
756
850
  */
757
851
  async ttl(key) {
758
- const normalizedKey = this.validateCacheKey(key);
759
- await this.startup;
852
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
853
+ await this.awaitStartup("ttl");
760
854
  for (const layer of this.layers) {
761
855
  if (this.shouldSkipLayer(layer)) {
762
856
  continue;
@@ -777,17 +871,17 @@ var CacheStack = class extends EventEmitter {
777
871
  * Stores a value in all cache layers. Overwrites any existing value.
778
872
  */
779
873
  async set(key, value, options) {
780
- const normalizedKey = this.validateCacheKey(key);
874
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
781
875
  this.validateWriteOptions(options);
782
- await this.startup;
876
+ await this.awaitStartup("set");
783
877
  await this.storeEntry(normalizedKey, "value", value, options);
784
878
  }
785
879
  /**
786
880
  * Deletes the key from all layers and publishes an invalidation message.
787
881
  */
788
882
  async delete(key) {
789
- const normalizedKey = this.validateCacheKey(key);
790
- await this.startup;
883
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
884
+ await this.awaitStartup("delete");
791
885
  await this.deleteKeys([normalizedKey]);
792
886
  await this.publishInvalidation({
793
887
  scope: "key",
@@ -797,7 +891,7 @@ var CacheStack = class extends EventEmitter {
797
891
  });
798
892
  }
799
893
  async clear() {
800
- await this.startup;
894
+ await this.awaitStartup("clear");
801
895
  await Promise.all(this.layers.map((layer) => layer.clear()));
802
896
  await this.tagIndex.clear();
803
897
  this.ttlResolver.clearProfiles();
@@ -813,23 +907,25 @@ var CacheStack = class extends EventEmitter {
813
907
  if (keys.length === 0) {
814
908
  return;
815
909
  }
816
- await this.startup;
910
+ await this.awaitStartup("mdelete");
817
911
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
818
- await this.deleteKeys(normalizedKeys);
912
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
913
+ await this.deleteKeys(cacheKeys);
819
914
  await this.publishInvalidation({
820
915
  scope: "keys",
821
- keys: normalizedKeys,
916
+ keys: cacheKeys,
822
917
  sourceId: this.instanceId,
823
918
  operation: "delete"
824
919
  });
825
920
  }
826
921
  async mget(entries) {
922
+ this.assertActive("mget");
827
923
  if (entries.length === 0) {
828
924
  return [];
829
925
  }
830
926
  const normalizedEntries = entries.map((entry) => ({
831
927
  ...entry,
832
- key: this.validateCacheKey(entry.key)
928
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
833
929
  }));
834
930
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
835
931
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -855,7 +951,7 @@ var CacheStack = class extends EventEmitter {
855
951
  })
856
952
  );
857
953
  }
858
- await this.startup;
954
+ await this.awaitStartup("mget");
859
955
  const pending = /* @__PURE__ */ new Set();
860
956
  const indexesByKey = /* @__PURE__ */ new Map();
861
957
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -903,14 +999,17 @@ var CacheStack = class extends EventEmitter {
903
999
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
904
1000
  }
905
1001
  async mset(entries) {
1002
+ this.assertActive("mset");
906
1003
  const normalizedEntries = entries.map((entry) => ({
907
1004
  ...entry,
908
- key: this.validateCacheKey(entry.key)
1005
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
909
1006
  }));
910
1007
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
911
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1008
+ await this.awaitStartup("mset");
1009
+ await this.writeBatch(normalizedEntries);
912
1010
  }
913
1011
  async warm(entries, options = {}) {
1012
+ this.assertActive("warm");
914
1013
  const concurrency = Math.max(1, options.concurrency ?? 4);
915
1014
  const total = entries.length;
916
1015
  let completed = 0;
@@ -959,14 +1058,31 @@ var CacheStack = class extends EventEmitter {
959
1058
  return new CacheNamespace(this, prefix);
960
1059
  }
961
1060
  async invalidateByTag(tag) {
962
- await this.startup;
1061
+ await this.awaitStartup("invalidateByTag");
963
1062
  const keys = await this.tagIndex.keysForTag(tag);
964
1063
  await this.deleteKeys(keys);
965
1064
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
966
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
+ }
967
1076
  async invalidateByPattern(pattern) {
968
- await this.startup;
969
- 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}*`);
970
1086
  await this.deleteKeys(keys);
971
1087
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
972
1088
  }
@@ -993,14 +1109,43 @@ var CacheStack = class extends EventEmitter {
993
1109
  getHitRate() {
994
1110
  return this.metricsCollector.hitRate();
995
1111
  }
1112
+ async healthCheck() {
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
+ }
996
1140
  /**
997
1141
  * Returns detailed metadata about a single cache key: which layers contain it,
998
1142
  * remaining fresh/stale/error TTLs, and associated tags.
999
1143
  * Returns `null` if the key does not exist in any layer.
1000
1144
  */
1001
1145
  async inspect(key) {
1002
- const normalizedKey = this.validateCacheKey(key);
1003
- await this.startup;
1146
+ const userKey = this.validateCacheKey(key);
1147
+ const normalizedKey = this.qualifyKey(userKey);
1148
+ await this.awaitStartup("inspect");
1004
1149
  const foundInLayers = [];
1005
1150
  let freshTtlSeconds = null;
1006
1151
  let staleTtlSeconds = null;
@@ -1031,10 +1176,10 @@ var CacheStack = class extends EventEmitter {
1031
1176
  return null;
1032
1177
  }
1033
1178
  const tags = await this.getTagsForKey(normalizedKey);
1034
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1179
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1035
1180
  }
1036
1181
  async exportState() {
1037
- await this.startup;
1182
+ await this.awaitStartup("exportState");
1038
1183
  const exported = /* @__PURE__ */ new Map();
1039
1184
  for (const layer of this.layers) {
1040
1185
  if (!layer.keys) {
@@ -1042,15 +1187,16 @@ var CacheStack = class extends EventEmitter {
1042
1187
  }
1043
1188
  const keys = await layer.keys();
1044
1189
  for (const key of keys) {
1045
- if (exported.has(key)) {
1190
+ const exportedKey = this.stripQualifiedKey(key);
1191
+ if (exported.has(exportedKey)) {
1046
1192
  continue;
1047
1193
  }
1048
1194
  const stored = await this.readLayerEntry(layer, key);
1049
1195
  if (stored === null) {
1050
1196
  continue;
1051
1197
  }
1052
- exported.set(key, {
1053
- key,
1198
+ exported.set(exportedKey, {
1199
+ key: exportedKey,
1054
1200
  value: stored,
1055
1201
  ttl: remainingStoredTtlSeconds(stored)
1056
1202
  });
@@ -1059,20 +1205,25 @@ var CacheStack = class extends EventEmitter {
1059
1205
  return [...exported.values()];
1060
1206
  }
1061
1207
  async importState(entries) {
1062
- await this.startup;
1208
+ await this.awaitStartup("importState");
1063
1209
  await Promise.all(
1064
1210
  entries.map(async (entry) => {
1065
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1066
- 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);
1067
1214
  })
1068
1215
  );
1069
1216
  }
1070
1217
  async persistToFile(filePath) {
1218
+ this.assertActive("persistToFile");
1071
1219
  const snapshot = await this.exportState();
1072
- 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");
1073
1222
  }
1074
1223
  async restoreFromFile(filePath) {
1075
- 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");
1076
1227
  let parsed;
1077
1228
  try {
1078
1229
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1095,7 +1246,13 @@ var CacheStack = class extends EventEmitter {
1095
1246
  this.disconnectPromise = (async () => {
1096
1247
  await this.startup;
1097
1248
  await this.unsubscribeInvalidation?.();
1249
+ await this.flushWriteBehindQueue();
1098
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()));
1099
1256
  })();
1100
1257
  }
1101
1258
  await this.disconnectPromise;
@@ -1155,7 +1312,10 @@ var CacheStack = class extends EventEmitter {
1155
1312
  const fetchStart = Date.now();
1156
1313
  let fetched;
1157
1314
  try {
1158
- fetched = await fetcher();
1315
+ fetched = await this.fetchRateLimiter.schedule(
1316
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1317
+ fetcher
1318
+ );
1159
1319
  this.circuitBreakerManager.recordSuccess(key);
1160
1320
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1161
1321
  } catch (error) {
@@ -1189,6 +1349,61 @@ var CacheStack = class extends EventEmitter {
1189
1349
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1190
1350
  }
1191
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
+ }
1192
1407
  async readFromLayers(key, options, mode) {
1193
1408
  let sawRetainableValue = false;
1194
1409
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1272,33 +1487,28 @@ var CacheStack = class extends EventEmitter {
1272
1487
  }
1273
1488
  async writeAcrossLayers(key, kind, value, options) {
1274
1489
  const now = Date.now();
1275
- const operations = this.layers.map((layer) => async () => {
1276
- if (this.shouldSkipLayer(layer)) {
1277
- return;
1278
- }
1279
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1280
- const staleWhileRevalidate = this.resolveLayerSeconds(
1281
- layer.name,
1282
- options?.staleWhileRevalidate,
1283
- this.options.staleWhileRevalidate
1284
- );
1285
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1286
- const payload = createStoredValueEnvelope({
1287
- kind,
1288
- value,
1289
- freshTtlSeconds: freshTtl,
1290
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1291
- staleIfErrorSeconds: staleIfError,
1292
- now
1293
- });
1294
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1295
- try {
1296
- await layer.set(key, payload, ttl);
1297
- } catch (error) {
1298
- 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);
1299
1508
  }
1300
- });
1301
- 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)));
1302
1512
  }
1303
1513
  async executeLayerOperations(operations, context) {
1304
1514
  if (this.options.writePolicy !== "best-effort") {
@@ -1322,8 +1532,17 @@ var CacheStack = class extends EventEmitter {
1322
1532
  );
1323
1533
  }
1324
1534
  }
1325
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1326
- 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
+ );
1327
1546
  }
1328
1547
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1329
1548
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1417,6 +1636,105 @@ var CacheStack = class extends EventEmitter {
1417
1636
  shouldBroadcastL1Invalidation() {
1418
1637
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1419
1638
  }
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
+ }
1420
1738
  async deleteKeysFromLayers(layers, keys) {
1421
1739
  await Promise.all(
1422
1740
  layers.map(async (layer) => {
@@ -1460,6 +1778,9 @@ var CacheStack = class extends EventEmitter {
1460
1778
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1461
1779
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1462
1780
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
1781
+ if (this.options.generation !== void 0) {
1782
+ this.validateNonNegativeNumber("generation", this.options.generation);
1783
+ }
1463
1784
  }
1464
1785
  validateWriteOptions(options) {
1465
1786
  if (!options) {
@@ -1471,6 +1792,7 @@ var CacheStack = class extends EventEmitter {
1471
1792
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1472
1793
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1473
1794
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
1795
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1474
1796
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1475
1797
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1476
1798
  }
@@ -1514,6 +1836,26 @@ var CacheStack = class extends EventEmitter {
1514
1836
  }
1515
1837
  return key;
1516
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
+ }
1517
1859
  serializeOptions(options) {
1518
1860
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1519
1861
  }
@@ -1619,18 +1961,23 @@ var CacheStack = class extends EventEmitter {
1619
1961
  return value;
1620
1962
  }
1621
1963
  };
1964
+ function createInstanceId() {
1965
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1966
+ }
1622
1967
 
1623
1968
  // src/invalidation/RedisInvalidationBus.ts
1624
1969
  var RedisInvalidationBus = class {
1625
1970
  channel;
1626
1971
  publisher;
1627
1972
  subscriber;
1973
+ logger;
1628
1974
  handlers = /* @__PURE__ */ new Set();
1629
1975
  sharedListener;
1630
1976
  constructor(options) {
1631
1977
  this.publisher = options.publisher;
1632
1978
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1633
1979
  this.channel = options.channel ?? "layercache:invalidation";
1980
+ this.logger = options.logger;
1634
1981
  }
1635
1982
  async subscribe(handler) {
1636
1983
  if (this.handlers.size === 0) {
@@ -1687,6 +2034,10 @@ var RedisInvalidationBus = class {
1687
2034
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1688
2035
  }
1689
2036
  reportError(message, error) {
2037
+ if (this.logger?.error) {
2038
+ this.logger.error(message, { error });
2039
+ return;
2040
+ }
1690
2041
  console.error(`[layercache] ${message}`, error);
1691
2042
  }
1692
2043
  };
@@ -1739,32 +2090,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1739
2090
  function createExpressCacheMiddleware(cache, options = {}) {
1740
2091
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
1741
2092
  return async (req, res, next) => {
1742
- const method = (req.method ?? "GET").toUpperCase();
1743
- if (!allowedMethods.has(method)) {
1744
- next();
1745
- return;
1746
- }
1747
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
1748
- const cached = await cache.get(key, void 0, options);
1749
- if (cached !== null) {
1750
- res.setHeader?.("content-type", "application/json; charset=utf-8");
1751
- res.setHeader?.("x-cache", "HIT");
1752
- if (res.json) {
1753
- res.json(cached);
1754
- } else {
1755
- res.end?.(JSON.stringify(cached));
2093
+ try {
2094
+ const method = (req.method ?? "GET").toUpperCase();
2095
+ if (!allowedMethods.has(method)) {
2096
+ next();
2097
+ return;
1756
2098
  }
1757
- return;
1758
- }
1759
- const originalJson = res.json?.bind(res);
1760
- if (originalJson) {
1761
- res.json = (body) => {
1762
- res.setHeader?.("x-cache", "MISS");
1763
- void cache.set(key, body, options);
1764
- return originalJson(body);
1765
- };
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);
1766
2122
  }
1767
- next();
1768
2123
  };
1769
2124
  }
1770
2125
 
@@ -1777,6 +2132,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1777
2132
  return (...args) => wrapped(...args);
1778
2133
  }
1779
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
+
1780
2197
  // src/integrations/trpc.ts
1781
2198
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1782
2199
  return async (context) => {
@@ -1802,163 +2219,6 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1802
2219
  };
1803
2220
  }
1804
2221
 
1805
- // src/layers/MemoryLayer.ts
1806
- var MemoryLayer = class {
1807
- name;
1808
- defaultTtl;
1809
- isLocal = true;
1810
- maxSize;
1811
- evictionPolicy;
1812
- entries = /* @__PURE__ */ new Map();
1813
- constructor(options = {}) {
1814
- this.name = options.name ?? "memory";
1815
- this.defaultTtl = options.ttl;
1816
- this.maxSize = options.maxSize ?? 1e3;
1817
- this.evictionPolicy = options.evictionPolicy ?? "lru";
1818
- }
1819
- async get(key) {
1820
- const value = await this.getEntry(key);
1821
- return unwrapStoredValue(value);
1822
- }
1823
- async getEntry(key) {
1824
- const entry = this.entries.get(key);
1825
- if (!entry) {
1826
- return null;
1827
- }
1828
- if (this.isExpired(entry)) {
1829
- this.entries.delete(key);
1830
- return null;
1831
- }
1832
- if (this.evictionPolicy === "lru") {
1833
- this.entries.delete(key);
1834
- entry.accessCount += 1;
1835
- this.entries.set(key, entry);
1836
- } else if (this.evictionPolicy === "lfu") {
1837
- entry.accessCount += 1;
1838
- }
1839
- return entry.value;
1840
- }
1841
- async getMany(keys) {
1842
- const values = [];
1843
- for (const key of keys) {
1844
- values.push(await this.getEntry(key));
1845
- }
1846
- return values;
1847
- }
1848
- async set(key, value, ttl = this.defaultTtl) {
1849
- this.entries.delete(key);
1850
- this.entries.set(key, {
1851
- value,
1852
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
1853
- accessCount: 0,
1854
- insertedAt: Date.now()
1855
- });
1856
- while (this.entries.size > this.maxSize) {
1857
- this.evict();
1858
- }
1859
- }
1860
- async has(key) {
1861
- const entry = this.entries.get(key);
1862
- if (!entry) {
1863
- return false;
1864
- }
1865
- if (this.isExpired(entry)) {
1866
- this.entries.delete(key);
1867
- return false;
1868
- }
1869
- return true;
1870
- }
1871
- async ttl(key) {
1872
- const entry = this.entries.get(key);
1873
- if (!entry) {
1874
- return null;
1875
- }
1876
- if (this.isExpired(entry)) {
1877
- this.entries.delete(key);
1878
- return null;
1879
- }
1880
- if (entry.expiresAt === null) {
1881
- return null;
1882
- }
1883
- return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
1884
- }
1885
- async size() {
1886
- this.pruneExpired();
1887
- return this.entries.size;
1888
- }
1889
- async delete(key) {
1890
- this.entries.delete(key);
1891
- }
1892
- async deleteMany(keys) {
1893
- for (const key of keys) {
1894
- this.entries.delete(key);
1895
- }
1896
- }
1897
- async clear() {
1898
- this.entries.clear();
1899
- }
1900
- async keys() {
1901
- this.pruneExpired();
1902
- return [...this.entries.keys()];
1903
- }
1904
- exportState() {
1905
- this.pruneExpired();
1906
- return [...this.entries.entries()].map(([key, entry]) => ({
1907
- key,
1908
- value: entry.value,
1909
- expiresAt: entry.expiresAt
1910
- }));
1911
- }
1912
- importState(entries) {
1913
- for (const entry of entries) {
1914
- if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
1915
- continue;
1916
- }
1917
- this.entries.set(entry.key, {
1918
- value: entry.value,
1919
- expiresAt: entry.expiresAt,
1920
- accessCount: 0,
1921
- insertedAt: Date.now()
1922
- });
1923
- }
1924
- while (this.entries.size > this.maxSize) {
1925
- this.evict();
1926
- }
1927
- }
1928
- evict() {
1929
- if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
1930
- const oldestKey = this.entries.keys().next().value;
1931
- if (oldestKey !== void 0) {
1932
- this.entries.delete(oldestKey);
1933
- }
1934
- return;
1935
- }
1936
- let victimKey;
1937
- let minCount = Number.POSITIVE_INFINITY;
1938
- let minInsertedAt = Number.POSITIVE_INFINITY;
1939
- for (const [key, entry] of this.entries.entries()) {
1940
- if (entry.accessCount < minCount || entry.accessCount === minCount && entry.insertedAt < minInsertedAt) {
1941
- minCount = entry.accessCount;
1942
- minInsertedAt = entry.insertedAt;
1943
- victimKey = key;
1944
- }
1945
- }
1946
- if (victimKey !== void 0) {
1947
- this.entries.delete(victimKey);
1948
- }
1949
- }
1950
- pruneExpired() {
1951
- for (const [key, entry] of this.entries.entries()) {
1952
- if (this.isExpired(entry)) {
1953
- this.entries.delete(key);
1954
- }
1955
- }
1956
- }
1957
- isExpired(entry) {
1958
- return entry.expiresAt !== null && entry.expiresAt <= Date.now();
1959
- }
1960
- };
1961
-
1962
2222
  // src/layers/RedisLayer.ts
1963
2223
  import { promisify } from "util";
1964
2224
  import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
@@ -1985,22 +2245,24 @@ var RedisLayer = class {
1985
2245
  defaultTtl;
1986
2246
  isLocal = false;
1987
2247
  client;
1988
- serializer;
2248
+ serializers;
1989
2249
  prefix;
1990
2250
  allowUnprefixedClear;
1991
2251
  scanCount;
1992
2252
  compression;
1993
2253
  compressionThreshold;
2254
+ disconnectOnDispose;
1994
2255
  constructor(options) {
1995
2256
  this.client = options.client;
1996
2257
  this.defaultTtl = options.ttl;
1997
2258
  this.name = options.name ?? "redis";
1998
- this.serializer = options.serializer ?? new JsonSerializer();
2259
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
1999
2260
  this.prefix = options.prefix ?? "";
2000
2261
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
2001
2262
  this.scanCount = options.scanCount ?? 100;
2002
2263
  this.compression = options.compression;
2003
2264
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2265
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
2004
2266
  }
2005
2267
  async get(key) {
2006
2268
  const payload = await this.getEntry(key);
@@ -2035,8 +2297,25 @@ var RedisLayer = class {
2035
2297
  })
2036
2298
  );
2037
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
+ }
2038
2317
  async set(key, value, ttl = this.defaultTtl) {
2039
- const serialized = this.serializer.serialize(value);
2318
+ const serialized = this.primarySerializer().serialize(value);
2040
2319
  const payload = await this.encodePayload(serialized);
2041
2320
  const normalizedKey = this.withPrefix(key);
2042
2321
  if (ttl && ttl > 0) {
@@ -2069,6 +2348,18 @@ var RedisLayer = class {
2069
2348
  const keys = await this.keys();
2070
2349
  return keys.length;
2071
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
+ }
2072
2363
  /**
2073
2364
  * Deletes all keys matching the layer's prefix in batches to avoid
2074
2365
  * loading millions of keys into memory at once.
@@ -2115,12 +2406,39 @@ var RedisLayer = class {
2115
2406
  return `${this.prefix}${key}`;
2116
2407
  }
2117
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
+ }
2118
2420
  try {
2119
- return this.serializer.deserialize(await this.decodePayload(payload));
2120
- } catch {
2121
2421
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
2122
- return null;
2422
+ } catch {
2123
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;
2124
2442
  }
2125
2443
  isSerializablePayload(payload) {
2126
2444
  return typeof payload === "string" || Buffer.isBuffer(payload);
@@ -2160,7 +2478,7 @@ var RedisLayer = class {
2160
2478
 
2161
2479
  // src/layers/DiskLayer.ts
2162
2480
  import { createHash } from "crypto";
2163
- import { promises as fs2 } from "fs";
2481
+ import { promises as fs } from "fs";
2164
2482
  import { join } from "path";
2165
2483
  var DiskLayer = class {
2166
2484
  name;
@@ -2169,6 +2487,7 @@ var DiskLayer = class {
2169
2487
  directory;
2170
2488
  serializer;
2171
2489
  maxFiles;
2490
+ writeQueue = Promise.resolve();
2172
2491
  constructor(options) {
2173
2492
  this.directory = options.directory;
2174
2493
  this.defaultTtl = options.ttl;
@@ -2183,7 +2502,7 @@ var DiskLayer = class {
2183
2502
  const filePath = this.keyToPath(key);
2184
2503
  let raw;
2185
2504
  try {
2186
- raw = await fs2.readFile(filePath);
2505
+ raw = await fs.readFile(filePath);
2187
2506
  } catch {
2188
2507
  return null;
2189
2508
  }
@@ -2201,16 +2520,29 @@ var DiskLayer = class {
2201
2520
  return entry.value;
2202
2521
  }
2203
2522
  async set(key, value, ttl = this.defaultTtl) {
2204
- await fs2.mkdir(this.directory, { recursive: true });
2205
- const entry = {
2206
- key,
2207
- value,
2208
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2209
- };
2210
- const payload = this.serializer.serialize(entry);
2211
- await fs2.writeFile(this.keyToPath(key), payload);
2212
- if (this.maxFiles !== void 0) {
2213
- await this.enforceMaxFiles();
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);
2214
2546
  }
2215
2547
  }
2216
2548
  async has(key) {
@@ -2221,7 +2553,7 @@ var DiskLayer = class {
2221
2553
  const filePath = this.keyToPath(key);
2222
2554
  let raw;
2223
2555
  try {
2224
- raw = await fs2.readFile(filePath);
2556
+ raw = await fs.readFile(filePath);
2225
2557
  } catch {
2226
2558
  return null;
2227
2559
  }
@@ -2241,21 +2573,25 @@ var DiskLayer = class {
2241
2573
  return remaining;
2242
2574
  }
2243
2575
  async delete(key) {
2244
- await this.safeDelete(this.keyToPath(key));
2576
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2245
2577
  }
2246
2578
  async deleteMany(keys) {
2247
- 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
+ });
2248
2582
  }
2249
2583
  async clear() {
2250
- let entries;
2251
- try {
2252
- entries = await fs2.readdir(this.directory);
2253
- } catch {
2254
- return;
2255
- }
2256
- await Promise.all(
2257
- entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
2258
- );
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
+ });
2259
2595
  }
2260
2596
  /**
2261
2597
  * Returns the original cache key strings stored on disk.
@@ -2264,7 +2600,7 @@ var DiskLayer = class {
2264
2600
  async keys() {
2265
2601
  let entries;
2266
2602
  try {
2267
- entries = await fs2.readdir(this.directory);
2603
+ entries = await fs.readdir(this.directory);
2268
2604
  } catch {
2269
2605
  return [];
2270
2606
  }
@@ -2275,7 +2611,7 @@ var DiskLayer = class {
2275
2611
  const filePath = join(this.directory, name);
2276
2612
  let raw;
2277
2613
  try {
2278
- raw = await fs2.readFile(filePath);
2614
+ raw = await fs.readFile(filePath);
2279
2615
  } catch {
2280
2616
  return;
2281
2617
  }
@@ -2299,16 +2635,31 @@ var DiskLayer = class {
2299
2635
  const keys = await this.keys();
2300
2636
  return keys.length;
2301
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
+ }
2302
2648
  keyToPath(key) {
2303
2649
  const hash = createHash("sha256").update(key).digest("hex");
2304
2650
  return join(this.directory, `${hash}.lc`);
2305
2651
  }
2306
2652
  async safeDelete(filePath) {
2307
2653
  try {
2308
- await fs2.unlink(filePath);
2654
+ await fs.unlink(filePath);
2309
2655
  } catch {
2310
2656
  }
2311
2657
  }
2658
+ enqueueWrite(operation) {
2659
+ const next = this.writeQueue.then(operation, operation);
2660
+ this.writeQueue = next.catch(() => void 0);
2661
+ return next;
2662
+ }
2312
2663
  /**
2313
2664
  * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
2314
2665
  */
@@ -2318,7 +2669,7 @@ var DiskLayer = class {
2318
2669
  }
2319
2670
  let entries;
2320
2671
  try {
2321
- entries = await fs2.readdir(this.directory);
2672
+ entries = await fs.readdir(this.directory);
2322
2673
  } catch {
2323
2674
  return;
2324
2675
  }
@@ -2330,7 +2681,7 @@ var DiskLayer = class {
2330
2681
  lcFiles.map(async (name) => {
2331
2682
  const filePath = join(this.directory, name);
2332
2683
  try {
2333
- const stat = await fs2.stat(filePath);
2684
+ const stat = await fs.stat(filePath);
2334
2685
  return { filePath, mtimeMs: stat.mtimeMs };
2335
2686
  } catch {
2336
2687
  return { filePath, mtimeMs: 0 };
@@ -2414,7 +2765,7 @@ var MsgpackSerializer = class {
2414
2765
  };
2415
2766
 
2416
2767
  // src/singleflight/RedisSingleFlightCoordinator.ts
2417
- import { randomUUID as randomUUID2 } from "crypto";
2768
+ import { randomUUID } from "crypto";
2418
2769
  var RELEASE_SCRIPT = `
2419
2770
  if redis.call("get", KEYS[1]) == ARGV[1] then
2420
2771
  return redis.call("del", KEYS[1])
@@ -2430,7 +2781,7 @@ var RedisSingleFlightCoordinator = class {
2430
2781
  }
2431
2782
  async execute(key, options, worker, waiter) {
2432
2783
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2433
- const token = randomUUID2();
2784
+ const token = randomUUID();
2434
2785
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2435
2786
  if (acquired === "OK") {
2436
2787
  try {
@@ -2542,6 +2893,8 @@ export {
2542
2893
  createCachedMethodDecorator,
2543
2894
  createExpressCacheMiddleware,
2544
2895
  createFastifyLayercachePlugin,
2896
+ createHonoCacheMiddleware,
2897
+ createOpenTelemetryPlugin,
2545
2898
  createPrometheusMetricsExporter,
2546
2899
  createTrpcCacheMiddleware
2547
2900
  };