layercache 1.2.4 → 1.2.6

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,11 +1,11 @@
1
1
  import {
2
2
  RedisTagIndex
3
- } from "./chunk-QHWG7QS5.js";
3
+ } from "./chunk-BQLL6IM5.js";
4
4
  import {
5
5
  MemoryLayer,
6
6
  TagIndex,
7
7
  createHonoCacheMiddleware
8
- } from "./chunk-KOYGHLVP.js";
8
+ } from "./chunk-GJBKCFE6.js";
9
9
  import {
10
10
  PatternMatcher,
11
11
  createStoredValueEnvelope,
@@ -15,7 +15,7 @@ import {
15
15
  remainingStoredTtlSeconds,
16
16
  resolveStoredValue,
17
17
  unwrapStoredValue
18
- } from "./chunk-7V7XAB74.js";
18
+ } from "./chunk-4PPBOOXT.js";
19
19
 
20
20
  // src/CacheStack.ts
21
21
  import { EventEmitter } from "events";
@@ -26,22 +26,23 @@ var CacheNamespace = class _CacheNamespace {
26
26
  constructor(cache, prefix) {
27
27
  this.cache = cache;
28
28
  this.prefix = prefix;
29
+ validateNamespaceKey(prefix);
29
30
  }
30
31
  cache;
31
32
  prefix;
32
33
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
33
34
  metrics = emptyMetrics();
34
35
  async get(key, fetcher, options) {
35
- return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
36
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
36
37
  }
37
38
  async getOrSet(key, fetcher, options) {
38
- return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
39
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
39
40
  }
40
41
  /**
41
42
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
42
43
  */
43
44
  async getOrThrow(key, fetcher, options) {
44
- return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
45
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
45
46
  }
46
47
  async has(key) {
47
48
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
@@ -50,7 +51,7 @@ var CacheNamespace = class _CacheNamespace {
50
51
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
51
52
  }
52
53
  async set(key, value, options) {
53
- await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
54
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
54
55
  }
55
56
  async delete(key) {
56
57
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
@@ -66,7 +67,8 @@ var CacheNamespace = class _CacheNamespace {
66
67
  () => this.cache.mget(
67
68
  entries.map((entry) => ({
68
69
  ...entry,
69
- key: this.qualify(entry.key)
70
+ key: this.qualify(entry.key),
71
+ options: this.qualifyGetOptions(entry.options)
70
72
  }))
71
73
  )
72
74
  );
@@ -76,16 +78,22 @@ var CacheNamespace = class _CacheNamespace {
76
78
  () => this.cache.mset(
77
79
  entries.map((entry) => ({
78
80
  ...entry,
79
- key: this.qualify(entry.key)
81
+ key: this.qualify(entry.key),
82
+ options: this.qualifyWriteOptions(entry.options)
80
83
  }))
81
84
  )
82
85
  );
83
86
  }
84
87
  async invalidateByTag(tag) {
85
- await this.trackMetrics(() => this.cache.invalidateByTag(tag));
88
+ await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
86
89
  }
87
90
  async invalidateByTags(tags, mode = "any") {
88
- await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
91
+ await this.trackMetrics(
92
+ () => this.cache.invalidateByTags(
93
+ tags.map((tag) => this.qualifyTag(tag)),
94
+ mode
95
+ )
96
+ );
89
97
  }
90
98
  async invalidateByPattern(pattern) {
91
99
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
@@ -97,16 +105,24 @@ var CacheNamespace = class _CacheNamespace {
97
105
  * Returns detailed metadata about a single cache key within this namespace.
98
106
  */
99
107
  async inspect(key) {
100
- return this.cache.inspect(this.qualify(key));
108
+ const result = await this.cache.inspect(this.qualify(key));
109
+ if (result === null) {
110
+ return null;
111
+ }
112
+ return {
113
+ ...result,
114
+ tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
115
+ };
101
116
  }
102
117
  wrap(keyPrefix, fetcher, options) {
103
- return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
118
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
104
119
  }
105
120
  warm(entries, options) {
106
121
  return this.cache.warm(
107
122
  entries.map((entry) => ({
108
123
  ...entry,
109
- key: this.qualify(entry.key)
124
+ key: this.qualify(entry.key),
125
+ options: this.qualifyGetOptions(entry.options)
110
126
  })),
111
127
  options
112
128
  );
@@ -136,11 +152,30 @@ var CacheNamespace = class _CacheNamespace {
136
152
  * ```
137
153
  */
138
154
  namespace(childPrefix) {
155
+ validateNamespaceKey(childPrefix);
139
156
  return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
140
157
  }
141
158
  qualify(key) {
142
159
  return `${this.prefix}:${key}`;
143
160
  }
161
+ qualifyTag(tag) {
162
+ return `${this.prefix}:${tag}`;
163
+ }
164
+ qualifyGetOptions(options) {
165
+ return this.qualifyWriteOptions(options);
166
+ }
167
+ qualifyWrapOptions(options) {
168
+ return this.qualifyWriteOptions(options);
169
+ }
170
+ qualifyWriteOptions(options) {
171
+ if (!options?.tags || options.tags.length === 0) {
172
+ return options;
173
+ }
174
+ return {
175
+ ...options,
176
+ tags: options.tags.map((tag) => this.qualifyTag(tag))
177
+ };
178
+ }
144
179
  async trackMetrics(operation) {
145
180
  return this.getMetricsMutex().runExclusive(async () => {
146
181
  const before = this.cache.getMetrics();
@@ -265,6 +300,20 @@ function addMap(base, delta) {
265
300
  }
266
301
  return result;
267
302
  }
303
+ function validateNamespaceKey(key) {
304
+ if (key.length === 0) {
305
+ throw new Error("Namespace prefix must not be empty.");
306
+ }
307
+ if (key.length > 256) {
308
+ throw new Error("Namespace prefix must be at most 256 characters.");
309
+ }
310
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
311
+ throw new Error("Namespace prefix contains unsupported control characters.");
312
+ }
313
+ if (/[\uD800-\uDFFF]/.test(key)) {
314
+ throw new Error("Namespace prefix contains unsupported surrogate code points.");
315
+ }
316
+ }
268
317
 
269
318
  // src/internal/CacheKeyDiscovery.ts
270
319
  var CacheKeyDiscovery = class {
@@ -272,21 +321,41 @@ var CacheKeyDiscovery = class {
272
321
  this.options = options;
273
322
  }
274
323
  options;
275
- async collectKeysWithPrefix(prefix) {
324
+ async collectKeysWithPrefix(prefix, maxMatches = false) {
276
325
  const { tagIndex } = this.options;
277
- const matches = new Set(
278
- tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
279
- );
326
+ const matches = /* @__PURE__ */ new Set();
327
+ if (tagIndex.forEachKeyForPrefix) {
328
+ await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
329
+ matches.add(key);
330
+ this.assertWithinMatchLimit(matches, maxMatches);
331
+ });
332
+ } else {
333
+ const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
334
+ for (const key of initialMatches) {
335
+ matches.add(key);
336
+ this.assertWithinMatchLimit(matches, maxMatches);
337
+ }
338
+ }
280
339
  await Promise.all(
281
340
  this.options.layers.map(async (layer) => {
282
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
341
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
283
342
  return;
284
343
  }
285
344
  try {
286
- const keys = await layer.keys();
287
- for (const key of keys) {
345
+ if (layer.forEachKey) {
346
+ await layer.forEachKey(async (key) => {
347
+ if (key.startsWith(prefix)) {
348
+ matches.add(key);
349
+ this.assertWithinMatchLimit(matches, maxMatches);
350
+ }
351
+ });
352
+ return;
353
+ }
354
+ const keys = await layer.keys?.();
355
+ for (const key of keys ?? []) {
288
356
  if (key.startsWith(prefix)) {
289
357
  matches.add(key);
358
+ this.assertWithinMatchLimit(matches, maxMatches);
290
359
  }
291
360
  }
292
361
  } catch (error) {
@@ -296,18 +365,39 @@ var CacheKeyDiscovery = class {
296
365
  );
297
366
  return [...matches];
298
367
  }
299
- async collectKeysMatchingPattern(pattern) {
300
- const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
368
+ async collectKeysMatchingPattern(pattern, maxMatches = false) {
369
+ const matches = /* @__PURE__ */ new Set();
370
+ if (this.options.tagIndex.forEachKeyMatchingPattern) {
371
+ await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
372
+ matches.add(key);
373
+ this.assertWithinMatchLimit(matches, maxMatches);
374
+ });
375
+ } else {
376
+ for (const key of await this.options.tagIndex.matchPattern(pattern)) {
377
+ matches.add(key);
378
+ this.assertWithinMatchLimit(matches, maxMatches);
379
+ }
380
+ }
301
381
  await Promise.all(
302
382
  this.options.layers.map(async (layer) => {
303
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
383
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
304
384
  return;
305
385
  }
306
386
  try {
307
- const keys = await layer.keys();
308
- for (const key of keys) {
387
+ if (layer.forEachKey) {
388
+ await layer.forEachKey(async (key) => {
389
+ if (PatternMatcher.matches(pattern, key)) {
390
+ matches.add(key);
391
+ this.assertWithinMatchLimit(matches, maxMatches);
392
+ }
393
+ });
394
+ return;
395
+ }
396
+ const keys = await layer.keys?.();
397
+ for (const key of keys ?? []) {
309
398
  if (PatternMatcher.matches(pattern, key)) {
310
399
  matches.add(key);
400
+ this.assertWithinMatchLimit(matches, maxMatches);
311
401
  }
312
402
  }
313
403
  } catch (error) {
@@ -317,8 +407,280 @@ var CacheKeyDiscovery = class {
317
407
  );
318
408
  return [...matches];
319
409
  }
410
+ assertWithinMatchLimit(matches, maxMatches) {
411
+ if (maxMatches !== false && matches.size > maxMatches) {
412
+ throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
413
+ }
414
+ }
320
415
  };
321
416
 
417
+ // src/internal/CacheKeySerialization.ts
418
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
419
+ function normalizeForSerialization(value) {
420
+ if (Array.isArray(value)) {
421
+ return value.map((entry) => normalizeForSerialization(entry));
422
+ }
423
+ if (value && typeof value === "object") {
424
+ return Object.keys(value).sort().reduce((normalized, key) => {
425
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
426
+ return normalized;
427
+ }
428
+ normalized[key] = normalizeForSerialization(value[key]);
429
+ return normalized;
430
+ }, {});
431
+ }
432
+ return value;
433
+ }
434
+ function serializeKeyPart(value) {
435
+ if (typeof value === "string") {
436
+ return `s:${value}`;
437
+ }
438
+ if (typeof value === "number") {
439
+ return `n:${value}`;
440
+ }
441
+ if (typeof value === "boolean") {
442
+ return `b:${value}`;
443
+ }
444
+ return `j:${JSON.stringify(normalizeForSerialization(value))}`;
445
+ }
446
+ function serializeOptions(options) {
447
+ return JSON.stringify(normalizeForSerialization(options) ?? null);
448
+ }
449
+ function createInstanceId() {
450
+ if (globalThis.crypto?.randomUUID) {
451
+ return globalThis.crypto.randomUUID();
452
+ }
453
+ const bytes = new Uint8Array(16);
454
+ if (globalThis.crypto?.getRandomValues) {
455
+ globalThis.crypto.getRandomValues(bytes);
456
+ } else {
457
+ for (let i = 0; i < bytes.length; i += 1) {
458
+ bytes[i] = Math.floor(Math.random() * 256);
459
+ }
460
+ }
461
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
462
+ }
463
+
464
+ // src/internal/CacheSnapshotFile.ts
465
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
466
+ const relative = path.relative(realBaseDir, candidatePath);
467
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
468
+ }
469
+ async function findExistingAncestor(directory, fs2, path) {
470
+ let current = directory;
471
+ while (true) {
472
+ try {
473
+ await fs2.lstat(current);
474
+ return current;
475
+ } catch (error) {
476
+ if (error.code !== "ENOENT") {
477
+ throw error;
478
+ }
479
+ }
480
+ const parent = path.dirname(current);
481
+ if (parent === current) {
482
+ return current;
483
+ }
484
+ current = parent;
485
+ }
486
+ }
487
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
488
+ if (filePath.length === 0) {
489
+ throw new Error("filePath must not be empty.");
490
+ }
491
+ if (filePath.includes("\0")) {
492
+ throw new Error("filePath must not contain null bytes.");
493
+ }
494
+ const { promises: fs2 } = await import("fs");
495
+ const path = await import("path");
496
+ const resolved = path.resolve(filePath);
497
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
498
+ if (baseDir === false) {
499
+ return resolved;
500
+ }
501
+ await fs2.mkdir(baseDir, { recursive: true });
502
+ const realBaseDir = await fs2.realpath(baseDir);
503
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
504
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
505
+ }
506
+ if (mode === "read") {
507
+ const realTarget = await fs2.realpath(resolved);
508
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
509
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
510
+ }
511
+ return realTarget;
512
+ }
513
+ const parentDir = path.dirname(resolved);
514
+ const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
515
+ const realExistingAncestor = await fs2.realpath(existingAncestor);
516
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
517
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
518
+ }
519
+ await fs2.mkdir(parentDir, { recursive: true });
520
+ const realParentDir = await fs2.realpath(parentDir);
521
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
522
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
523
+ }
524
+ const targetPath = path.join(realParentDir, path.basename(resolved));
525
+ try {
526
+ const existing = await fs2.lstat(targetPath);
527
+ if (existing.isSymbolicLink()) {
528
+ throw new Error("filePath must not point to a symbolic link.");
529
+ }
530
+ } catch (error) {
531
+ if (error.code !== "ENOENT") {
532
+ throw error;
533
+ }
534
+ }
535
+ return targetPath;
536
+ }
537
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
538
+ if (byteLimit === false) {
539
+ return handle.readFile({ encoding: "utf8" });
540
+ }
541
+ const chunks = [];
542
+ let totalBytes = 0;
543
+ let position = 0;
544
+ while (true) {
545
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
546
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
547
+ if (bytesRead === 0) {
548
+ break;
549
+ }
550
+ totalBytes += bytesRead;
551
+ if (totalBytes > byteLimit) {
552
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
553
+ }
554
+ chunks.push(buffer.subarray(0, bytesRead));
555
+ position += bytesRead;
556
+ }
557
+ return Buffer.concat(chunks).toString("utf8");
558
+ }
559
+
560
+ // src/internal/CacheStackValidation.ts
561
+ var MAX_CACHE_KEY_LENGTH = 1024;
562
+ var MAX_PATTERN_LENGTH = 1024;
563
+ var MAX_TAGS_PER_OPERATION = 128;
564
+ function validatePositiveNumber(name, value) {
565
+ if (value === void 0) {
566
+ return;
567
+ }
568
+ if (!Number.isFinite(value) || value <= 0) {
569
+ throw new Error(`${name} must be a positive finite number.`);
570
+ }
571
+ }
572
+ function validateNonNegativeNumber(name, value) {
573
+ if (!Number.isFinite(value) || value < 0) {
574
+ throw new Error(`${name} must be a non-negative finite number.`);
575
+ }
576
+ }
577
+ function validateLayerNumberOption(name, value) {
578
+ if (value === void 0) {
579
+ return;
580
+ }
581
+ if (typeof value === "number") {
582
+ validateNonNegativeNumber(name, value);
583
+ return;
584
+ }
585
+ for (const [layerName, layerValue] of Object.entries(value)) {
586
+ if (layerValue === void 0) {
587
+ continue;
588
+ }
589
+ validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
590
+ }
591
+ }
592
+ function validateRateLimitOptions(name, options) {
593
+ if (!options) {
594
+ return;
595
+ }
596
+ validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
597
+ validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
598
+ validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
599
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
600
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
601
+ }
602
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
603
+ throw new Error(`${name}.bucketKey must not be empty.`);
604
+ }
605
+ }
606
+ function validateCacheKey(key) {
607
+ if (key.length === 0) {
608
+ throw new Error("Cache key must not be empty.");
609
+ }
610
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
611
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
612
+ }
613
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
614
+ throw new Error("Cache key contains unsupported control characters.");
615
+ }
616
+ if (/[\uD800-\uDFFF]/.test(key)) {
617
+ throw new Error("Cache key contains unsupported surrogate code points.");
618
+ }
619
+ return key;
620
+ }
621
+ function validateTag(tag) {
622
+ if (tag.length === 0) {
623
+ throw new Error("Cache tag must not be empty.");
624
+ }
625
+ if (tag.length > MAX_CACHE_KEY_LENGTH) {
626
+ throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
627
+ }
628
+ if (/[\u0000-\u001F\u007F]/.test(tag)) {
629
+ throw new Error("Cache tag contains unsupported control characters.");
630
+ }
631
+ if (/[\uD800-\uDFFF]/.test(tag)) {
632
+ throw new Error("Cache tag contains unsupported surrogate code points.");
633
+ }
634
+ return tag;
635
+ }
636
+ function validateTags(tags) {
637
+ if (!tags) {
638
+ return;
639
+ }
640
+ if (tags.length > MAX_TAGS_PER_OPERATION) {
641
+ throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
642
+ }
643
+ for (const tag of tags) {
644
+ validateTag(tag);
645
+ }
646
+ }
647
+ function validatePattern(pattern) {
648
+ if (pattern.length === 0) {
649
+ throw new Error("Pattern must not be empty.");
650
+ }
651
+ if (pattern.length > MAX_PATTERN_LENGTH) {
652
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
653
+ }
654
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
655
+ throw new Error("Pattern contains unsupported control characters.");
656
+ }
657
+ }
658
+ function validateTtlPolicy(name, policy) {
659
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
660
+ return;
661
+ }
662
+ if ("alignTo" in policy) {
663
+ validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
664
+ return;
665
+ }
666
+ throw new Error(`${name} is invalid.`);
667
+ }
668
+ function validateAdaptiveTtlOptions(options) {
669
+ if (!options || options === true) {
670
+ return;
671
+ }
672
+ validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
673
+ validateLayerNumberOption("adaptiveTtl.step", options.step);
674
+ validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
675
+ }
676
+ function validateCircuitBreakerOptions(options) {
677
+ if (!options) {
678
+ return;
679
+ }
680
+ validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
681
+ validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
682
+ }
683
+
322
684
  // src/internal/CircuitBreakerManager.ts
323
685
  var CircuitBreakerManager = class {
324
686
  breakers = /* @__PURE__ */ new Map();
@@ -337,9 +699,7 @@ var CircuitBreakerManager = class {
337
699
  }
338
700
  const now = Date.now();
339
701
  if (state.openUntil <= now) {
340
- state.openUntil = null;
341
- state.failures = 0;
342
- this.breakers.set(key, state);
702
+ this.breakers.delete(key);
343
703
  return;
344
704
  }
345
705
  const remainingMs = state.openUntil - now;
@@ -350,15 +710,15 @@ var CircuitBreakerManager = class {
350
710
  if (!options) {
351
711
  return;
352
712
  }
713
+ this.pruneIfNeeded();
353
714
  const failureThreshold = options.failureThreshold ?? 3;
354
715
  const cooldownMs = options.cooldownMs ?? 3e4;
355
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
716
+ const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
356
717
  state.failures += 1;
357
718
  if (state.failures >= failureThreshold) {
358
719
  state.openUntil = Date.now() + cooldownMs;
359
720
  }
360
721
  this.breakers.set(key, state);
361
- this.pruneIfNeeded();
362
722
  }
363
723
  recordSuccess(key) {
364
724
  this.breakers.delete(key);
@@ -369,8 +729,7 @@ var CircuitBreakerManager = class {
369
729
  return false;
370
730
  }
371
731
  if (state.openUntil <= Date.now()) {
372
- state.openUntil = null;
373
- state.failures = 0;
732
+ this.breakers.delete(key);
374
733
  return false;
375
734
  }
376
735
  return true;
@@ -394,15 +753,20 @@ var CircuitBreakerManager = class {
394
753
  if (this.breakers.size <= this.maxEntries) {
395
754
  return;
396
755
  }
756
+ const now = Date.now();
397
757
  for (const [key, state] of this.breakers.entries()) {
398
758
  if (this.breakers.size <= this.maxEntries) {
399
- break;
759
+ return;
400
760
  }
401
- if (!state.openUntil || state.openUntil <= Date.now()) {
761
+ if (!state.openUntil || state.openUntil <= now) {
402
762
  this.breakers.delete(key);
403
763
  }
404
764
  }
405
- for (const key of this.breakers.keys()) {
765
+ if (this.breakers.size <= this.maxEntries) {
766
+ return;
767
+ }
768
+ const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
769
+ for (const [key] of sorted) {
406
770
  if (this.breakers.size <= this.maxEntries) {
407
771
  break;
408
772
  }
@@ -412,6 +776,7 @@ var CircuitBreakerManager = class {
412
776
  };
413
777
 
414
778
  // src/internal/FetchRateLimiter.ts
779
+ var MAX_BUCKETS = 1e4;
415
780
  var FetchRateLimiter = class {
416
781
  buckets = /* @__PURE__ */ new Map();
417
782
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -577,10 +942,25 @@ var FetchRateLimiter = class {
577
942
  if (existing) {
578
943
  return existing;
579
944
  }
945
+ if (this.buckets.size >= MAX_BUCKETS) {
946
+ this.evictIdleBuckets();
947
+ }
580
948
  const bucket = { active: 0, startedAt: [] };
581
949
  this.buckets.set(bucketKey, bucket);
582
950
  return bucket;
583
951
  }
952
+ evictIdleBuckets() {
953
+ for (const [key, bucket] of this.buckets.entries()) {
954
+ if (this.buckets.size <= MAX_BUCKETS * 0.9) {
955
+ break;
956
+ }
957
+ if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
958
+ this.buckets.delete(key);
959
+ this.queuesByBucket.delete(key);
960
+ this.pendingBuckets.delete(key);
961
+ }
962
+ }
963
+ }
584
964
  cleanupBucket(bucketKey, bucket, intervalMs) {
585
965
  const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
586
966
  if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
@@ -783,35 +1163,39 @@ var TtlResolver = class {
783
1163
  return;
784
1164
  }
785
1165
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
786
- let removed = 0;
787
- for (const key of this.accessProfiles.keys()) {
788
- if (removed >= toRemove) {
789
- break;
1166
+ const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
1167
+ for (let i = 0; i < toRemove && i < sorted.length; i++) {
1168
+ const entry = sorted[i];
1169
+ if (entry) {
1170
+ this.accessProfiles.delete(entry[0]);
790
1171
  }
791
- this.accessProfiles.delete(key);
792
- removed += 1;
793
1172
  }
794
1173
  }
795
1174
  };
796
1175
 
797
1176
  // src/serialization/JsonSerializer.ts
798
1177
  var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1178
+ var MAX_SANITIZE_NODES = 1e4;
799
1179
  var JsonSerializer = class {
800
1180
  serialize(value) {
801
1181
  return JSON.stringify(value);
802
1182
  }
803
1183
  deserialize(payload) {
804
1184
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
805
- return sanitizeJsonValue(JSON.parse(normalized), 0);
1185
+ return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
806
1186
  }
807
1187
  };
808
1188
  var MAX_SANITIZE_DEPTH = 200;
809
- function sanitizeJsonValue(value, depth) {
1189
+ function sanitizeJsonValue(value, depth, state) {
1190
+ state.count += 1;
1191
+ if (state.count > MAX_SANITIZE_NODES) {
1192
+ throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
1193
+ }
810
1194
  if (depth > MAX_SANITIZE_DEPTH) {
811
- return value;
1195
+ throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
812
1196
  }
813
1197
  if (Array.isArray(value)) {
814
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
1198
+ return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
815
1199
  }
816
1200
  if (!isPlainObject(value)) {
817
1201
  return value;
@@ -821,7 +1205,7 @@ function sanitizeJsonValue(value, depth) {
821
1205
  if (DANGEROUS_JSON_KEYS.has(key)) {
822
1206
  continue;
823
1207
  }
824
- sanitized[key] = sanitizeJsonValue(entry, depth + 1);
1208
+ sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
825
1209
  }
826
1210
  return sanitized;
827
1211
  }
@@ -871,9 +1255,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
871
1255
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
872
1256
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
873
1257
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
874
- var MAX_CACHE_KEY_LENGTH = 1024;
1258
+ var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1259
+ var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1260
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1261
+ var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
875
1262
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
876
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
877
1263
  var DebugLogger = class {
878
1264
  enabled;
879
1265
  constructor(enabled) {
@@ -960,6 +1346,7 @@ var CacheStack = class extends EventEmitter {
960
1346
  snapshotSerializer = new JsonSerializer();
961
1347
  backgroundRefreshes = /* @__PURE__ */ new Map();
962
1348
  layerDegradedUntil = /* @__PURE__ */ new Map();
1349
+ keyEpochs = /* @__PURE__ */ new Map();
963
1350
  ttlResolver;
964
1351
  circuitBreakerManager;
965
1352
  currentGeneration;
@@ -967,6 +1354,7 @@ var CacheStack = class extends EventEmitter {
967
1354
  writeBehindTimer;
968
1355
  writeBehindFlushPromise;
969
1356
  generationCleanupPromise;
1357
+ clearEpoch = 0;
970
1358
  isDisconnecting = false;
971
1359
  disconnectPromise;
972
1360
  /**
@@ -976,7 +1364,7 @@ var CacheStack = class extends EventEmitter {
976
1364
  * and no `fetcher` is provided.
977
1365
  */
978
1366
  async get(key, fetcher, options) {
979
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1367
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
980
1368
  this.validateWriteOptions(options);
981
1369
  await this.awaitStartup("get");
982
1370
  return this.getPrepared(normalizedKey, fetcher, options);
@@ -1046,7 +1434,7 @@ var CacheStack = class extends EventEmitter {
1046
1434
  * Returns true if the given key exists and is not expired in any layer.
1047
1435
  */
1048
1436
  async has(key) {
1049
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1437
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1050
1438
  await this.awaitStartup("has");
1051
1439
  for (const layer of this.layers) {
1052
1440
  if (this.shouldSkipLayer(layer)) {
@@ -1079,7 +1467,7 @@ var CacheStack = class extends EventEmitter {
1079
1467
  * that has it, or null if the key is not found / has no TTL.
1080
1468
  */
1081
1469
  async ttl(key) {
1082
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1470
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1083
1471
  await this.awaitStartup("ttl");
1084
1472
  for (const layer of this.layers) {
1085
1473
  if (this.shouldSkipLayer(layer)) {
@@ -1101,7 +1489,7 @@ var CacheStack = class extends EventEmitter {
1101
1489
  * Stores a value in all cache layers. Overwrites any existing value.
1102
1490
  */
1103
1491
  async set(key, value, options) {
1104
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1492
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1105
1493
  this.validateWriteOptions(options);
1106
1494
  await this.awaitStartup("set");
1107
1495
  await this.storeEntry(normalizedKey, "value", value, options);
@@ -1110,7 +1498,7 @@ var CacheStack = class extends EventEmitter {
1110
1498
  * Deletes the key from all layers and publishes an invalidation message.
1111
1499
  */
1112
1500
  async delete(key) {
1113
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1501
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1114
1502
  await this.awaitStartup("delete");
1115
1503
  await this.deleteKeys([normalizedKey]);
1116
1504
  await this.publishInvalidation({
@@ -1122,6 +1510,7 @@ var CacheStack = class extends EventEmitter {
1122
1510
  }
1123
1511
  async clear() {
1124
1512
  await this.awaitStartup("clear");
1513
+ this.beginClearEpoch();
1125
1514
  await Promise.all(this.layers.map((layer) => layer.clear()));
1126
1515
  await this.tagIndex.clear();
1127
1516
  this.ttlResolver.clearProfiles();
@@ -1138,7 +1527,7 @@ var CacheStack = class extends EventEmitter {
1138
1527
  return;
1139
1528
  }
1140
1529
  await this.awaitStartup("mdelete");
1141
- const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1530
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
1142
1531
  const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1143
1532
  await this.deleteKeys(cacheKeys);
1144
1533
  await this.publishInvalidation({
@@ -1155,7 +1544,7 @@ var CacheStack = class extends EventEmitter {
1155
1544
  }
1156
1545
  const normalizedEntries = entries.map((entry) => ({
1157
1546
  ...entry,
1158
- key: this.qualifyKey(this.validateCacheKey(entry.key))
1547
+ key: this.qualifyKey(validateCacheKey(entry.key))
1159
1548
  }));
1160
1549
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1161
1550
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1164,7 +1553,7 @@ var CacheStack = class extends EventEmitter {
1164
1553
  const pendingReads = /* @__PURE__ */ new Map();
1165
1554
  return Promise.all(
1166
1555
  normalizedEntries.map((entry) => {
1167
- const optionsSignature = this.serializeOptions(entry.options);
1556
+ const optionsSignature = serializeOptions(entry.options);
1168
1557
  const existing = pendingReads.get(entry.key);
1169
1558
  if (!existing) {
1170
1559
  const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
@@ -1233,7 +1622,7 @@ var CacheStack = class extends EventEmitter {
1233
1622
  this.assertActive("mset");
1234
1623
  const normalizedEntries = entries.map((entry) => ({
1235
1624
  ...entry,
1236
- key: this.qualifyKey(this.validateCacheKey(entry.key))
1625
+ key: this.qualifyKey(validateCacheKey(entry.key))
1237
1626
  }));
1238
1627
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1239
1628
  await this.awaitStartup("mset");
@@ -1276,7 +1665,7 @@ var CacheStack = class extends EventEmitter {
1276
1665
  */
1277
1666
  wrap(prefix, fetcher, options = {}) {
1278
1667
  return (...args) => {
1279
- const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
1668
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
1280
1669
  const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
1281
1670
  return this.get(key, () => fetcher(...args), options);
1282
1671
  };
@@ -1286,11 +1675,13 @@ var CacheStack = class extends EventEmitter {
1286
1675
  * `prefix:`. Useful for multi-tenant or module-level isolation.
1287
1676
  */
1288
1677
  namespace(prefix) {
1678
+ validateNamespaceKey(prefix);
1289
1679
  return new CacheNamespace(this, prefix);
1290
1680
  }
1291
1681
  async invalidateByTag(tag) {
1682
+ validateTag(tag);
1292
1683
  await this.awaitStartup("invalidateByTag");
1293
- const keys = await this.tagIndex.keysForTag(tag);
1684
+ const keys = await this.collectKeysForTag(tag);
1294
1685
  await this.deleteKeys(keys);
1295
1686
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1296
1687
  }
@@ -1298,22 +1689,28 @@ var CacheStack = class extends EventEmitter {
1298
1689
  if (tags.length === 0) {
1299
1690
  return;
1300
1691
  }
1692
+ validateTags(tags);
1301
1693
  await this.awaitStartup("invalidateByTags");
1302
- const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1694
+ const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
1303
1695
  const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1696
+ this.assertWithinInvalidationKeyLimit(keys.length);
1304
1697
  await this.deleteKeys(keys);
1305
1698
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1306
1699
  }
1307
1700
  async invalidateByPattern(pattern) {
1701
+ validatePattern(pattern);
1308
1702
  await this.awaitStartup("invalidateByPattern");
1309
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
1703
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
1704
+ this.qualifyPattern(pattern),
1705
+ this.invalidationMaxKeys()
1706
+ );
1310
1707
  await this.deleteKeys(keys);
1311
1708
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1312
1709
  }
1313
1710
  async invalidateByPrefix(prefix) {
1314
1711
  await this.awaitStartup("invalidateByPrefix");
1315
- const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1316
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
1712
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
1713
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
1317
1714
  await this.deleteKeys(keys);
1318
1715
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1319
1716
  }
@@ -1383,7 +1780,7 @@ var CacheStack = class extends EventEmitter {
1383
1780
  * Returns `null` if the key does not exist in any layer.
1384
1781
  */
1385
1782
  async inspect(key) {
1386
- const userKey = this.validateCacheKey(key);
1783
+ const userKey = validateCacheKey(key);
1387
1784
  const normalizedKey = this.qualifyKey(userKey);
1388
1785
  await this.awaitStartup("inspect");
1389
1786
  const foundInLayers = [];
@@ -1420,50 +1817,79 @@ var CacheStack = class extends EventEmitter {
1420
1817
  }
1421
1818
  async exportState() {
1422
1819
  await this.awaitStartup("exportState");
1423
- const exported = /* @__PURE__ */ new Map();
1424
- for (const layer of this.layers) {
1425
- if (!layer.keys) {
1426
- continue;
1427
- }
1428
- const keys = await layer.keys();
1429
- for (const key of keys) {
1430
- const exportedKey = this.stripQualifiedKey(key);
1431
- if (exported.has(exportedKey)) {
1432
- continue;
1433
- }
1434
- const stored = await this.readLayerEntry(layer, key);
1435
- if (stored === null) {
1436
- continue;
1437
- }
1438
- exported.set(exportedKey, {
1439
- key: exportedKey,
1440
- value: stored,
1441
- ttl: remainingStoredTtlSeconds(stored)
1442
- });
1443
- }
1444
- }
1445
- return [...exported.values()];
1820
+ const entries = [];
1821
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
1822
+ entries.push(entry);
1823
+ });
1824
+ return entries;
1446
1825
  }
1447
1826
  async importState(entries) {
1448
1827
  await this.awaitStartup("importState");
1449
- await Promise.all(
1450
- entries.map(async (entry) => {
1451
- const qualifiedKey = this.qualifyKey(entry.key);
1452
- await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1453
- await this.tagIndex.touch(qualifiedKey);
1454
- })
1455
- );
1828
+ const normalizedEntries = entries.map((entry) => ({
1829
+ key: this.qualifyKey(validateCacheKey(entry.key)),
1830
+ value: entry.value,
1831
+ ttl: entry.ttl
1832
+ }));
1833
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1834
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1835
+ await Promise.all(
1836
+ batch.map(async (entry) => {
1837
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1838
+ await this.tagIndex.touch(entry.key);
1839
+ })
1840
+ );
1841
+ }
1456
1842
  }
1457
1843
  async persistToFile(filePath) {
1458
1844
  this.assertActive("persistToFile");
1459
- const snapshot = await this.exportState();
1460
1845
  const { promises: fs2 } = await import("fs");
1461
- await fs2.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
1846
+ const path = await import("path");
1847
+ const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
1848
+ const tempPath = path.join(
1849
+ path.dirname(targetPath),
1850
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
1851
+ );
1852
+ let handle;
1853
+ try {
1854
+ handle = await fs2.open(tempPath, "wx");
1855
+ const openedHandle = handle;
1856
+ await openedHandle.writeFile("[", "utf8");
1857
+ let wroteAny = false;
1858
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
1859
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1860
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1861
+ wroteAny = true;
1862
+ });
1863
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1864
+ await openedHandle.close();
1865
+ handle = void 0;
1866
+ await fs2.rename(tempPath, targetPath);
1867
+ } catch (error) {
1868
+ await handle?.close().catch(() => void 0);
1869
+ await fs2.unlink(tempPath).catch(() => void 0);
1870
+ throw error;
1871
+ }
1462
1872
  }
1463
1873
  async restoreFromFile(filePath) {
1464
1874
  this.assertActive("restoreFromFile");
1465
- const { promises: fs2 } = await import("fs");
1466
- const raw = await fs2.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
1875
+ const { promises: fs2, constants } = await import("fs");
1876
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
1877
+ const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1878
+ const snapshotMaxBytes = this.snapshotMaxBytes();
1879
+ let raw;
1880
+ try {
1881
+ if (snapshotMaxBytes !== false) {
1882
+ const stat = await handle.stat();
1883
+ if (stat.size > snapshotMaxBytes) {
1884
+ throw new Error(
1885
+ `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
1886
+ );
1887
+ }
1888
+ }
1889
+ raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
1890
+ } finally {
1891
+ await handle.close();
1892
+ }
1467
1893
  let parsed;
1468
1894
  try {
1469
1895
  parsed = JSON.parse(raw);
@@ -1507,14 +1933,14 @@ var CacheStack = class extends EventEmitter {
1507
1933
  await this.handleInvalidationMessage(message);
1508
1934
  });
1509
1935
  }
1510
- async fetchWithGuards(key, fetcher, options) {
1936
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1511
1937
  const fetchTask = async () => {
1512
1938
  const secondHit = await this.readFromLayers(key, options, "fresh-only");
1513
1939
  if (secondHit.found) {
1514
1940
  this.metricsCollector.increment("hits");
1515
1941
  return secondHit.value;
1516
1942
  }
1517
- return this.fetchAndPopulate(key, fetcher, options);
1943
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1518
1944
  };
1519
1945
  const singleFlightTask = async () => {
1520
1946
  if (!this.options.singleFlightCoordinator) {
@@ -1524,7 +1950,7 @@ var CacheStack = class extends EventEmitter {
1524
1950
  key,
1525
1951
  this.resolveSingleFlightOptions(),
1526
1952
  fetchTask,
1527
- () => this.waitForFreshValue(key, fetcher, options)
1953
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
1528
1954
  );
1529
1955
  };
1530
1956
  if (this.options.stampedePrevention === false) {
@@ -1532,7 +1958,7 @@ var CacheStack = class extends EventEmitter {
1532
1958
  }
1533
1959
  return this.stampedeGuard.execute(key, singleFlightTask);
1534
1960
  }
1535
- async waitForFreshValue(key, fetcher, options) {
1961
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1536
1962
  const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1537
1963
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1538
1964
  const deadline = Date.now() + timeoutMs;
@@ -1546,9 +1972,9 @@ var CacheStack = class extends EventEmitter {
1546
1972
  }
1547
1973
  await this.sleep(pollIntervalMs);
1548
1974
  }
1549
- return this.fetchAndPopulate(key, fetcher, options);
1975
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1550
1976
  }
1551
- async fetchAndPopulate(key, fetcher, options) {
1977
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1552
1978
  this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1553
1979
  this.metricsCollector.increment("fetches");
1554
1980
  const fetchStart = Date.now();
@@ -1569,6 +1995,16 @@ var CacheStack = class extends EventEmitter {
1569
1995
  if (!this.shouldNegativeCache(options)) {
1570
1996
  return null;
1571
1997
  }
1998
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1999
+ this.logger.debug?.("skip-negative-store-after-invalidation", {
2000
+ key,
2001
+ expectedClearEpoch,
2002
+ clearEpoch: this.clearEpoch,
2003
+ expectedKeyEpoch,
2004
+ keyEpoch: this.currentKeyEpoch(key)
2005
+ });
2006
+ return null;
2007
+ }
1572
2008
  await this.storeEntry(key, "empty", null, options);
1573
2009
  return null;
1574
2010
  }
@@ -1581,11 +2017,26 @@ var CacheStack = class extends EventEmitter {
1581
2017
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
1582
2018
  }
1583
2019
  }
2020
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2021
+ this.logger.debug?.("skip-store-after-invalidation", {
2022
+ key,
2023
+ expectedClearEpoch,
2024
+ clearEpoch: this.clearEpoch,
2025
+ expectedKeyEpoch,
2026
+ keyEpoch: this.currentKeyEpoch(key)
2027
+ });
2028
+ return fetched;
2029
+ }
1584
2030
  await this.storeEntry(key, "value", fetched, options);
