layercache 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,27 @@
1
1
  import {
2
- PatternMatcher,
3
2
  RedisTagIndex
4
- } from "./chunk-BWM4MU2X.js";
3
+ } from "./chunk-IXCMHVHP.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,148 @@ var CircuitBreakerManager = class {
201
358
  }
202
359
  };
203
360
 
361
+ // src/internal/FetchRateLimiter.ts
362
+ var FetchRateLimiter = class {
363
+ queue = [];
364
+ buckets = /* @__PURE__ */ new Map();
365
+ fetcherBuckets = /* @__PURE__ */ new WeakMap();
366
+ nextFetcherBucketId = 0;
367
+ drainTimer;
368
+ async schedule(options, context, task) {
369
+ if (!options) {
370
+ return task();
371
+ }
372
+ const normalized = this.normalize(options);
373
+ if (!normalized) {
374
+ return task();
375
+ }
376
+ return new Promise((resolve2, reject) => {
377
+ this.queue.push({
378
+ bucketKey: this.resolveBucketKey(normalized, context),
379
+ options: normalized,
380
+ task,
381
+ resolve: resolve2,
382
+ reject
383
+ });
384
+ this.drain();
385
+ });
386
+ }
387
+ normalize(options) {
388
+ const maxConcurrent = options.maxConcurrent;
389
+ const intervalMs = options.intervalMs;
390
+ const maxPerInterval = options.maxPerInterval;
391
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
392
+ return void 0;
393
+ }
394
+ return {
395
+ maxConcurrent,
396
+ intervalMs,
397
+ maxPerInterval,
398
+ scope: options.scope ?? "global",
399
+ bucketKey: options.bucketKey
400
+ };
401
+ }
402
+ resolveBucketKey(options, context) {
403
+ if (options.bucketKey) {
404
+ return `custom:${options.bucketKey}`;
405
+ }
406
+ if (options.scope === "key") {
407
+ return `key:${context.key}`;
408
+ }
409
+ if (options.scope === "fetcher") {
410
+ const existing = this.fetcherBuckets.get(context.fetcher);
411
+ if (existing) {
412
+ return existing;
413
+ }
414
+ const bucket = `fetcher:${this.nextFetcherBucketId}`;
415
+ this.nextFetcherBucketId += 1;
416
+ this.fetcherBuckets.set(context.fetcher, bucket);
417
+ return bucket;
418
+ }
419
+ return "global";
420
+ }
421
+ drain() {
422
+ if (this.drainTimer) {
423
+ clearTimeout(this.drainTimer);
424
+ this.drainTimer = void 0;
425
+ }
426
+ while (this.queue.length > 0) {
427
+ let nextIndex = -1;
428
+ let nextWaitMs = Number.POSITIVE_INFINITY;
429
+ for (let index = 0; index < this.queue.length; index += 1) {
430
+ const next2 = this.queue[index];
431
+ if (!next2) {
432
+ continue;
433
+ }
434
+ const waitMs = this.waitTime(next2.bucketKey, next2.options);
435
+ if (waitMs <= 0) {
436
+ nextIndex = index;
437
+ break;
438
+ }
439
+ nextWaitMs = Math.min(nextWaitMs, waitMs);
440
+ }
441
+ if (nextIndex < 0) {
442
+ if (Number.isFinite(nextWaitMs)) {
443
+ this.drainTimer = setTimeout(() => {
444
+ this.drainTimer = void 0;
445
+ this.drain();
446
+ }, nextWaitMs);
447
+ this.drainTimer.unref?.();
448
+ }
449
+ return;
450
+ }
451
+ const next = this.queue.splice(nextIndex, 1)[0];
452
+ if (!next) {
453
+ return;
454
+ }
455
+ const bucket = this.bucketState(next.bucketKey);
456
+ bucket.active += 1;
457
+ bucket.startedAt.push(Date.now());
458
+ void next.task().then(next.resolve, next.reject).finally(() => {
459
+ bucket.active -= 1;
460
+ this.drain();
461
+ });
462
+ }
463
+ }
464
+ waitTime(bucketKey, options) {
465
+ const bucket = this.bucketState(bucketKey);
466
+ const now = Date.now();
467
+ if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
468
+ return 1;
469
+ }
470
+ if (!options.intervalMs || !options.maxPerInterval) {
471
+ return 0;
472
+ }
473
+ this.prune(bucket, now, options.intervalMs);
474
+ if (bucket.startedAt.length < options.maxPerInterval) {
475
+ return 0;
476
+ }
477
+ const oldest = bucket.startedAt[0];
478
+ if (!oldest) {
479
+ return 0;
480
+ }
481
+ return Math.max(1, options.intervalMs - (now - oldest));
482
+ }
483
+ prune(bucket, now, intervalMs) {
484
+ while (bucket.startedAt.length > 0) {
485
+ const startedAt = bucket.startedAt[0];
486
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
487
+ break;
488
+ }
489
+ bucket.startedAt.shift();
490
+ }
491
+ }
492
+ bucketState(bucketKey) {
493
+ const existing = this.buckets.get(bucketKey);
494
+ if (existing) {
495
+ return existing;
496
+ }
497
+ const bucket = { active: 0, startedAt: [] };
498
+ this.buckets.set(bucketKey, bucket);
499
+ return bucket;
500
+ }
501
+ };
502
+
204
503
  // src/internal/MetricsCollector.ts
205
504
  var MetricsCollector = class {
206
505
  data = this.empty();
@@ -275,107 +574,6 @@ var MetricsCollector = class {
275
574
  }
276
575
  };
277
576
 
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
577
  // src/internal/TtlResolver.ts