1585
2031
  return fetched;
1586
2032
  }
1587
2033
  async storeEntry(key, kind, value, options) {
2034
+ const clearEpoch = this.clearEpoch;
2035
+ const keyEpoch = this.currentKeyEpoch(key);
1588
2036
  await this.writeAcrossLayers(key, kind, value, options);
2037
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2038
+ return;
2039
+ }
1589
2040
  if (options?.tags) {
1590
2041
  await this.tagIndex.track(key, options.tags);
1591
2042
  } else {
@@ -1600,6 +2051,8 @@ var CacheStack = class extends EventEmitter {
1600
2051
  }
1601
2052
  async writeBatch(entries) {
1602
2053
  const now = Date.now();
2054
+ const clearEpoch = this.clearEpoch;
2055
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
1603
2056
  const entriesByLayer = /* @__PURE__ */ new Map();
1604
2057
  const immediateOperations = [];
1605
2058
  const deferredOperations = [];
@@ -1616,12 +2069,21 @@ var CacheStack = class extends EventEmitter {
1616
2069
  }
1617
2070
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
1618
2071
  const operation = async () => {
2072
+ if (clearEpoch !== this.clearEpoch) {
2073
+ return;
2074
+ }
2075
+ const activeEntries = layerEntries.filter(
2076
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2077
+ );
2078
+ if (activeEntries.length === 0) {
2079
+ return;
2080
+ }
1619
2081
  try {
1620
2082
  if (layer.setMany) {
1621
- await layer.setMany(layerEntries);
2083
+ await layer.setMany(activeEntries);
1622
2084
  return;
1623
2085
  }
1624
- await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2086
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1625
2087
  } catch (error) {
1626
2088
  await this.handleLayerFailure(layer, "write", error);
1627
2089
  }
@@ -1634,7 +2096,13 @@ var CacheStack = class extends EventEmitter {
1634
2096
  }
1635
2097
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1636
2098
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2099
+ if (clearEpoch !== this.clearEpoch) {
2100
+ return;
2101
+ }
1637
2102
  for (const entry of entries) {
2103
+ if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2104
+ continue;
2105
+ }
1638
2106
  if (entry.options?.tags) {
1639
2107
  await this.tagIndex.track(entry.key, entry.options.tags);
1640
2108
  } else {
@@ -1736,10 +2204,15 @@ var CacheStack = class extends EventEmitter {
1736
2204
  }
1737
2205
  async writeAcrossLayers(key, kind, value, options) {
1738
2206
  const now = Date.now();
2207
+ const clearEpoch = this.clearEpoch;
2208
+ const keyEpoch = this.currentKeyEpoch(key);
1739
2209
  const immediateOperations = [];
1740
2210
  const deferredOperations = [];
1741
2211
  for (const layer of this.layers) {
1742
2212
  const operation = async () => {
2213
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2214
+ return;
2215
+ }
1743
2216
  if (this.shouldSkipLayer(layer)) {
1744
2217
  return;
1745
2218
  }
@@ -1803,10 +2276,12 @@ var CacheStack = class extends EventEmitter {
1803
2276
  if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
1804
2277
  return;
1805
2278
  }
2279
+ const clearEpoch = this.clearEpoch;
2280
+ const keyEpoch = this.currentKeyEpoch(key);
1806
2281
  const refresh = (async () => {
1807
2282
  this.metricsCollector.increment("refreshes");
1808
2283
  try {
1809
- await this.runBackgroundRefresh(key, fetcher, options);
2284
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
1810
2285
  } catch (error) {
1811
2286
  this.metricsCollector.increment("refreshErrors");
1812
2287
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
@@ -1816,14 +2291,16 @@ var CacheStack = class extends EventEmitter {
1816
2291
  })();
1817
2292
  this.backgroundRefreshes.set(key, refresh);
1818
2293
  }
1819
- async runBackgroundRefresh(key, fetcher, options) {
2294
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1820
2295
  const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1821
2296
  await this.fetchWithGuards(
1822
2297
  key,
1823
2298
  () => this.withTimeout(fetcher(), timeoutMs, () => {
1824
2299
  return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1825
2300
  }),
1826
- options
2301
+ options,
2302
+ expectedClearEpoch,
2303
+ expectedKeyEpoch
1827
2304
  );
1828
2305
  }
1829
2306
  resolveSingleFlightOptions() {
@@ -1838,6 +2315,7 @@ var CacheStack = class extends EventEmitter {
1838
2315
  if (keys.length === 0) {
1839
2316
  return;
1840
2317
  }
2318
+ this.bumpKeyEpochs(keys);
1841
2319
  await this.deleteKeysFromLayers(this.layers, keys);
1842
2320
  for (const key of keys) {
1843
2321
  await this.tagIndex.remove(key);
@@ -1860,21 +2338,22 @@ var CacheStack = class extends EventEmitter {
1860
2338
  return;
1861
2339
  }
1862
2340
  const localLayers = this.layers.filter((layer) => layer.isLocal);
1863
- if (localLayers.length === 0) {
1864
- return;
1865
- }
1866
2341
  if (message.scope === "clear") {
2342
+ this.beginClearEpoch();
1867
2343
  await Promise.all(localLayers.map((layer) => layer.clear()));
1868
2344
  await this.tagIndex.clear();
1869
2345
  this.ttlResolver.clearProfiles();
2346
+ this.circuitBreakerManager.clear();
1870
2347
  return;
1871
2348
  }
1872
2349
  const keys = message.keys ?? [];
2350
+ this.bumpKeyEpochs(keys);
1873
2351
  await this.deleteKeysFromLayers(localLayers, keys);
1874
2352
  if (message.operation !== "write") {
1875
2353
  for (const key of keys) {
1876
2354
  await this.tagIndex.remove(key);
1877
2355
  this.ttlResolver.deleteProfile(key);
2356
+ this.circuitBreakerManager.delete(key);
1878
2357
  }
1879
2358
  }
1880
2359
  }
@@ -1980,6 +2459,28 @@ var CacheStack = class extends EventEmitter {
1980
2459
  shouldWriteBehind(layer) {
1981
2460
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
1982
2461
  }
2462
+ beginClearEpoch() {
2463
+ this.clearEpoch += 1;
2464
+ this.keyEpochs.clear();
2465
+ this.writeBehindQueue.length = 0;
2466
+ }
2467
+ currentKeyEpoch(key) {
2468
+ return this.keyEpochs.get(key) ?? 0;
2469
+ }
2470
+ bumpKeyEpochs(keys) {
2471
+ for (const key of keys) {
2472
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
2473
+ }
2474
+ }
2475
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
2476
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
2477
+ return true;
2478
+ }
2479
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
2480
+ return true;
2481
+ }
2482
+ return false;
2483
+ }
1983
2484
  async enqueueWriteBehind(operation) {
1984
2485
  this.writeBehindQueue.push(operation);
1985
2486
  const batchSize = this.options.writeBehind?.batchSize ?? 100;
@@ -2106,107 +2607,50 @@ var CacheStack = class extends EventEmitter {
2106
2607
  if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
2107
2608
  throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
2108
2609
  }
2109
- this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
2110
- this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
2111
- this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
2112
- this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
2113
- this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
2114
- this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2115
- this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2116
- this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2117
- this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2118
- this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2119
- this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2120
- this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2121
- this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2610
+ validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
2611
+ validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
2612
+ validateLayerNumberOption("staleIfError", this.options.staleIfError);
2613
+ validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
2614
+ validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
2615
+ validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2616
+ validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2617
+ validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2618
+ validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2619
+ validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2620
+ if (this.options.snapshotMaxBytes !== false) {
2621
+ validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
2622
+ }
2623
+ if (this.options.snapshotMaxEntries !== false) {
2624
+ validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
2625
+ }
2626
+ if (this.options.invalidationMaxKeys !== false) {
2627
+ validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
2628
+ }
2629
+ validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2630
+ validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2631
+ validateCircuitBreakerOptions(this.options.circuitBreaker);
2122
2632
  if (typeof this.options.generationCleanup === "object") {
2123
- this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2633
+ validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2124
2634
  }
2125
2635
  if (this.options.generation !== void 0) {
2126
- this.validateNonNegativeNumber("generation", this.options.generation);
2636
+ validateNonNegativeNumber("generation", this.options.generation);
2127
2637
  }
2128
2638
  }
2129
2639
  validateWriteOptions(options) {
2130
2640
  if (!options) {
2131
2641
  return;
2132
2642
  }
2133
- this.validateLayerNumberOption("options.ttl", options.ttl);
2134
- this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
2135
- this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
2136
- this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
2137
- this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
2138
- this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2139
- this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2140
- this.validateAdaptiveTtlOptions(options.adaptiveTtl);
2141
- this.validateCircuitBreakerOptions(options.circuitBreaker);
2142
- this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2143
- }
2144
- validateLayerNumberOption(name, value) {
2145
- if (value === void 0) {
2146
- return;
2147
- }
2148
- if (typeof value === "number") {
2149
- this.validateNonNegativeNumber(name, value);
2150
- return;
2151
- }
2152
- for (const [layerName, layerValue] of Object.entries(value)) {
2153
- if (layerValue === void 0) {
2154
- continue;
2155
- }
2156
- this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
2157
- }
2158
- }
2159
- validatePositiveNumber(name, value) {
2160
- if (value === void 0) {
2161
- return;
2162
- }
2163
- if (!Number.isFinite(value) || value <= 0) {
2164
- throw new Error(`${name} must be a positive finite number.`);
2165
- }
2166
- }
2167
- validateRateLimitOptions(name, options) {
2168
- if (!options) {
2169
- return;
2170
- }
2171
- this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2172
- this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2173
- this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2174
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2175
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2176
- }
2177
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2178
- throw new Error(`${name}.bucketKey must not be empty.`);
2179
- }
2180
- }
2181
- validateNonNegativeNumber(name, value) {
2182
- if (!Number.isFinite(value) || value < 0) {
2183
- throw new Error(`${name} must be a non-negative finite number.`);
2184
- }
2185
- }
2186
- validateCacheKey(key) {
2187
- if (key.length === 0) {
2188
- throw new Error("Cache key must not be empty.");
2189
- }
2190
- if (key.length > MAX_CACHE_KEY_LENGTH) {
2191
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
2192
- }
2193
- if (/[\u0000-\u001F\u007F]/.test(key)) {
2194
- throw new Error("Cache key contains unsupported control characters.");
2195
- }
2196
- if (/[\uD800-\uDFFF]/.test(key)) {
2197
- throw new Error("Cache key contains unsupported surrogate code points.");
2198
- }
2199
- return key;
2200
- }
2201
- validateTtlPolicy(name, policy) {
2202
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2203
- return;
2204
- }
2205
- if ("alignTo" in policy) {
2206
- this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2207
- return;
2208
- }
2209
- throw new Error(`${name} is invalid.`);
2643
+ validateLayerNumberOption("options.ttl", options.ttl);
2644
+ validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
2645
+ validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
2646
+ validateLayerNumberOption("options.staleIfError", options.staleIfError);
2647
+ validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
2648
+ validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2649
+ validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2650
+ validateAdaptiveTtlOptions(options.adaptiveTtl);
2651
+ validateCircuitBreakerOptions(options.circuitBreaker);
2652
+ validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2653
+ validateTags(options.tags);
2210
2654
  }
2211
2655
  assertActive(operation) {
2212
2656
  if (this.isDisconnecting) {
@@ -2218,24 +2662,6 @@ var CacheStack = class extends EventEmitter {
2218
2662
  await this.startup;
2219
2663
  this.assertActive(operation);
2220
2664
  }
2221
- serializeOptions(options) {
2222
- return JSON.stringify(this.normalizeForSerialization(options) ?? null);
2223
- }
2224
- validateAdaptiveTtlOptions(options) {
2225
- if (!options || options === true) {
2226
- return;
2227
- }
2228
- this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
2229
- this.validateLayerNumberOption("adaptiveTtl.step", options.step);
2230
- this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
2231
- }
2232
- validateCircuitBreakerOptions(options) {
2233
- if (!options) {
2234
- return;
2235
- }
2236
- this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
2237
- this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
2238
- }
2239
2665
  async applyFreshReadPolicies(key, hit, options, fetcher) {
2240
2666
  const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
2241
2667
  const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
@@ -2303,18 +2729,6 @@ var CacheStack = class extends EventEmitter {
2303
2729
  this.emit("error", { operation, ...context });
2304
2730
  }
2305
2731
  }
2306
- serializeKeyPart(value) {
2307
- if (typeof value === "string") {
2308
- return `s:${value}`;
2309
- }
2310
- if (typeof value === "number") {
2311
- return `n:${value}`;
2312
- }
2313
- if (typeof value === "boolean") {
2314
- return `b:${value}`;
2315
- }
2316
- return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
2317
- }
2318
2732
  isCacheSnapshotEntries(value) {
2319
2733
  return Array.isArray(value) && value.every((entry) => {
2320
2734
  if (!entry || typeof entry !== "object") {
@@ -2327,43 +2741,72 @@ var CacheStack = class extends EventEmitter {
2327
2741
  sanitizeSnapshotValue(value) {
2328
2742
  return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
2329
2743
  }
2330
- async validateSnapshotFilePath(filePath) {
2331
- if (filePath.length === 0) {
2332
- throw new Error("filePath must not be empty.");
2333
- }
2334
- if (filePath.includes("\0")) {
2335
- throw new Error("filePath must not contain null bytes.");
2744
+ snapshotMaxBytes() {
2745
+ return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
2746
+ }
2747
+ snapshotMaxEntries() {
2748
+ return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
2749
+ }
2750
+ invalidationMaxKeys() {
2751
+ return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
2752
+ }
2753
+ async collectKeysForTag(tag) {
2754
+ const keys = /* @__PURE__ */ new Set();
2755
+ if (this.tagIndex.forEachKeyForTag) {
2756
+ await this.tagIndex.forEachKeyForTag(tag, async (key) => {
2757
+ keys.add(key);
2758
+ this.assertWithinInvalidationKeyLimit(keys.size);
2759
+ });
2760
+ return [...keys];
2336
2761
  }
2337
- const path = await import("path");
2338
- const resolved = path.resolve(filePath);
2339
- const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
2340
- if (baseDir !== false) {
2341
- const relative = path.relative(baseDir, resolved);
2342
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2343
- throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
2344
- }
2762
+ for (const key of await this.tagIndex.keysForTag(tag)) {
2763
+ keys.add(key);
2764
+ this.assertWithinInvalidationKeyLimit(keys.size);
2345
2765
  }
2346
- return resolved;
2766
+ return [...keys];
2347
2767
  }
2348
- normalizeForSerialization(value) {
2349
- if (Array.isArray(value)) {
2350
- return value.map((entry) => this.normalizeForSerialization(entry));
2768
+ assertWithinInvalidationKeyLimit(size) {
2769
+ const maxKeys = this.invalidationMaxKeys();
2770
+ if (maxKeys !== false && size > maxKeys) {
2771
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
2351
2772
  }
2352
- if (value && typeof value === "object") {
2353
- return Object.keys(value).sort().reduce((normalized, key) => {
2354
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
2355
- return normalized;
2773
+ }
2774
+ async visitExportEntries(maxEntries, visitor) {
2775
+ const exported = /* @__PURE__ */ new Set();
2776
+ for (const layer of this.layers) {
2777
+ if (!layer.keys && !layer.forEachKey) {
2778
+ continue;
2779
+ }
2780
+ const visitKey = async (key) => {
2781
+ const exportedKey = this.stripQualifiedKey(key);
2782
+ if (exported.has(exportedKey)) {
2783
+ return;
2356
2784
  }
2357
- normalized[key] = this.normalizeForSerialization(value[key]);
2358
- return normalized;
2359
- }, {});
2785
+ const stored = await this.readLayerEntry(layer, key);
2786
+ if (stored === null) {
2787
+ return;
2788
+ }
2789
+ exported.add(exportedKey);
2790
+ if (maxEntries !== false && exported.size > maxEntries) {
2791
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
2792
+ }
2793
+ await visitor({
2794
+ key: exportedKey,
2795
+ value: stored,
2796
+ ttl: remainingStoredTtlSeconds(stored)
2797
+ });
2798
+ };
2799
+ if (layer.forEachKey) {
2800
+ await layer.forEachKey(visitKey);
2801
+ continue;
2802
+ }
2803
+ const keys = await layer.keys?.();
2804
+ for (const key of keys ?? []) {
2805
+ await visitKey(key);
2806
+ }
2360
2807
  }
2361
- return value;
2362
2808
  }
2363
2809
  };
2364
- function createInstanceId() {
2365
- return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2366
- }
2367
2810
 
2368
2811
  // src/invalidation/RedisInvalidationBus.ts
2369
2812
  var RedisInvalidationBus = class {
@@ -2404,7 +2847,7 @@ var RedisInvalidationBus = class {
2404
2847
  async dispatchToHandlers(payload) {
2405
2848
  let message;
2406
2849
  try {
2407
- const parsed = JSON.parse(payload);
2850
+ const parsed = sanitizeJsonValue2(JSON.parse(payload));
2408
2851
  if (!this.isInvalidationMessage(parsed)) {
2409
2852
  throw new Error("Invalid invalidation payload shape.");
2410
2853
  }
@@ -2441,12 +2884,45 @@ var RedisInvalidationBus = class {
2441
2884
  console.error(`[layercache] ${message}`, error);
2442
2885
  }
2443
2886
  };
2887
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2888
+ var MAX_SANITIZE_DEPTH2 = 64;
2889
+ var MAX_SANITIZE_NODES2 = 1e4;
2890
+ function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
2891
+ state.count += 1;
2892
+ if (state.count > MAX_SANITIZE_NODES2) {
2893
+ throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
2894
+ }
2895
+ if (depth > MAX_SANITIZE_DEPTH2) {
2896
+ throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
2897
+ }
2898
+ if (Array.isArray(value)) {
2899
+ return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
2900
+ }
2901
+ if (value && typeof value === "object") {
2902
+ const result = /* @__PURE__ */ Object.create(null);
2903
+ for (const key of Object.keys(value)) {
2904
+ if (!DANGEROUS_KEYS.has(key)) {
2905
+ result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
2906
+ }
2907
+ }
2908
+ return result;
2909
+ }
2910
+ return value;
2911
+ }
2444
2912
 
2445
2913
  // src/http/createCacheStatsHandler.ts
2446
- function createCacheStatsHandler(cache) {
2447
- return async (_request, response) => {
2448
- response.statusCode = 200;
2914
+ function createCacheStatsHandler(cache, options = {}) {
2915
+ return async (request, response) => {
2449
2916
  response.setHeader?.("content-type", "application/json; charset=utf-8");
2917
+ response.setHeader?.("cache-control", "no-store");
2918
+ response.setHeader?.("x-content-type-options", "nosniff");
2919
+ const isAuthorized = options.allowPublicAccess === true || (options.authorize ? await options.authorize(request) : false);
2920
+ if (!isAuthorized) {
2921
+ response.statusCode = options.unauthorizedStatusCode ?? 403;
2922
+ response.end(JSON.stringify({ error: "Forbidden" }));
2923
+ return;
2924
+ }
2925
+ response.statusCode = 200;
2450
2926
  response.end(JSON.stringify(cache.getStats(), null, 2));
2451
2927
  };
2452
2928
  }
@@ -2481,7 +2957,26 @@ function createFastifyLayercachePlugin(cache, options = {}) {
2481
2957
  return async (fastify) => {
2482
2958
  fastify.decorate("cache", cache);
2483
2959
  if (options.exposeStatsRoute === true && fastify.get) {
2484
- fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
2960
+ fastify.get(options.statsPath ?? "/cache/stats", async (request, reply) => {
2961
+ const isAuthorized = options.allowPublicStatsRoute === true || (options.authorizeStatsRoute ? await options.authorizeStatsRoute(request) : false);
2962
+ reply.header?.("cache-control", "no-store");
2963
+ reply.header?.("x-content-type-options", "nosniff");
2964
+ if (!isAuthorized) {
2965
+ reply.statusCode = options.unauthorizedStatusCode ?? 403;
2966
+ const body2 = { error: "Forbidden" };
2967
+ if (reply.send) {
2968
+ reply.send(body2);
2969
+ return;
2970
+ }
2971
+ return body2;
2972
+ }
2973
+ const body = cache.getStats();
2974
+ if (reply.send) {
2975
+ reply.send(body);
2976
+ return;
2977
+ }
2978
+ return body;
2979
+ });
2485
2980
  }
2486
2981
  };
2487
2982
  }
@@ -2496,6 +2991,10 @@ function createExpressCacheMiddleware(cache, options = {}) {
2496
2991
  next();
2497
2992
  return;
2498
2993
  }
2994
+ if (!options.keyResolver && options.allowPrivateCaching !== true) {
2995
+ next();
2996
+ return;
2997
+ }
2499
2998
  const rawUrl = req.originalUrl ?? req.url ?? "/";
2500
2999
  const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
2501
3000
  const cached = await cache.get(key, void 0, options);
@@ -2532,7 +3031,7 @@ function normalizeUrl(url) {
2532
3031
  try {
2533
3032
  const parsed = new URL(url, "http://localhost");
2534
3033
  parsed.searchParams.sort();
2535
- return decodeURIComponent(parsed.pathname) + parsed.search;
3034
+ return parsed.pathname + parsed.search;
2536
3035
  } catch {
2537
3036
  return url;
2538
3037
  }
@@ -2540,6 +3039,11 @@ function normalizeUrl(url) {
2540
3039
 
2541
3040
  // src/integrations/graphql.ts
2542
3041
  function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
3042
+ if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
3043
+ throw new Error(
3044
+ "cacheGraphqlResolver requires a keyResolver or allowImplicitContextCaching=true because resolver output may depend on request context."
3045
+ );
3046
+ }
2543
3047
  const wrapped = cache.wrap(prefix, resolver, {
2544
3048
  ...options,
2545
3049
  keyResolver: options.keyResolver
@@ -2611,6 +3115,11 @@ function instrument(name, tracer, method, attributes) {
2611
3115
 
2612
3116
  // src/integrations/trpc.ts
2613
3117
  function createTrpcCacheMiddleware(cache, prefix, options = {}) {
3118
+ if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
3119
+ throw new Error(
3120
+ "createTrpcCacheMiddleware requires a keyResolver or allowImplicitContextCaching=true because procedure output may depend on request context."
3121
+ );
3122
+ }
2614
3123
  return async (context) => {
2615
3124
  const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
2616
3125
  let didFetch = false;
@@ -2635,13 +3144,12 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
2635
3144
  }
2636
3145
 
2637
3146
  // src/layers/RedisLayer.ts
3147
+ import { Readable } from "stream";
2638
3148
  import { promisify } from "util";
2639
- import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
3149
+ import { brotliCompress, createBrotliDecompress, createGunzip, gzip } from "zlib";
2640
3150
  var BATCH_DELETE_SIZE = 500;
2641
3151
  var gzipAsync = promisify(gzip);
2642
- var gunzipAsync = promisify(gunzip);
2643
3152
  var brotliCompressAsync = promisify(brotliCompress);
2644
- var brotliDecompressAsync = promisify(brotliDecompress);
2645
3153
  var RedisLayer = class {
2646
3154
  name;
2647
3155
  defaultTtl;
@@ -2749,8 +3257,18 @@ var RedisLayer = class {
2749
3257
  return remaining;
2750
3258
  }
2751
3259
  async size() {
2752
- const keys = await this.keys();
2753
- return keys.length;
3260
+ if (!this.prefix) {
3261
+ return this.client.dbsize();
3262
+ }
3263
+ const pattern = `${this.prefix}*`;
3264
+ let cursor = "0";
3265
+ let count = 0;
3266
+ do {
3267
+ const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
3268
+ cursor = nextCursor;
3269
+ count += keys.length;
3270
+ } while (cursor !== "0");
3271
+ return count;
2754
3272
  }
2755
3273
  async ping() {
2756
3274
  try {
@@ -2796,6 +3314,17 @@ var RedisLayer = class {
2796
3314
  }
2797
3315
  return keys.map((key) => key.slice(this.prefix.length));
2798
3316
  }
3317
+ async forEachKey(visitor) {
3318
+ const pattern = `${this.prefix}*`;
3319
+ let cursor = "0";
3320
+ do {
3321
+ const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
3322
+ cursor = nextCursor;
3323
+ for (const key of keys) {
3324
+ await visitor(this.prefix ? key.slice(this.prefix.length) : key);
3325
+ }
3326
+ } while (cursor !== "0");
3327
+ }
2799
3328
  async scanKeys(pattern) {
2800
3329
  const matches = [];
2801
3330
  let cursor = "0";
@@ -2810,7 +3339,13 @@ var RedisLayer = class {
2810
3339
  return `${this.prefix}${key}`;
2811
3340
  }
2812
3341
  async deserializeOrDelete(key, payload) {
2813
- const decodedPayload = await this.decodePayload(payload);
3342
+ let decodedPayload;
3343
+ try {
3344
+ decodedPayload = await this.decodePayload(payload);
3345
+ } catch {
3346
+ await this.deleteCorruptedKey(key);
3347
+ return null;
3348
+ }
2814
3349
  for (const serializer of this.serializers) {
2815
3350
  try {
2816
3351
  const value = serializer.deserialize(decodedPayload);
@@ -2821,11 +3356,15 @@ var RedisLayer = class {
2821
3356
  } catch {
2822
3357
  }
2823
3358
  }
3359
+ await this.deleteCorruptedKey(key);
3360
+ return null;
3361
+ }
3362
+ async deleteCorruptedKey(key) {
2824
3363
  try {
2825
- await this.client.del(this.withPrefix(key)).catch(() => void 0);
2826
- } catch {
3364
+ await this.client.del(this.withPrefix(key));
3365
+ } catch (deleteError) {
3366
+ console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
2827
3367
  }
2828
- return null;
2829
3368
  }
2830
3369
  async rewriteWithPrimarySerializer(key, value) {
2831
3370
  const serialized = this.primarySerializer().serialize(value);
@@ -2872,31 +3411,72 @@ var RedisLayer = class {
2872
3411
  return payload;
2873
3412
  }
2874
3413
  if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
2875
- const decompressed = await gunzipAsync(payload.subarray(10));
2876
- if (decompressed.byteLength > this.decompressionMaxBytes) {
2877
- throw new Error(
2878
- `Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
2879
- );
2880
- }
2881
- return decompressed;
3414
+ return this.decompressWithLimit(createGunzip(), payload.subarray(10));
2882
3415
  }
2883
3416
  if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
2884
- const decompressed = await brotliDecompressAsync(payload.subarray(12));
2885
- if (decompressed.byteLength > this.decompressionMaxBytes) {
2886
- throw new Error(
2887
- `Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
2888
- );
2889
- }
2890
- return decompressed;
3417
+ return this.decompressWithLimit(createBrotliDecompress(), payload.subarray(12));
2891
3418
  }
2892
3419
  return payload;
2893
3420
  }
3421
+ async decompressWithLimit(decompressor, payload) {
3422
+ return new Promise((resolve2, reject) => {
3423
+ const source = Readable.from(payload);
3424
+ const chunks = [];
3425
+ let totalBytes = 0;
3426
+ let settled = false;
3427
+ const cleanup = () => {
3428
+ decompressor.removeAllListeners();
3429
+ };
3430
+ const fail = (error) => {
3431
+ if (settled) {
3432
+ return;
3433
+ }
3434
+ settled = true;
3435
+ cleanup();
3436
+ source.unpipe(decompressor);
3437
+ source.destroy();
3438
+ decompressor.destroy();
3439
+ reject(error);
3440
+ };
3441
+ decompressor.on("data", (chunk) => {
3442
+ const normalized = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3443
+ totalBytes += normalized.byteLength;
3444
+ if (totalBytes > this.decompressionMaxBytes) {
3445
+ fail(
3446
+ new Error(
3447
+ `Decompressed payload (${totalBytes} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
3448
+ )
3449
+ );
3450
+ return;
3451
+ }
3452
+ chunks.push(normalized);
3453
+ });
3454
+ decompressor.once("error", (error) => {
3455
+ if (settled) {
3456
+ return;
3457
+ }
3458
+ settled = true;
3459
+ cleanup();
3460
+ reject(error);
3461
+ });
3462
+ decompressor.once("end", () => {
3463
+ if (settled) {
3464
+ return;
3465
+ }
3466
+ settled = true;
3467
+ cleanup();
3468
+ resolve2(Buffer.concat(chunks));
3469
+ });
3470
+ source.pipe(decompressor);
3471
+ });
3472
+ }
2894
3473
  };
2895
3474
 
2896
3475
  // src/layers/DiskLayer.ts
2897
3476
  import { createHash } from "crypto";
2898
3477
  import { promises as fs } from "fs";
2899
3478
  import { join, resolve } from "path";
3479
+ var FILE_SCAN_CONCURRENCY = 32;
2900
3480
  var DiskLayer = class {
2901
3481
  name;
2902
3482
  defaultTtl;
@@ -2904,6 +3484,7 @@ var DiskLayer = class {
2904
3484
  directory;
2905
3485
  serializer;
2906
3486
  maxFiles;
3487
+ maxEntryBytes;
2907
3488
  writeQueue = Promise.resolve();
2908
3489
  constructor(options) {
2909
3490
  this.directory = this.resolveDirectory(options.directory);
@@ -2911,16 +3492,15 @@ var DiskLayer = class {
2911
3492
  this.name = options.name ?? "disk";
2912
3493
  this.serializer = options.serializer ?? new JsonSerializer();
2913
3494
  this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
3495
+ this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
2914
3496
  }
2915
3497
  async get(key) {
2916
3498
  return unwrapStoredValue(await this.getEntry(key));
2917
3499
  }
2918
3500
  async getEntry(key) {
2919
3501
  const filePath = this.keyToPath(key);
2920
- let raw;
2921
- try {
2922
- raw = await fs.readFile(filePath);
2923
- } catch {
3502
+ const raw = await this.readEntryFile(filePath);
3503
+ if (raw === null) {
2924
3504
  return null;
2925
3505
  }
2926
3506
  let entry;
@@ -2971,10 +3551,8 @@ var DiskLayer = class {
2971
3551
  }
2972
3552
  async ttl(key) {
2973
3553
  const filePath = this.keyToPath(key);
2974
- let raw;
2975
- try {
2976
- raw = await fs.readFile(filePath);
2977
- } catch {
3554
+ const raw = await this.readEntryFile(filePath);
3555
+ if (raw === null) {
2978
3556
  return null;
2979
3557
  }
2980
3558
  let entry;
@@ -2998,7 +3576,7 @@ var DiskLayer = class {
2998
3576
  }
2999
3577
  async deleteMany(keys) {
3000
3578
  await this.enqueueWrite(async () => {
3001
- await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
3579
+ await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
3002
3580
  });
3003
3581
  }
3004
3582
  async clear() {
@@ -3009,8 +3587,8 @@ var DiskLayer = class {
3009
3587
  } catch {
3010
3588
  return;
3011
3589
  }
3012
- await Promise.all(
3013
- entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
3590
+ await this.deletePathsWithConcurrency(
3591
+ entries.filter((name) => name.endsWith(".lc")).map((name) => join(this.directory, name))
3014
3592
  );
3015
3593
  });
3016
3594
  }
@@ -3019,42 +3597,23 @@ var DiskLayer = class {
3019
3597
  * Expired entries are skipped and cleaned up during the scan.
3020
3598
  */
3021
3599
  async keys() {
3022
- let entries;
3023
- try {
3024
- entries = await fs.readdir(this.directory);
3025
- } catch {
3026
- return [];
3027
- }
3028
- const lcFiles = entries.filter((name) => name.endsWith(".lc"));
3029
3600
  const keys = [];
3030
- await Promise.all(
3031
- lcFiles.map(async (name) => {
3032
- const filePath = join(this.directory, name);
3033
- let raw;
3034
- try {
3035
- raw = await fs.readFile(filePath);
3036
- } catch {
3037
- return;
3038
- }
3039
- let entry;
3040
- try {
3041
- entry = this.deserializeEntry(raw);
3042
- } catch {
3043
- await this.safeDelete(filePath);
3044
- return;
3045
- }
3046
- if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
3047
- await this.safeDelete(filePath);
3048
- return;
3049
- }
3050
- keys.push(entry.key);
3051
- })
3052
- );
3601
+ await this.scanEntries(async (entry) => {
3602
+ keys.push(entry.key);
3603
+ });
3053
3604
  return keys;
3054
3605
  }
3606
+ async forEachKey(visitor) {
3607
+ await this.scanEntries(async (entry) => {
3608
+ await visitor(entry.key);
3609
+ });
3610
+ }
3055
3611
  async size() {
3056
- const keys = await this.keys();
3057
- return keys.length;
3612
+ let count = 0;
3613
+ await this.scanEntries(async () => {
3614
+ count += 1;
3615
+ });
3616
+ return count;
3058
3617
  }
3059
3618
  async ping() {
3060
3619
  try {
@@ -3088,6 +3647,113 @@ var DiskLayer = class {
3088
3647
  }
3089
3648
  return maxFiles;
3090
3649
  }
3650
+ normalizeMaxEntryBytes(maxEntryBytes) {
3651
+ if (maxEntryBytes === false) {
3652
+ return false;
3653
+ }
3654
+ const normalized = maxEntryBytes ?? 16 * 1024 * 1024;
3655
+ if (!Number.isFinite(normalized) || normalized <= 0) {
3656
+ throw new Error("DiskLayer.maxEntryBytes must be a positive number or false.");
3657
+ }
3658
+ return normalized;
3659
+ }
3660
+ async readEntryFile(filePath) {
3661
+ let handle;
3662
+ try {
3663
+ handle = await fs.open(filePath, "r");
3664
+ return await this.readHandleWithLimit(handle);
3665
+ } catch {
3666
+ await this.safeDelete(filePath);
3667
+ return null;
3668
+ } finally {
3669
+ await handle?.close().catch(() => void 0);
3670
+ }
3671
+ }
3672
+ async readHandleWithLimit(handle) {
3673
+ if (this.maxEntryBytes === false) {
3674
+ return handle.readFile();
3675
+ }
3676
+ const stat = await handle.stat();
3677
+ if (stat.size > this.maxEntryBytes) {
3678
+ throw new Error(`DiskLayer entry exceeds maxEntryBytes limit (${stat.size} bytes > ${this.maxEntryBytes} bytes).`);
3679
+ }
3680
+ const chunks = [];
3681
+ let totalBytes = 0;
3682
+ let position = 0;
3683
+ while (true) {
3684
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, this.maxEntryBytes - totalBytes + 1));
3685
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
3686
+ if (bytesRead === 0) {
3687
+ break;
3688
+ }
3689
+ totalBytes += bytesRead;
3690
+ if (totalBytes > this.maxEntryBytes) {
3691
+ throw new Error(
3692
+ `DiskLayer entry exceeds maxEntryBytes limit (${totalBytes} bytes > ${this.maxEntryBytes} bytes).`
3693
+ );
3694
+ }
3695
+ chunks.push(buffer.subarray(0, bytesRead));
3696
+ position += bytesRead;
3697
+ }
3698
+ return Buffer.concat(chunks);
3699
+ }
3700
+ async scanEntries(visitor) {
3701
+ let entries;
3702
+ try {
3703
+ entries = await fs.readdir(this.directory);
3704
+ } catch {
3705
+ return;
3706
+ }
3707
+ const lcFiles = entries.filter((name) => name.endsWith(".lc"));
3708
+ let nextIndex = 0;
3709
+ const workerCount = Math.min(FILE_SCAN_CONCURRENCY, lcFiles.length);
3710
+ await Promise.all(
3711
+ Array.from({ length: workerCount }, async () => {
3712
+ while (true) {
3713
+ const currentIndex = nextIndex;
3714
+ nextIndex += 1;
3715
+ const name = lcFiles[currentIndex];
3716
+ if (name === void 0) {
3717
+ return;
3718
+ }
3719
+ const filePath = join(this.directory, name);
3720
+ const raw = await this.readEntryFile(filePath);
3721
+ if (raw === null) {
3722
+ continue;
3723
+ }
3724
+ let entry;
3725
+ try {
3726
+ entry = this.deserializeEntry(raw);
3727
+ } catch {
3728
+ await this.safeDelete(filePath);
3729
+ continue;
3730
+ }
3731
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
3732
+ await this.safeDelete(filePath);
3733
+ continue;
3734
+ }
3735
+ await visitor(entry);
3736
+ }
3737
+ })
3738
+ );
3739
+ }
3740
+ async deletePathsWithConcurrency(paths) {
3741
+ let nextIndex = 0;
3742
+ const workerCount = Math.min(FILE_SCAN_CONCURRENCY, paths.length);
3743
+ await Promise.all(
3744
+ Array.from({ length: workerCount }, async () => {
3745
+ while (true) {
3746
+ const currentIndex = nextIndex;
3747
+ nextIndex += 1;
3748
+ const filePath = paths[currentIndex];
3749
+ if (filePath === void 0) {
3750
+ return;
3751
+ }
3752
+ await this.safeDelete(filePath);
3753
+ }
3754
+ })
3755
+ );
3756
+ }
3091
3757
  deserializeEntry(raw) {
3092
3758
  const entry = this.serializer.deserialize(raw);
3093
3759
  if (!isDiskEntry(entry)) {
@@ -3223,29 +3889,38 @@ var MemcachedLayer = class {
3223
3889
 
3224
3890
  // src/serialization/MsgpackSerializer.ts
3225
3891
  import { decode, encode } from "@msgpack/msgpack";
3226
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3892
+ var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3893
+ var MAX_SANITIZE_DEPTH3 = 64;
3894
+ var MAX_SANITIZE_NODES3 = 1e4;
3227
3895
  var MsgpackSerializer = class {
3228
3896
  serialize(value) {
3229
3897
  return Buffer.from(encode(value));
3230
3898
  }
3231
3899
  deserialize(payload) {
3232
3900
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
3233
- return sanitizeMsgpackValue(decode(normalized));
3901
+ return sanitizeMsgpackValue(decode(normalized), 0, { count: 0 });
3234
3902
  }
3235
3903
  };
3236
- function sanitizeMsgpackValue(value) {
3904
+ function sanitizeMsgpackValue(value, depth, state) {
3905
+ state.count += 1;
3906
+ if (state.count > MAX_SANITIZE_NODES3) {
3907
+ throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
3908
+ }
3909
+ if (depth > MAX_SANITIZE_DEPTH3) {
3910
+ throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
3911
+ }
3237
3912
  if (Array.isArray(value)) {
3238
- return value.map((entry) => sanitizeMsgpackValue(entry));
3913
+ return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
3239
3914
  }
3240
3915
  if (!isPlainObject2(value)) {
3241
3916
  return value;
3242
3917
  }
3243
3918
  const sanitized = {};
3244
3919
  for (const [key, entry] of Object.entries(value)) {
3245
- if (DANGEROUS_KEYS.has(key)) {
3920
+ if (DANGEROUS_KEYS2.has(key)) {
3246
3921
  continue;
3247
3922
  }
3248
- sanitized[key] = sanitizeMsgpackValue(entry);
3923
+ sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
3249
3924
  }
3250
3925
  return sanitized;
3251
3926
  }