380
578
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
381
579
  var TtlResolver = class {
@@ -397,13 +595,14 @@ var TtlResolver = class {
397
595
  clearProfiles() {
398
596
  this.accessProfiles.clear();
399
597
  }
400
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
598
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
599
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
401
600
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
402
601
  layerName,
403
602
  options?.negativeTtl,
404
603
  globalNegativeTtl,
405
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
406
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
604
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
605
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
407
606
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
408
607
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
409
608
  return this.applyJitter(adaptiveTtl, jitter);
@@ -442,6 +641,29 @@ var TtlResolver = class {
442
641
  const delta = (Math.random() * 2 - 1) * jitter;
443
642
  return Math.max(1, Math.round(ttl + delta));
444
643
  }
644
+ resolvePolicyTtl(key, value, policy) {
645
+ if (!policy) {
646
+ return void 0;
647
+ }
648
+ if (typeof policy === "function") {
649
+ return policy({ key, value });
650
+ }
651
+ const now = /* @__PURE__ */ new Date();
652
+ if (policy === "until-midnight") {
653
+ const nextMidnight = new Date(now);
654
+ nextMidnight.setHours(24, 0, 0, 0);
655
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
656
+ }
657
+ if (policy === "next-hour") {
658
+ const nextHour = new Date(now);
659
+ nextHour.setMinutes(60, 0, 0);
660
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
661
+ }
662
+ const alignToSeconds = policy.alignTo;
663
+ const currentSeconds = Math.floor(Date.now() / 1e3);
664
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
665
+ return Math.max(1, nextBoundary - currentSeconds);
666
+ }
445
667
  readLayerNumber(layerName, value) {
446
668
  if (typeof value === "number") {
447
669
  return value;
@@ -464,90 +686,8 @@ var TtlResolver = class {
464
686
  }
465
687
  };
466
688
 
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
689
  // src/stampede/StampedeGuard.ts
550
- import { Mutex } from "async-mutex";
690
+ import { Mutex as Mutex2 } from "async-mutex";
551
691
  var StampedeGuard = class {
552
692
  mutexes = /* @__PURE__ */ new Map();
553
693
  async execute(key, task) {
@@ -564,7 +704,7 @@ var StampedeGuard = class {
564
704
  getMutexEntry(key) {
565
705
  let entry = this.mutexes.get(key);
566
706
  if (!entry) {
567
- entry = { mutex: new Mutex(), references: 0 };
707
+ entry = { mutex: new Mutex2(), references: 0 };
568
708
  this.mutexes.set(key, entry);
569
709
  }
570
710
  entry.references += 1;
@@ -625,6 +765,7 @@ var CacheStack = class extends EventEmitter {
625
765
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
626
766
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
627
767
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
768
+ this.currentGeneration = options.generation;
628
769
  if (options.publishSetInvalidation !== void 0) {
629
770
  console.warn(
630
771
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -633,21 +774,27 @@ var CacheStack = class extends EventEmitter {
633
774
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
634
775
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
635
776
  this.tagIndex = options.tagIndex ?? new TagIndex();
777
+ this.initializeWriteBehind(options.writeBehind);
636
778
  this.startup = this.initialize();
637
779
  }
638
780
  layers;
639
781
  options;
640
782
  stampedeGuard = new StampedeGuard();
641
783
  metricsCollector = new MetricsCollector();
642
- instanceId = randomUUID();
784
+ instanceId = createInstanceId();
643
785
  startup;
644
786
  unsubscribeInvalidation;
645
787
  logger;
646
788
  tagIndex;
789
+ fetchRateLimiter = new FetchRateLimiter();
647
790
  backgroundRefreshes = /* @__PURE__ */ new Map();
648
791
  layerDegradedUntil = /* @__PURE__ */ new Map();
649
792
  ttlResolver;
650
793
  circuitBreakerManager;
794
+ currentGeneration;
795
+ writeBehindQueue = [];
796
+ writeBehindTimer;
797
+ writeBehindFlushPromise;
651
798
  isDisconnecting = false;
652
799
  disconnectPromise;
653
800
  /**
@@ -657,9 +804,9 @@ var CacheStack = class extends EventEmitter {
657
804
  * and no `fetcher` is provided.
658
805
  */
659
806
  async get(key, fetcher, options) {
660
- const normalizedKey = this.validateCacheKey(key);
807
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
661
808
  this.validateWriteOptions(options);
662
- await this.startup;
809
+ await this.awaitStartup("get");
663
810
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
664
811
  if (hit.found) {
665
812
  this.ttlResolver.recordAccess(normalizedKey);
@@ -724,8 +871,8 @@ var CacheStack = class extends EventEmitter {
724
871
  * Returns true if the given key exists and is not expired in any layer.
725
872
  */
726
873
  async has(key) {
727
- const normalizedKey = this.validateCacheKey(key);
728
- await this.startup;
874
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
875
+ await this.awaitStartup("has");
729
876
  for (const layer of this.layers) {
730
877
  if (this.shouldSkipLayer(layer)) {
731
878
  continue;
@@ -755,8 +902,8 @@ var CacheStack = class extends EventEmitter {
755
902
  * that has it, or null if the key is not found / has no TTL.
756
903
  */
757
904
  async ttl(key) {
758
- const normalizedKey = this.validateCacheKey(key);
759
- await this.startup;
905
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
906
+ await this.awaitStartup("ttl");
760
907
  for (const layer of this.layers) {
761
908
  if (this.shouldSkipLayer(layer)) {
762
909
  continue;
@@ -777,17 +924,17 @@ var CacheStack = class extends EventEmitter {
777
924
  * Stores a value in all cache layers. Overwrites any existing value.
778
925
  */
779
926
  async set(key, value, options) {
780
- const normalizedKey = this.validateCacheKey(key);
927
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
781
928
  this.validateWriteOptions(options);
782
- await this.startup;
929
+ await this.awaitStartup("set");
783
930
  await this.storeEntry(normalizedKey, "value", value, options);
784
931
  }
785
932
  /**
786
933
  * Deletes the key from all layers and publishes an invalidation message.
787
934
  */
788
935
  async delete(key) {
789
- const normalizedKey = this.validateCacheKey(key);
790
- await this.startup;
936
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
937
+ await this.awaitStartup("delete");
791
938
  await this.deleteKeys([normalizedKey]);
792
939
  await this.publishInvalidation({
793
940
  scope: "key",
@@ -797,7 +944,7 @@ var CacheStack = class extends EventEmitter {
797
944
  });
798
945
  }
799
946
  async clear() {
800
- await this.startup;
947
+ await this.awaitStartup("clear");
801
948
  await Promise.all(this.layers.map((layer) => layer.clear()));
802
949
  await this.tagIndex.clear();
803
950
  this.ttlResolver.clearProfiles();
@@ -813,23 +960,25 @@ var CacheStack = class extends EventEmitter {
813
960
  if (keys.length === 0) {
814
961
  return;
815
962
  }
816
- await this.startup;
963
+ await this.awaitStartup("mdelete");
817
964
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
818
- await this.deleteKeys(normalizedKeys);
965
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
966
+ await this.deleteKeys(cacheKeys);
819
967
  await this.publishInvalidation({
820
968
  scope: "keys",
821
- keys: normalizedKeys,
969
+ keys: cacheKeys,
822
970
  sourceId: this.instanceId,
823
971
  operation: "delete"
824
972
  });
825
973
  }
826
974
  async mget(entries) {
975
+ this.assertActive("mget");
827
976
  if (entries.length === 0) {
828
977
  return [];
829
978
  }
830
979
  const normalizedEntries = entries.map((entry) => ({
831
980
  ...entry,
832
- key: this.validateCacheKey(entry.key)
981
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
833
982
  }));
834
983
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
835
984
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -855,7 +1004,7 @@ var CacheStack = class extends EventEmitter {
855
1004
  })
856
1005
  );
857
1006
  }
858
- await this.startup;
1007
+ await this.awaitStartup("mget");
859
1008
  const pending = /* @__PURE__ */ new Set();
860
1009
  const indexesByKey = /* @__PURE__ */ new Map();
861
1010
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -903,14 +1052,17 @@ var CacheStack = class extends EventEmitter {
903
1052
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
904
1053
  }
905
1054
  async mset(entries) {
1055
+ this.assertActive("mset");
906
1056
  const normalizedEntries = entries.map((entry) => ({
907
1057
  ...entry,
908
- key: this.validateCacheKey(entry.key)
1058
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
909
1059
  }));
910
1060
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
911
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1061
+ await this.awaitStartup("mset");
1062
+ await this.writeBatch(normalizedEntries);
912
1063
  }
913
1064
  async warm(entries, options = {}) {
1065
+ this.assertActive("warm");
914
1066
  const concurrency = Math.max(1, options.concurrency ?? 4);
915
1067
  const total = entries.length;
916
1068
  let completed = 0;
@@ -959,14 +1111,31 @@ var CacheStack = class extends EventEmitter {
959
1111
  return new CacheNamespace(this, prefix);
960
1112
  }
961
1113
  async invalidateByTag(tag) {
962
- await this.startup;
1114
+ await this.awaitStartup("invalidateByTag");
963
1115
  const keys = await this.tagIndex.keysForTag(tag);
964
1116
  await this.deleteKeys(keys);
965
1117
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
966
1118
  }
1119
+ async invalidateByTags(tags, mode = "any") {
1120
+ if (tags.length === 0) {
1121
+ return;
1122
+ }
1123
+ await this.awaitStartup("invalidateByTags");
1124
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1125
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1126
+ await this.deleteKeys(keys);
1127
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1128
+ }
967
1129
  async invalidateByPattern(pattern) {
968
- await this.startup;
969
- const keys = await this.tagIndex.matchPattern(pattern);
1130
+ await this.awaitStartup("invalidateByPattern");
1131
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1132
+ await this.deleteKeys(keys);
1133
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1134
+ }
1135
+ async invalidateByPrefix(prefix) {
1136
+ await this.awaitStartup("invalidateByPrefix");
1137
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1138
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
970
1139
  await this.deleteKeys(keys);
971
1140
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
972
1141
  }
@@ -993,14 +1162,43 @@ var CacheStack = class extends EventEmitter {
993
1162
  getHitRate() {
994
1163
  return this.metricsCollector.hitRate();
995
1164
  }
1165
+ async healthCheck() {
1166
+ await this.startup;
1167
+ return Promise.all(
1168
+ this.layers.map(async (layer) => {
1169
+ const startedAt = performance.now();
1170
+ try {
1171
+ const healthy = layer.ping ? await layer.ping() : true;
1172
+ return {
1173
+ layer: layer.name,
1174
+ healthy,
1175
+ latencyMs: performance.now() - startedAt
1176
+ };
1177
+ } catch (error) {
1178
+ return {
1179
+ layer: layer.name,
1180
+ healthy: false,
1181
+ latencyMs: performance.now() - startedAt,
1182
+ error: this.formatError(error)
1183
+ };
1184
+ }
1185
+ })
1186
+ );
1187
+ }
1188
+ bumpGeneration(nextGeneration) {
1189
+ const current = this.currentGeneration ?? 0;
1190
+ this.currentGeneration = nextGeneration ?? current + 1;
1191
+ return this.currentGeneration;
1192
+ }
996
1193
  /**
997
1194
  * Returns detailed metadata about a single cache key: which layers contain it,
998
1195
  * remaining fresh/stale/error TTLs, and associated tags.
999
1196
  * Returns `null` if the key does not exist in any layer.
1000
1197
  */
1001
1198
  async inspect(key) {
1002
- const normalizedKey = this.validateCacheKey(key);
1003
- await this.startup;
1199
+ const userKey = this.validateCacheKey(key);
1200
+ const normalizedKey = this.qualifyKey(userKey);
1201
+ await this.awaitStartup("inspect");
1004
1202
  const foundInLayers = [];
1005
1203
  let freshTtlSeconds = null;
1006
1204
  let staleTtlSeconds = null;
@@ -1031,10 +1229,10 @@ var CacheStack = class extends EventEmitter {
1031
1229
  return null;
1032
1230
  }
1033
1231
  const tags = await this.getTagsForKey(normalizedKey);
1034
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1232
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1035
1233
  }
1036
1234
  async exportState() {
1037
- await this.startup;
1235
+ await this.awaitStartup("exportState");
1038
1236
  const exported = /* @__PURE__ */ new Map();
1039
1237
  for (const layer of this.layers) {
1040
1238
  if (!layer.keys) {
@@ -1042,15 +1240,16 @@ var CacheStack = class extends EventEmitter {
1042
1240
  }
1043
1241
  const keys = await layer.keys();
1044
1242
  for (const key of keys) {
1045
- if (exported.has(key)) {
1243
+ const exportedKey = this.stripQualifiedKey(key);
1244
+ if (exported.has(exportedKey)) {
1046
1245
  continue;
1047
1246
  }
1048
1247
  const stored = await this.readLayerEntry(layer, key);
1049
1248
  if (stored === null) {
1050
1249
  continue;
1051
1250
  }
1052
- exported.set(key, {
1053
- key,
1251
+ exported.set(exportedKey, {
1252
+ key: exportedKey,
1054
1253
  value: stored,
1055
1254
  ttl: remainingStoredTtlSeconds(stored)
1056
1255
  });
@@ -1059,20 +1258,25 @@ var CacheStack = class extends EventEmitter {
1059
1258
  return [...exported.values()];
1060
1259
  }
1061
1260
  async importState(entries) {
1062
- await this.startup;
1261
+ await this.awaitStartup("importState");
1063
1262
  await Promise.all(
1064
1263
  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);
1264
+ const qualifiedKey = this.qualifyKey(entry.key);
1265
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1266
+ await this.tagIndex.touch(qualifiedKey);
1067
1267
  })
1068
1268
  );
1069
1269
  }
1070
1270
  async persistToFile(filePath) {
1271
+ this.assertActive("persistToFile");
1071
1272
  const snapshot = await this.exportState();
1072
- await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1273
+ const { promises: fs2 } = await import("fs");
1274
+ await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1073
1275
  }
1074
1276
  async restoreFromFile(filePath) {
1075
- const raw = await fs.readFile(filePath, "utf8");
1277
+ this.assertActive("restoreFromFile");
1278
+ const { promises: fs2 } = await import("fs");
1279
+ const raw = await fs2.readFile(filePath, "utf8");
1076
1280
  let parsed;
1077
1281
  try {
1078
1282
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1095,7 +1299,13 @@ var CacheStack = class extends EventEmitter {
1095
1299
  this.disconnectPromise = (async () => {
1096
1300
  await this.startup;
1097
1301
  await this.unsubscribeInvalidation?.();
1302
+ await this.flushWriteBehindQueue();
1098
1303
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1304
+ if (this.writeBehindTimer) {
1305
+ clearInterval(this.writeBehindTimer);
1306
+ this.writeBehindTimer = void 0;
1307
+ }
1308
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1099
1309
  })();
1100
1310
  }
1101
1311
  await this.disconnectPromise;
@@ -1155,7 +1365,11 @@ var CacheStack = class extends EventEmitter {
1155
1365
  const fetchStart = Date.now();
1156
1366
  let fetched;
1157
1367
  try {
1158
- fetched = await fetcher();
1368
+ fetched = await this.fetchRateLimiter.schedule(
1369
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1370
+ { key, fetcher },
1371
+ fetcher
1372
+ );
1159
1373
  this.circuitBreakerManager.recordSuccess(key);
1160
1374
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1161
1375
  } catch (error) {
@@ -1189,6 +1403,61 @@ var CacheStack = class extends EventEmitter {
1189
1403
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1190
1404
  }
1191
1405
  }
1406
+ async writeBatch(entries) {
1407
+ const now = Date.now();
1408
+ const entriesByLayer = /* @__PURE__ */ new Map();
1409
+ const immediateOperations = [];
1410
+ const deferredOperations = [];
1411
+ for (const entry of entries) {
1412
+ for (const layer of this.layers) {
1413
+ if (this.shouldSkipLayer(layer)) {
1414
+ continue;
1415
+ }
1416
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1417
+ const bucket = entriesByLayer.get(layer) ?? [];
1418
+ bucket.push(layerEntry);
1419
+ entriesByLayer.set(layer, bucket);
1420
+ }
1421
+ }
1422
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1423
+ const operation = async () => {
1424
+ try {
1425
+ if (layer.setMany) {
1426
+ await layer.setMany(layerEntries);
1427
+ return;
1428
+ }
1429
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1430
+ } catch (error) {
1431
+ await this.handleLayerFailure(layer, "write", error);
1432
+ }
1433
+ };
1434
+ if (this.shouldWriteBehind(layer)) {
1435
+ deferredOperations.push(operation);
1436
+ } else {
1437
+ immediateOperations.push(operation);
1438
+ }
1439
+ }
1440
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1441
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1442
+ for (const entry of entries) {
1443
+ if (entry.options?.tags) {
1444
+ await this.tagIndex.track(entry.key, entry.options.tags);
1445
+ } else {
1446
+ await this.tagIndex.touch(entry.key);
1447
+ }
1448
+ this.metricsCollector.increment("sets");
1449
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1450
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1451
+ }
1452
+ if (this.shouldBroadcastL1Invalidation()) {
1453
+ await this.publishInvalidation({
1454
+ scope: "keys",
1455
+ keys: entries.map((entry) => entry.key),
1456
+ sourceId: this.instanceId,
1457
+ operation: "write"
1458
+ });
1459
+ }
1460
+ }
1192
1461
  async readFromLayers(key, options, mode) {
1193
1462
  let sawRetainableValue = false;
1194
1463
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1272,33 +1541,28 @@ var CacheStack = class extends EventEmitter {
1272
1541
  }
1273
1542
  async writeAcrossLayers(key, kind, value, options) {
1274
1543
  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);
1544
+ const immediateOperations = [];
1545
+ const deferredOperations = [];
1546
+ for (const layer of this.layers) {
1547
+ const operation = async () => {
1548
+ if (this.shouldSkipLayer(layer)) {
1549
+ return;
1550
+ }
1551
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1552
+ try {
1553
+ await layer.set(entry.key, entry.value, entry.ttl);
1554
+ } catch (error) {
1555
+ await this.handleLayerFailure(layer, "write", error);
1556
+ }
1557
+ };
1558
+ if (this.shouldWriteBehind(layer)) {
1559
+ deferredOperations.push(operation);
1560
+ } else {
1561
+ immediateOperations.push(operation);
1299
1562
  }
1300
- });
1301
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1563
+ }
1564
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1565
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1302
1566
  }
1303
1567
  async executeLayerOperations(operations, context) {
1304
1568
  if (this.options.writePolicy !== "best-effort") {
@@ -1322,8 +1586,17 @@ var CacheStack = class extends EventEmitter {
1322
1586
  );
1323
1587
  }
1324
1588
  }
1325
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1326
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1589
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1590
+ return this.ttlResolver.resolveFreshTtl(
1591
+ key,
1592
+ layerName,
1593
+ kind,
1594
+ options,
1595
+ fallbackTtl,
1596
+ this.options.negativeTtl,
1597
+ void 0,
1598
+ value
1599
+ );
1327
1600
  }
1328
1601
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1329
1602
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1352,7 +1625,8 @@ var CacheStack = class extends EventEmitter {
1352
1625
  return {
1353
1626
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1354
1627
  waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1355
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
1628
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1629
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1356
1630
  };
1357
1631
  }
1358
1632
  async deleteKeys(keys) {
@@ -1412,11 +1686,110 @@ var CacheStack = class extends EventEmitter {
1412
1686
  return String(error);
1413
1687
  }
1414
1688
  sleep(ms) {
1415
- return new Promise((resolve) => setTimeout(resolve, ms));
1689
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1416
1690
  }
1417
1691
  shouldBroadcastL1Invalidation() {
1418
1692
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1419
1693
  }
1694
+ initializeWriteBehind(options) {
1695
+ if (this.options.writeStrategy !== "write-behind") {
1696
+ return;
1697
+ }
1698
+ const flushIntervalMs = options?.flushIntervalMs;
1699
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1700
+ return;
1701
+ }
1702
+ this.writeBehindTimer = setInterval(() => {
1703
+ void this.flushWriteBehindQueue();
1704
+ }, flushIntervalMs);
1705
+ this.writeBehindTimer.unref?.();
1706
+ }
1707
+ shouldWriteBehind(layer) {
1708
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
1709
+ }
1710
+ async enqueueWriteBehind(operation) {
1711
+ this.writeBehindQueue.push(operation);
1712
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1713
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
1714
+ if (this.writeBehindQueue.length >= batchSize) {
1715
+ await this.flushWriteBehindQueue();
1716
+ return;
1717
+ }
1718
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1719
+ await this.flushWriteBehindQueue();
1720
+ }
1721
+ }
1722
+ async flushWriteBehindQueue() {
1723
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1724
+ await this.writeBehindFlushPromise;
1725
+ return;
1726
+ }
1727
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
1728
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1729
+ this.writeBehindFlushPromise = (async () => {
1730
+ await Promise.allSettled(batch.map((operation) => operation()));
1731
+ })();
1732
+ await this.writeBehindFlushPromise;
1733
+ this.writeBehindFlushPromise = void 0;
1734
+ if (this.writeBehindQueue.length > 0) {
1735
+ await this.flushWriteBehindQueue();
1736
+ }
1737
+ }
1738
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
1739
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
1740
+ const staleWhileRevalidate = this.resolveLayerSeconds(
1741
+ layer.name,
1742
+ options?.staleWhileRevalidate,
1743
+ this.options.staleWhileRevalidate
1744
+ );
1745
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1746
+ const payload = createStoredValueEnvelope({
1747
+ kind,
1748
+ value,
1749
+ freshTtlSeconds: freshTtl,
1750
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
1751
+ staleIfErrorSeconds: staleIfError,
1752
+ now
1753
+ });
1754
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1755
+ return {
1756
+ key,
1757
+ value: payload,
1758
+ ttl
1759
+ };
1760
+ }
1761
+ intersectKeys(groups) {
1762
+ if (groups.length === 0) {
1763
+ return [];
1764
+ }
1765
+ const [firstGroup, ...rest] = groups;
1766
+ if (!firstGroup) {
1767
+ return [];
1768
+ }
1769
+ const restSets = rest.map((group) => new Set(group));
1770
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
1771
+ }
1772
+ qualifyKey(key) {
1773
+ const prefix = this.generationPrefix();
1774
+ return prefix ? `${prefix}${key}` : key;
1775
+ }
1776
+ qualifyPattern(pattern) {
1777
+ const prefix = this.generationPrefix();
1778
+ return prefix ? `${prefix}${pattern}` : pattern;
1779
+ }
1780
+ stripQualifiedKey(key) {
1781
+ const prefix = this.generationPrefix();
1782
+ if (!prefix || !key.startsWith(prefix)) {
1783
+ return key;
1784
+ }
1785
+ return key.slice(prefix.length);
1786
+ }
1787
+ generationPrefix() {
1788
+ if (this.currentGeneration === void 0) {
1789
+ return "";
1790
+ }
1791
+ return `v${this.currentGeneration}:`;
1792
+ }
1420
1793
  async deleteKeysFromLayers(layers, keys) {
1421
1794
  await Promise.all(
1422
1795
  layers.map(async (layer) => {
@@ -1458,8 +1831,13 @@ var CacheStack = class extends EventEmitter {
1458
1831
  this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1459
1832
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1460
1833
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1834
+ this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
1835
+ this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
1461
1836
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1462
1837
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
1838
+ if (this.options.generation !== void 0) {
1839
+ this.validateNonNegativeNumber("generation", this.options.generation);
1840
+ }
1463
1841
  }
1464
1842
  validateWriteOptions(options) {
1465
1843
  if (!options) {
@@ -1471,8 +1849,10 @@ var CacheStack = class extends EventEmitter {
1471
1849
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1472
1850
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1473
1851
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
1852
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1474
1853
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1475
1854
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1855
+ this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
1476
1856
  }
1477
1857
  validateLayerNumberOption(name, value) {
1478
1858
  if (value === void 0) {
@@ -1497,6 +1877,20 @@ var CacheStack = class extends EventEmitter {
1497
1877
  throw new Error(`${name} must be a positive finite number.`);
1498
1878
  }
1499
1879
  }
1880
+ validateRateLimitOptions(name, options) {
1881
+ if (!options) {
1882
+ return;
1883
+ }
1884
+ this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
1885
+ this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
1886
+ this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
1887
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
1888
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
1889
+ }
1890
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
1891
+ throw new Error(`${name}.bucketKey must not be empty.`);
1892
+ }
1893
+ }
1500
1894
  validateNonNegativeNumber(name, value) {
1501
1895
  if (!Number.isFinite(value) || value < 0) {
1502
1896
  throw new Error(`${name} must be a non-negative finite number.`);
@@ -1514,6 +1908,26 @@ var CacheStack = class extends EventEmitter {
1514
1908
  }
1515
1909
  return key;
1516
1910
  }
1911
+ validateTtlPolicy(name, policy) {
1912
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
1913
+ return;
1914
+ }
1915
+ if ("alignTo" in policy) {
1916
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
1917
+ return;
1918
+ }
1919
+ throw new Error(`${name} is invalid.`);
1920
+ }
1921
+ assertActive(operation) {
1922
+ if (this.isDisconnecting) {
1923
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
1924
+ }
1925
+ }
1926
+ async awaitStartup(operation) {
1927
+ this.assertActive(operation);
1928
+ await this.startup;
1929
+ this.assertActive(operation);
1930
+ }
1517
1931
  serializeOptions(options) {
1518
1932
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1519
1933
  }
@@ -1619,18 +2033,23 @@ var CacheStack = class extends EventEmitter {
1619
2033
  return value;
1620
2034
  }
1621
2035
  };
2036
+ function createInstanceId() {
2037
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2038
+ }
1622
2039
 
1623
2040
  // src/invalidation/RedisInvalidationBus.ts
1624
2041
  var RedisInvalidationBus = class {
1625
2042
  channel;
1626
2043
  publisher;
1627
2044
  subscriber;
2045
+ logger;
1628
2046
  handlers = /* @__PURE__ */ new Set();
1629
2047
  sharedListener;
1630
2048
  constructor(options) {
1631
2049
  this.publisher = options.publisher;
1632
2050
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
1633
2051
  this.channel = options.channel ?? "layercache:invalidation";
2052
+ this.logger = options.logger;
1634
2053
  }
1635
2054
  async subscribe(handler) {
1636
2055
  if (this.handlers.size === 0) {
@@ -1687,6 +2106,10 @@ var RedisInvalidationBus = class {
1687
2106
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
1688
2107
  }
1689
2108
  reportError(message, error) {
2109
+ if (this.logger?.error) {
2110
+ this.logger.error(message, { error });
2111
+ return;
2112
+ }
1690
2113
  console.error(`[layercache] ${message}`, error);
1691
2114
  }
1692
2115
  };
@@ -1739,32 +2162,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
1739
2162
  function createExpressCacheMiddleware(cache, options = {}) {
1740
2163
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
1741
2164
  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));
2165
+ try {
2166
+ const method = (req.method ?? "GET").toUpperCase();
2167
+ if (!allowedMethods.has(method)) {
2168
+ next();
2169
+ return;
1756
2170
  }
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
- };
2171
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
2172
+ const cached = await cache.get(key, void 0, options);
2173
+ if (cached !== null) {
2174
+ res.setHeader?.("content-type", "application/json; charset=utf-8");
2175
+ res.setHeader?.("x-cache", "HIT");
2176
+ if (res.json) {
2177
+ res.json(cached);
2178
+ } else {
2179
+ res.end?.(JSON.stringify(cached));
2180
+ }
2181
+ return;
2182
+ }
2183
+ const originalJson = res.json?.bind(res);
2184
+ if (originalJson) {
2185
+ res.json = (body) => {
2186
+ res.setHeader?.("x-cache", "MISS");
2187
+ void cache.set(key, body, options);
2188
+ return originalJson(body);
2189
+ };
2190
+ }
2191
+ next();
2192
+ } catch (error) {
2193
+ next(error);
1766
2194
  }
1767
- next();
1768
2195
  };
1769
2196
  }
1770
2197
 
@@ -1777,6 +2204,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
1777
2204
  return (...args) => wrapped(...args);
1778
2205
  }
1779
2206
 
2207
+ // src/integrations/opentelemetry.ts
2208
+ function createOpenTelemetryPlugin(cache, tracer) {
2209
+ const originals = {
2210
+ get: cache.get.bind(cache),
2211
+ set: cache.set.bind(cache),
2212
+ delete: cache.delete.bind(cache),
2213
+ mget: cache.mget.bind(cache),
2214
+ mset: cache.mset.bind(cache),
2215
+ invalidateByTag: cache.invalidateByTag.bind(cache),
2216
+ invalidateByTags: cache.invalidateByTags.bind(cache),
2217
+ invalidateByPattern: cache.invalidateByPattern.bind(cache),
2218
+ invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
2219
+ };
2220
+ cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
2221
+ "layercache.key": String(args[0] ?? "")
2222
+ }));
2223
+ cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
2224
+ "layercache.key": String(args[0] ?? "")
2225
+ }));
2226
+ cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
2227
+ "layercache.key": String(args[0] ?? "")
2228
+ }));
2229
+ cache.mget = instrument("layercache.mget", tracer, originals.mget);
2230
+ cache.mset = instrument("layercache.mset", tracer, originals.mset);
2231
+ cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
2232
+ cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
2233
+ cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
2234
+ cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
2235
+ return {
2236
+ uninstall() {
2237
+ cache.get = originals.get;
2238
+ cache.set = originals.set;
2239
+ cache.delete = originals.delete;
2240
+ cache.mget = originals.mget;
2241
+ cache.mset = originals.mset;
2242
+ cache.invalidateByTag = originals.invalidateByTag;
2243
+ cache.invalidateByTags = originals.invalidateByTags;
2244
+ cache.invalidateByPattern = originals.invalidateByPattern;
2245
+ cache.invalidateByPrefix = originals.invalidateByPrefix;
2246
+ }
2247
+ };
2248
+ }
2249
+ function instrument(name, tracer, method, attributes) {
2250
+ return (async (...args) => {
2251
+ const span = tracer.startSpan(name, { attributes: attributes?.(args) });
2252
+ try {
2253
+ const result = await method(...args);
2254
+ span.setAttribute?.("layercache.success", true);
2255
+ if (result === null) {
2256
+ span.setAttribute?.("layercache.result", "null");
2257
+ }
2258
+ return result;
2259
+ } catch (error) {
2260
+ span.setAttribute?.("layercache.success", false);
2261
+ span.recordException?.(error);
2262
+ throw error;
2263
+ } finally {
2264
+ span.end();
2265
+ }
2266
+ });
2267
+ }
2268
+
1780
2269
  // src/integrations/trpc.ts
1781
2270
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1782
2271
  return async (context) => {
@@ -1802,177 +2291,40 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
1802
2291
  };
1803
2292
  }
1804
2293
 
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
2294
  // src/layers/RedisLayer.ts
1963
2295
  import { promisify } from "util";
1964
2296
  import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
1965
2297
 
1966
2298
  // src/serialization/JsonSerializer.ts
2299
+ var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1967
2300
  var JsonSerializer = class {
1968
2301
  serialize(value) {
1969
2302
  return JSON.stringify(value);
1970
2303
  }
1971
2304
  deserialize(payload) {
1972
2305
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1973
- return JSON.parse(normalized);
2306
+ return sanitizeJsonValue(JSON.parse(normalized));
1974
2307
  }
1975
2308
  };
2309
+ function sanitizeJsonValue(value) {
2310
+ if (Array.isArray(value)) {
2311
+ return value.map((entry) => sanitizeJsonValue(entry));
2312
+ }
2313
+ if (!isPlainObject(value)) {
2314
+ return value;
2315
+ }
2316
+ const sanitized = {};
2317
+ for (const [key, entry] of Object.entries(value)) {
2318
+ if (DANGEROUS_JSON_KEYS.has(key)) {
2319
+ continue;
2320
+ }
2321
+ sanitized[key] = sanitizeJsonValue(entry);
2322
+ }
2323
+ return sanitized;
2324
+ }
2325
+ function isPlainObject(value) {
2326
+ return Object.prototype.toString.call(value) === "[object Object]";
2327
+ }
1976
2328
 
1977
2329
  // src/layers/RedisLayer.ts
1978
2330
  var BATCH_DELETE_SIZE = 500;
@@ -1985,22 +2337,24 @@ var RedisLayer = class {
1985
2337
  defaultTtl;
1986
2338
  isLocal = false;
1987
2339
  client;
1988
- serializer;
2340
+ serializers;
1989
2341
  prefix;
1990
2342
  allowUnprefixedClear;
1991
2343
  scanCount;
1992
2344
  compression;
1993
2345
  compressionThreshold;
2346
+ disconnectOnDispose;
1994
2347
  constructor(options) {
1995
2348
  this.client = options.client;
1996
2349
  this.defaultTtl = options.ttl;
1997
2350
  this.name = options.name ?? "redis";
1998
- this.serializer = options.serializer ?? new JsonSerializer();
2351
+ this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
1999
2352
  this.prefix = options.prefix ?? "";
2000
2353
  this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
2001
2354
  this.scanCount = options.scanCount ?? 100;
2002
2355
  this.compression = options.compression;
2003
2356
  this.compressionThreshold = options.compressionThreshold ?? 1024;
2357
+ this.disconnectOnDispose = options.disconnectOnDispose ?? false;
2004
2358
  }
2005
2359
  async get(key) {
2006
2360
  const payload = await this.getEntry(key);
@@ -2035,8 +2389,25 @@ var RedisLayer = class {
2035
2389
  })
2036
2390
  );
2037
2391
  }
2392
+ async setMany(entries) {
2393
+ if (entries.length === 0) {
2394
+ return;
2395
+ }
2396
+ const pipeline = this.client.pipeline();
2397
+ for (const entry of entries) {
2398
+ const serialized = this.primarySerializer().serialize(entry.value);
2399
+ const payload = await this.encodePayload(serialized);
2400
+ const normalizedKey = this.withPrefix(entry.key);
2401
+ if (entry.ttl && entry.ttl > 0) {
2402
+ pipeline.set(normalizedKey, payload, "EX", entry.ttl);
2403
+ } else {
2404
+ pipeline.set(normalizedKey, payload);
2405
+ }
2406
+ }
2407
+ await pipeline.exec();
2408
+ }
2038
2409
  async set(key, value, ttl = this.defaultTtl) {
2039
- const serialized = this.serializer.serialize(value);
2410
+ const serialized = this.primarySerializer().serialize(value);
2040
2411
  const payload = await this.encodePayload(serialized);
2041
2412
  const normalizedKey = this.withPrefix(key);
2042
2413
  if (ttl && ttl > 0) {
@@ -2069,6 +2440,18 @@ var RedisLayer = class {
2069
2440
  const keys = await this.keys();
2070
2441
  return keys.length;
2071
2442
  }
2443
+ async ping() {
2444
+ try {
2445
+ return await this.client.ping() === "PONG";
2446
+ } catch {
2447
+ return false;
2448
+ }
2449
+ }
2450
+ async dispose() {
2451
+ if (this.disconnectOnDispose) {
2452
+ this.client.disconnect();
2453
+ }
2454
+ }
2072
2455
  /**
2073
2456
  * Deletes all keys matching the layer's prefix in batches to avoid
2074
2457
  * loading millions of keys into memory at once.
@@ -2115,12 +2498,39 @@ var RedisLayer = class {
2115
2498
  return `${this.prefix}${key}`;
2116
2499
  }
2117
2500
  async deserializeOrDelete(key, payload) {
2501
+ const decodedPayload = await this.decodePayload(payload);
2502
+ for (const serializer of this.serializers) {
2503
+ try {
2504
+ const value = serializer.deserialize(decodedPayload);
2505
+ if (serializer !== this.primarySerializer()) {
2506
+ await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
2507
+ }
2508
+ return value;
2509
+ } catch {
2510
+ }
2511
+ }
2118
2512
  try {
2119
- return this.serializer.deserialize(await this.decodePayload(payload));
2120
- } catch {
2121
2513
  await this.client.del(this.withPrefix(key)).catch(() => void 0);
2122
- return null;
2514
+ } catch {
2515
+ }
2516
+ return null;
2517
+ }
2518
+ async rewriteWithPrimarySerializer(key, value) {
2519
+ const serialized = this.primarySerializer().serialize(value);
2520
+ const payload = await this.encodePayload(serialized);
2521
+ const ttl = await this.client.ttl(this.withPrefix(key));
2522
+ if (ttl > 0) {
2523
+ await this.client.set(this.withPrefix(key), payload, "EX", ttl);
2524
+ return;
2525
+ }
2526
+ await this.client.set(this.withPrefix(key), payload);
2527
+ }
2528
+ primarySerializer() {
2529
+ const serializer = this.serializers[0];
2530
+ if (!serializer) {
2531
+ throw new Error("RedisLayer requires at least one serializer.");
2123
2532
  }
2533
+ return serializer;
2124
2534
  }
2125
2535
  isSerializablePayload(payload) {
2126
2536
  return typeof payload === "string" || Buffer.isBuffer(payload);
@@ -2160,8 +2570,8 @@ var RedisLayer = class {
2160
2570
 
2161
2571
  // src/layers/DiskLayer.ts
2162
2572
  import { createHash } from "crypto";
2163
- import { promises as fs2 } from "fs";
2164
- import { join } from "path";
2573
+ import { promises as fs } from "fs";
2574
+ import { join, resolve } from "path";
2165
2575
  var DiskLayer = class {
2166
2576
  name;
2167
2577
  defaultTtl;
@@ -2169,12 +2579,13 @@ var DiskLayer = class {
2169
2579
  directory;
2170
2580
  serializer;
2171
2581
  maxFiles;
2582
+ writeQueue = Promise.resolve();
2172
2583
  constructor(options) {
2173
- this.directory = options.directory;
2584
+ this.directory = this.resolveDirectory(options.directory);
2174
2585
  this.defaultTtl = options.ttl;
2175
2586
  this.name = options.name ?? "disk";
2176
2587
  this.serializer = options.serializer ?? new JsonSerializer();
2177
- this.maxFiles = options.maxFiles;
2588
+ this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
2178
2589
  }
2179
2590
  async get(key) {
2180
2591
  return unwrapStoredValue(await this.getEntry(key));
@@ -2183,13 +2594,13 @@ var DiskLayer = class {
2183
2594
  const filePath = this.keyToPath(key);
2184
2595
  let raw;
2185
2596
  try {
2186
- raw = await fs2.readFile(filePath);
2597
+ raw = await fs.readFile(filePath);
2187
2598
  } catch {
2188
2599
  return null;
2189
2600
  }
2190
2601
  let entry;
2191
2602
  try {
2192
- entry = this.serializer.deserialize(raw);
2603
+ entry = this.deserializeEntry(raw);
2193
2604
  } catch {
2194
2605
  await this.safeDelete(filePath);
2195
2606
  return null;
@@ -2201,16 +2612,29 @@ var DiskLayer = class {
2201
2612
  return entry.value;
2202
2613
  }
2203
2614
  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();
2615
+ await this.enqueueWrite(async () => {
2616
+ await fs.mkdir(this.directory, { recursive: true });
2617
+ const entry = {
2618
+ key,
2619
+ value,
2620
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
2621
+ };
2622
+ const payload = this.serializer.serialize(entry);
2623
+ const targetPath = this.keyToPath(key);
2624
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
2625
+ await fs.writeFile(tempPath, payload);
2626
+ await fs.rename(tempPath, targetPath);
2627
+ if (this.maxFiles !== void 0) {
2628
+ await this.enforceMaxFiles();
2629
+ }
2630
+ });
2631
+ }
2632
+ async getMany(keys) {
2633
+ return Promise.all(keys.map((key) => this.getEntry(key)));
2634
+ }
2635
+ async setMany(entries) {
2636
+ for (const entry of entries) {
2637
+ await this.set(entry.key, entry.value, entry.ttl);
2214
2638
  }
2215
2639
  }
2216
2640
  async has(key) {
@@ -2221,14 +2645,15 @@ var DiskLayer = class {
2221
2645
  const filePath = this.keyToPath(key);
2222
2646
  let raw;
2223
2647
  try {
2224
- raw = await fs2.readFile(filePath);
2648
+ raw = await fs.readFile(filePath);
2225
2649
  } catch {
2226
2650
  return null;
2227
2651
  }
2228
2652
  let entry;
2229
2653
  try {
2230
- entry = this.serializer.deserialize(raw);
2654
+ entry = this.deserializeEntry(raw);
2231
2655
  } catch {
2656
+ await this.safeDelete(filePath);
2232
2657
  return null;
2233
2658
  }
2234
2659
  if (entry.expiresAt === null) {
@@ -2241,21 +2666,25 @@ var DiskLayer = class {
2241
2666
  return remaining;
2242
2667
  }
2243
2668
  async delete(key) {
2244
- await this.safeDelete(this.keyToPath(key));
2669
+ await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
2245
2670
  }
2246
2671
  async deleteMany(keys) {
2247
- await Promise.all(keys.map((key) => this.delete(key)));
2672
+ await this.enqueueWrite(async () => {
2673
+ await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
2674
+ });
2248
2675
  }
2249
2676
  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
- );
2677
+ await this.enqueueWrite(async () => {
2678
+ let entries;
2679
+ try {
2680
+ entries = await fs.readdir(this.directory);
2681
+ } catch {
2682
+ return;
2683
+ }
2684
+ await Promise.all(
2685
+ entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
2686
+ );
2687
+ });
2259
2688
  }
2260
2689
  /**
2261
2690
  * Returns the original cache key strings stored on disk.
@@ -2264,7 +2693,7 @@ var DiskLayer = class {
2264
2693
  async keys() {
2265
2694
  let entries;
2266
2695
  try {
2267
- entries = await fs2.readdir(this.directory);
2696
+ entries = await fs.readdir(this.directory);
2268
2697
  } catch {
2269
2698
  return [];
2270
2699
  }
@@ -2275,13 +2704,13 @@ var DiskLayer = class {
2275
2704
  const filePath = join(this.directory, name);
2276
2705
  let raw;
2277
2706
  try {
2278
- raw = await fs2.readFile(filePath);
2707
+ raw = await fs.readFile(filePath);
2279
2708
  } catch {
2280
2709
  return;
2281
2710
  }
2282
2711
  let entry;
2283
2712
  try {
2284
- entry = this.serializer.deserialize(raw);
2713
+ entry = this.deserializeEntry(raw);
2285
2714
  } catch {
2286
2715
  await this.safeDelete(filePath);
2287
2716
  return;
@@ -2299,16 +2728,56 @@ var DiskLayer = class {
2299
2728
  const keys = await this.keys();
2300
2729
  return keys.length;
2301
2730
  }
2731
+ async ping() {
2732
+ try {
2733
+ await fs.mkdir(this.directory, { recursive: true });
2734
+ return true;
2735
+ } catch {
2736
+ return false;
2737
+ }
2738
+ }
2739
+ async dispose() {
2740
+ }
2302
2741
  keyToPath(key) {
2303
2742
  const hash = createHash("sha256").update(key).digest("hex");
2304
2743
  return join(this.directory, `${hash}.lc`);
2305
2744
  }
2745
+ resolveDirectory(directory) {
2746
+ if (typeof directory !== "string" || directory.trim().length === 0) {
2747
+ throw new Error("DiskLayer.directory must be a non-empty path.");
2748
+ }
2749
+ if (directory.includes("\0")) {
2750
+ throw new Error("DiskLayer.directory must not contain null bytes.");
2751
+ }
2752
+ return resolve(directory);
2753
+ }
2754
+ normalizeMaxFiles(maxFiles) {
2755
+ if (maxFiles === void 0) {
2756
+ return void 0;
2757
+ }
2758
+ if (!Number.isInteger(maxFiles) || maxFiles <= 0) {
2759
+ throw new Error("DiskLayer.maxFiles must be a positive integer.");
2760
+ }
2761
+ return maxFiles;
2762
+ }
2763
+ deserializeEntry(raw) {
2764
+ const entry = this.serializer.deserialize(raw);
2765
+ if (!isDiskEntry(entry)) {
2766
+ throw new Error("Invalid disk cache entry.");
2767
+ }
2768
+ return entry;
2769
+ }
2306
2770
  async safeDelete(filePath) {
2307
2771
  try {
2308
- await fs2.unlink(filePath);
2772
+ await fs.unlink(filePath);
2309
2773
  } catch {
2310
2774
  }
2311
2775
  }
2776
+ enqueueWrite(operation) {
2777
+ const next = this.writeQueue.then(operation, operation);
2778
+ this.writeQueue = next.catch(() => void 0);
2779
+ return next;
2780
+ }
2312
2781
  /**
2313
2782
  * Removes the oldest files (by mtime) when the directory exceeds maxFiles.
2314
2783
  */
@@ -2318,7 +2787,7 @@ var DiskLayer = class {
2318
2787
  }
2319
2788
  let entries;
2320
2789
  try {
2321
- entries = await fs2.readdir(this.directory);
2790
+ entries = await fs.readdir(this.directory);
2322
2791
  } catch {
2323
2792
  return;
2324
2793
  }
@@ -2330,7 +2799,7 @@ var DiskLayer = class {
2330
2799
  lcFiles.map(async (name) => {
2331
2800
  const filePath = join(this.directory, name);
2332
2801
  try {
2333
- const stat = await fs2.stat(filePath);
2802
+ const stat = await fs.stat(filePath);
2334
2803
  return { filePath, mtimeMs: stat.mtimeMs };
2335
2804
  } catch {
2336
2805
  return { filePath, mtimeMs: 0 };
@@ -2342,6 +2811,14 @@ var DiskLayer = class {
2342
2811
  await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
2343
2812
  }
2344
2813
  };
2814
+ function isDiskEntry(value) {
2815
+ if (!value || typeof value !== "object") {
2816
+ return false;
2817
+ }
2818
+ const candidate = value;
2819
+ const validExpiry = candidate.expiresAt === null || typeof candidate.expiresAt === "number";
2820
+ return typeof candidate.key === "string" && validExpiry && "value" in candidate;
2821
+ }
2345
2822
 
2346
2823
  // src/layers/MemcachedLayer.ts
2347
2824
  var MemcachedLayer = class {
@@ -2414,13 +2891,19 @@ var MsgpackSerializer = class {
2414
2891
  };
2415
2892
 
2416
2893
  // src/singleflight/RedisSingleFlightCoordinator.ts
2417
- import { randomUUID as randomUUID2 } from "crypto";
2894
+ import { randomUUID } from "crypto";
2418
2895
  var RELEASE_SCRIPT = `
2419
2896
  if redis.call("get", KEYS[1]) == ARGV[1] then
2420
2897
  return redis.call("del", KEYS[1])
2421
2898
  end
2422
2899
  return 0
2423
2900
  `;
2901
+ var RENEW_SCRIPT = `
2902
+ if redis.call("get", KEYS[1]) == ARGV[1] then
2903
+ return redis.call("pexpire", KEYS[1], ARGV[2])
2904
+ end
2905
+ return 0
2906
+ `;
2424
2907
  var RedisSingleFlightCoordinator = class {
2425
2908
  client;
2426
2909
  prefix;
@@ -2430,17 +2913,32 @@ var RedisSingleFlightCoordinator = class {
2430
2913
  }
2431
2914
  async execute(key, options, worker, waiter) {
2432
2915
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
2433
- const token = randomUUID2();
2916
+ const token = randomUUID();
2434
2917
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
2435
2918
  if (acquired === "OK") {
2919
+ const renewTimer = this.startLeaseRenewal(lockKey, token, options);
2436
2920
  try {
2437
2921
  return await worker();
2438
2922
  } finally {
2923
+ if (renewTimer) {
2924
+ clearInterval(renewTimer);
2925
+ }
2439
2926
  await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
2440
2927
  }
2441
2928
  }
2442
2929
  return waiter();
2443
2930
  }
2931
+ startLeaseRenewal(lockKey, token, options) {
2932
+ const renewIntervalMs = options.renewIntervalMs ?? Math.max(100, Math.floor(options.leaseMs / 2));
2933
+ if (renewIntervalMs <= 0 || renewIntervalMs >= options.leaseMs) {
2934
+ return void 0;
2935
+ }
2936
+ const timer = setInterval(() => {
2937
+ void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
2938
+ }, renewIntervalMs);
2939
+ timer.unref?.();
2940
+ return timer;
2941
+ }
2444
2942
  };
2445
2943
 
2446
2944
  // src/metrics/PrometheusExporter.ts
@@ -2542,6 +3040,8 @@ export {
2542
3040
  createCachedMethodDecorator,
2543
3041
  createExpressCacheMiddleware,
2544
3042
  createFastifyLayercachePlugin,
3043
+ createHonoCacheMiddleware,
3044
+ createOpenTelemetryPlugin,
2545
3045
  createPrometheusMetricsExporter,
2546
3046
  createTrpcCacheMiddleware
2547
3047
  };