layercache 1.2.5 → 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.
@@ -264,22 +264,23 @@ var CacheNamespace = class _CacheNamespace {
264
264
  constructor(cache, prefix) {
265
265
  this.cache = cache;
266
266
  this.prefix = prefix;
267
+ validateNamespaceKey(prefix);
267
268
  }
268
269
  cache;
269
270
  prefix;
270
271
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
271
272
  metrics = emptyMetrics();
272
273
  async get(key, fetcher, options) {
273
- return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
274
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
274
275
  }
275
276
  async getOrSet(key, fetcher, options) {
276
- return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
277
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
277
278
  }
278
279
  /**
279
280
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
280
281
  */
281
282
  async getOrThrow(key, fetcher, options) {
282
- return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
283
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
283
284
  }
284
285
  async has(key) {
285
286
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
@@ -288,7 +289,7 @@ var CacheNamespace = class _CacheNamespace {
288
289
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
289
290
  }
290
291
  async set(key, value, options) {
291
- await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
292
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
292
293
  }
293
294
  async delete(key) {
294
295
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
@@ -304,7 +305,8 @@ var CacheNamespace = class _CacheNamespace {
304
305
  () => this.cache.mget(
305
306
  entries.map((entry) => ({
306
307
  ...entry,
307
- key: this.qualify(entry.key)
308
+ key: this.qualify(entry.key),
309
+ options: this.qualifyGetOptions(entry.options)
308
310
  }))
309
311
  )
310
312
  );
@@ -314,16 +316,22 @@ var CacheNamespace = class _CacheNamespace {
314
316
  () => this.cache.mset(
315
317
  entries.map((entry) => ({
316
318
  ...entry,
317
- key: this.qualify(entry.key)
319
+ key: this.qualify(entry.key),
320
+ options: this.qualifyWriteOptions(entry.options)
318
321
  }))
319
322
  )
320
323
  );
321
324
  }
322
325
  async invalidateByTag(tag) {
323
- await this.trackMetrics(() => this.cache.invalidateByTag(tag));
326
+ await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
324
327
  }
325
328
  async invalidateByTags(tags, mode = "any") {
326
- await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
329
+ await this.trackMetrics(
330
+ () => this.cache.invalidateByTags(
331
+ tags.map((tag) => this.qualifyTag(tag)),
332
+ mode
333
+ )
334
+ );
327
335
  }
328
336
  async invalidateByPattern(pattern) {
329
337
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
@@ -335,16 +343,24 @@ var CacheNamespace = class _CacheNamespace {
335
343
  * Returns detailed metadata about a single cache key within this namespace.
336
344
  */
337
345
  async inspect(key) {
338
- return this.cache.inspect(this.qualify(key));
346
+ const result = await this.cache.inspect(this.qualify(key));
347
+ if (result === null) {
348
+ return null;
349
+ }
350
+ return {
351
+ ...result,
352
+ tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
353
+ };
339
354
  }
340
355
  wrap(keyPrefix, fetcher, options) {
341
- return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
356
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
342
357
  }
343
358
  warm(entries, options) {
344
359
  return this.cache.warm(
345
360
  entries.map((entry) => ({
346
361
  ...entry,
347
- key: this.qualify(entry.key)
362
+ key: this.qualify(entry.key),
363
+ options: this.qualifyGetOptions(entry.options)
348
364
  })),
349
365
  options
350
366
  );
@@ -380,6 +396,24 @@ var CacheNamespace = class _CacheNamespace {
380
396
  qualify(key) {
381
397
  return `${this.prefix}:${key}`;
382
398
  }
399
+ qualifyTag(tag) {
400
+ return `${this.prefix}:${tag}`;
401
+ }
402
+ qualifyGetOptions(options) {
403
+ return this.qualifyWriteOptions(options);
404
+ }
405
+ qualifyWrapOptions(options) {
406
+ return this.qualifyWriteOptions(options);
407
+ }
408
+ qualifyWriteOptions(options) {
409
+ if (!options?.tags || options.tags.length === 0) {
410
+ return options;
411
+ }
412
+ return {
413
+ ...options,
414
+ tags: options.tags.map((tag) => this.qualifyTag(tag))
415
+ };
416
+ }
383
417
  async trackMetrics(operation) {
384
418
  return this.getMetricsMutex().runExclusive(async () => {
385
419
  const before = this.cache.getMetrics();
@@ -514,6 +548,9 @@ function validateNamespaceKey(key) {
514
548
  if (/[\u0000-\u001F\u007F]/.test(key)) {
515
549
  throw new Error("Namespace prefix contains unsupported control characters.");
516
550
  }
551
+ if (/[\uD800-\uDFFF]/.test(key)) {
552
+ throw new Error("Namespace prefix contains unsupported surrogate code points.");
553
+ }
517
554
  }
518
555
 
519
556
  // ../../src/invalidation/PatternMatcher.ts
@@ -570,21 +607,41 @@ var CacheKeyDiscovery = class {
570
607
  this.options = options;
571
608
  }
572
609
  options;
573
- async collectKeysWithPrefix(prefix) {
610
+ async collectKeysWithPrefix(prefix, maxMatches = false) {
574
611
  const { tagIndex } = this.options;
575
- const matches = new Set(
576
- tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
577
- );
612
+ const matches = /* @__PURE__ */ new Set();
613
+ if (tagIndex.forEachKeyForPrefix) {
614
+ await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
615
+ matches.add(key);
616
+ this.assertWithinMatchLimit(matches, maxMatches);
617
+ });
618
+ } else {
619
+ const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
620
+ for (const key of initialMatches) {
621
+ matches.add(key);
622
+ this.assertWithinMatchLimit(matches, maxMatches);
623
+ }
624
+ }
578
625
  await Promise.all(
579
626
  this.options.layers.map(async (layer) => {
580
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
627
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
581
628
  return;
582
629
  }
583
630
  try {
584
- const keys = await layer.keys();
585
- for (const key of keys) {
631
+ if (layer.forEachKey) {
632
+ await layer.forEachKey(async (key) => {
633
+ if (key.startsWith(prefix)) {
634
+ matches.add(key);
635
+ this.assertWithinMatchLimit(matches, maxMatches);
636
+ }
637
+ });
638
+ return;
639
+ }
640
+ const keys = await layer.keys?.();
641
+ for (const key of keys ?? []) {
586
642
  if (key.startsWith(prefix)) {
587
643
  matches.add(key);
644
+ this.assertWithinMatchLimit(matches, maxMatches);
588
645
  }
589
646
  }
590
647
  } catch (error) {
@@ -594,18 +651,39 @@ var CacheKeyDiscovery = class {
594
651
  );
595
652
  return [...matches];
596
653
  }
597
- async collectKeysMatchingPattern(pattern) {
598
- const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
654
+ async collectKeysMatchingPattern(pattern, maxMatches = false) {
655
+ const matches = /* @__PURE__ */ new Set();
656
+ if (this.options.tagIndex.forEachKeyMatchingPattern) {
657
+ await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
658
+ matches.add(key);
659
+ this.assertWithinMatchLimit(matches, maxMatches);
660
+ });
661
+ } else {
662
+ for (const key of await this.options.tagIndex.matchPattern(pattern)) {
663
+ matches.add(key);
664
+ this.assertWithinMatchLimit(matches, maxMatches);
665
+ }
666
+ }
599
667
  await Promise.all(
600
668
  this.options.layers.map(async (layer) => {
601
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
669
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
602
670
  return;
603
671
  }
604
672
  try {
605
- const keys = await layer.keys();
606
- for (const key of keys) {
673
+ if (layer.forEachKey) {
674
+ await layer.forEachKey(async (key) => {
675
+ if (PatternMatcher.matches(pattern, key)) {
676
+ matches.add(key);
677
+ this.assertWithinMatchLimit(matches, maxMatches);
678
+ }
679
+ });
680
+ return;
681
+ }
682
+ const keys = await layer.keys?.();
683
+ for (const key of keys ?? []) {
607
684
  if (PatternMatcher.matches(pattern, key)) {
608
685
  matches.add(key);
686
+ this.assertWithinMatchLimit(matches, maxMatches);
609
687
  }
610
688
  }
611
689
  } catch (error) {
@@ -615,8 +693,280 @@ var CacheKeyDiscovery = class {
615
693
  );
616
694
  return [...matches];
617
695
  }
696
+ assertWithinMatchLimit(matches, maxMatches) {
697
+ if (maxMatches !== false && matches.size > maxMatches) {
698
+ throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
699
+ }
700
+ }
618
701
  };
619
702
 
703
+ // ../../src/internal/CacheKeySerialization.ts
704
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
705
+ function normalizeForSerialization(value) {
706
+ if (Array.isArray(value)) {
707
+ return value.map((entry) => normalizeForSerialization(entry));
708
+ }
709
+ if (value && typeof value === "object") {
710
+ return Object.keys(value).sort().reduce((normalized, key) => {
711
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
712
+ return normalized;
713
+ }
714
+ normalized[key] = normalizeForSerialization(value[key]);
715
+ return normalized;
716
+ }, {});
717
+ }
718
+ return value;
719
+ }
720
+ function serializeKeyPart(value) {
721
+ if (typeof value === "string") {
722
+ return `s:${value}`;
723
+ }
724
+ if (typeof value === "number") {
725
+ return `n:${value}`;
726
+ }
727
+ if (typeof value === "boolean") {
728
+ return `b:${value}`;
729
+ }
730
+ return `j:${JSON.stringify(normalizeForSerialization(value))}`;
731
+ }
732
+ function serializeOptions(options) {
733
+ return JSON.stringify(normalizeForSerialization(options) ?? null);
734
+ }
735
+ function createInstanceId() {
736
+ if (globalThis.crypto?.randomUUID) {
737
+ return globalThis.crypto.randomUUID();
738
+ }
739
+ const bytes = new Uint8Array(16);
740
+ if (globalThis.crypto?.getRandomValues) {
741
+ globalThis.crypto.getRandomValues(bytes);
742
+ } else {
743
+ for (let i = 0; i < bytes.length; i += 1) {
744
+ bytes[i] = Math.floor(Math.random() * 256);
745
+ }
746
+ }
747
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
748
+ }
749
+
750
+ // ../../src/internal/CacheSnapshotFile.ts
751
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
752
+ const relative = path.relative(realBaseDir, candidatePath);
753
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
754
+ }
755
+ async function findExistingAncestor(directory, fs, path) {
756
+ let current = directory;
757
+ while (true) {
758
+ try {
759
+ await fs.lstat(current);
760
+ return current;
761
+ } catch (error) {
762
+ if (error.code !== "ENOENT") {
763
+ throw error;
764
+ }
765
+ }
766
+ const parent = path.dirname(current);
767
+ if (parent === current) {
768
+ return current;
769
+ }
770
+ current = parent;
771
+ }
772
+ }
773
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
774
+ if (filePath.length === 0) {
775
+ throw new Error("filePath must not be empty.");
776
+ }
777
+ if (filePath.includes("\0")) {
778
+ throw new Error("filePath must not contain null bytes.");
779
+ }
780
+ const { promises: fs } = await import("fs");
781
+ const path = await import("path");
782
+ const resolved = path.resolve(filePath);
783
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
784
+ if (baseDir === false) {
785
+ return resolved;
786
+ }
787
+ await fs.mkdir(baseDir, { recursive: true });
788
+ const realBaseDir = await fs.realpath(baseDir);
789
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
790
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
791
+ }
792
+ if (mode === "read") {
793
+ const realTarget = await fs.realpath(resolved);
794
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
795
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
796
+ }
797
+ return realTarget;
798
+ }
799
+ const parentDir = path.dirname(resolved);
800
+ const existingAncestor = await findExistingAncestor(parentDir, fs, path);
801
+ const realExistingAncestor = await fs.realpath(existingAncestor);
802
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
803
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
804
+ }
805
+ await fs.mkdir(parentDir, { recursive: true });
806
+ const realParentDir = await fs.realpath(parentDir);
807
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
808
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
809
+ }
810
+ const targetPath = path.join(realParentDir, path.basename(resolved));
811
+ try {
812
+ const existing = await fs.lstat(targetPath);
813
+ if (existing.isSymbolicLink()) {
814
+ throw new Error("filePath must not point to a symbolic link.");
815
+ }
816
+ } catch (error) {
817
+ if (error.code !== "ENOENT") {
818
+ throw error;
819
+ }
820
+ }
821
+ return targetPath;
822
+ }
823
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
824
+ if (byteLimit === false) {
825
+ return handle.readFile({ encoding: "utf8" });
826
+ }
827
+ const chunks = [];
828
+ let totalBytes = 0;
829
+ let position = 0;
830
+ while (true) {
831
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
832
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
833
+ if (bytesRead === 0) {
834
+ break;
835
+ }
836
+ totalBytes += bytesRead;
837
+ if (totalBytes > byteLimit) {
838
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
839
+ }
840
+ chunks.push(buffer.subarray(0, bytesRead));
841
+ position += bytesRead;
842
+ }
843
+ return Buffer.concat(chunks).toString("utf8");
844
+ }
845
+
846
+ // ../../src/internal/CacheStackValidation.ts
847
+ var MAX_CACHE_KEY_LENGTH = 1024;
848
+ var MAX_PATTERN_LENGTH = 1024;
849
+ var MAX_TAGS_PER_OPERATION = 128;
850
+ function validatePositiveNumber(name, value) {
851
+ if (value === void 0) {
852
+ return;
853
+ }
854
+ if (!Number.isFinite(value) || value <= 0) {
855
+ throw new Error(`${name} must be a positive finite number.`);
856
+ }
857
+ }
858
+ function validateNonNegativeNumber(name, value) {
859
+ if (!Number.isFinite(value) || value < 0) {
860
+ throw new Error(`${name} must be a non-negative finite number.`);
861
+ }
862
+ }
863
+ function validateLayerNumberOption(name, value) {
864
+ if (value === void 0) {
865
+ return;
866
+ }
867
+ if (typeof value === "number") {
868
+ validateNonNegativeNumber(name, value);
869
+ return;
870
+ }
871
+ for (const [layerName, layerValue] of Object.entries(value)) {
872
+ if (layerValue === void 0) {
873
+ continue;
874
+ }
875
+ validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
876
+ }
877
+ }
878
+ function validateRateLimitOptions(name, options) {
879
+ if (!options) {
880
+ return;
881
+ }
882
+ validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
883
+ validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
884
+ validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
885
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
886
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
887
+ }
888
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
889
+ throw new Error(`${name}.bucketKey must not be empty.`);
890
+ }
891
+ }
892
+ function validateCacheKey(key) {
893
+ if (key.length === 0) {
894
+ throw new Error("Cache key must not be empty.");
895
+ }
896
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
897
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
898
+ }
899
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
900
+ throw new Error("Cache key contains unsupported control characters.");
901
+ }
902
+ if (/[\uD800-\uDFFF]/.test(key)) {
903
+ throw new Error("Cache key contains unsupported surrogate code points.");
904
+ }
905
+ return key;
906
+ }
907
+ function validateTag(tag) {
908
+ if (tag.length === 0) {
909
+ throw new Error("Cache tag must not be empty.");
910
+ }
911
+ if (tag.length > MAX_CACHE_KEY_LENGTH) {
912
+ throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
913
+ }
914
+ if (/[\u0000-\u001F\u007F]/.test(tag)) {
915
+ throw new Error("Cache tag contains unsupported control characters.");
916
+ }
917
+ if (/[\uD800-\uDFFF]/.test(tag)) {
918
+ throw new Error("Cache tag contains unsupported surrogate code points.");
919
+ }
920
+ return tag;
921
+ }
922
+ function validateTags(tags) {
923
+ if (!tags) {
924
+ return;
925
+ }
926
+ if (tags.length > MAX_TAGS_PER_OPERATION) {
927
+ throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
928
+ }
929
+ for (const tag of tags) {
930
+ validateTag(tag);
931
+ }
932
+ }
933
+ function validatePattern(pattern) {
934
+ if (pattern.length === 0) {
935
+ throw new Error("Pattern must not be empty.");
936
+ }
937
+ if (pattern.length > MAX_PATTERN_LENGTH) {
938
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
939
+ }
940
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
941
+ throw new Error("Pattern contains unsupported control characters.");
942
+ }
943
+ }
944
+ function validateTtlPolicy(name, policy) {
945
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
946
+ return;
947
+ }
948
+ if ("alignTo" in policy) {
949
+ validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
950
+ return;
951
+ }
952
+ throw new Error(`${name} is invalid.`);
953
+ }
954
+ function validateAdaptiveTtlOptions(options) {
955
+ if (!options || options === true) {
956
+ return;
957
+ }
958
+ validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
959
+ validateLayerNumberOption("adaptiveTtl.step", options.step);
960
+ validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
961
+ }
962
+ function validateCircuitBreakerOptions(options) {
963
+ if (!options) {
964
+ return;
965
+ }
966
+ validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
967
+ validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
968
+ }
969
+
620
970
  // ../../src/internal/CircuitBreakerManager.ts
621
971
  var CircuitBreakerManager = class {
622
972
  breakers = /* @__PURE__ */ new Map();
@@ -1010,19 +1360,47 @@ function isStoredValueEnvelope(value) {
1010
1360
  if (v.kind !== "value" && v.kind !== "empty") {
1011
1361
  return false;
1012
1362
  }
1013
- if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
1363
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1014
1364
  return false;
1015
1365
  }
1016
- if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
1366
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1017
1367
  return false;
1018
1368
  }
1019
- if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
1369
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1020
1370
  return false;
1021
1371
  }
1022
1372
  const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1023
1373
  if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1024
1374
  return false;
1025
1375
  }
1376
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1377
+ return false;
1378
+ }
1379
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1380
+ return false;
1381
+ }
1382
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1383
+ return false;
1384
+ }
1385
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1386
+ return false;
1387
+ }
1388
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1389
+ return false;
1390
+ }
1391
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1392
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1393
+ return false;
1394
+ }
1395
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1396
+ return false;
1397
+ }
1398
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1399
+ return false;
1400
+ }
1401
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1402
+ return false;
1403
+ }
1026
1404
  return true;
1027
1405
  }
1028
1406
  function createStoredValueEnvelope(options) {
@@ -1121,6 +1499,12 @@ function normalizePositiveSeconds(value) {
1121
1499
  }
1122
1500
  return value;
1123
1501
  }
1502
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1503
+ if (value == null) {
1504
+ return true;
1505
+ }
1506
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1507
+ }
1124
1508
 
1125
1509
  // ../../src/internal/TtlResolver.ts
1126
1510
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
@@ -1275,6 +1659,11 @@ var TagIndex = class {
1275
1659
  async keysForTag(tag) {
1276
1660
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
1277
1661
  }
1662
+ async forEachKeyForTag(tag, visitor) {
1663
+ for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
1664
+ await visitor(key);
1665
+ }
1666
+ }
1278
1667
  async keysForPrefix(prefix) {
1279
1668
  const node = this.findNode(prefix);
1280
1669
  if (!node) {
@@ -1284,6 +1673,13 @@ var TagIndex = class {
1284
1673
  this.collectFromNode(node, prefix, matches);
1285
1674
  return matches;
1286
1675
  }
1676
+ async forEachKeyForPrefix(prefix, visitor) {
1677
+ const node = this.findNode(prefix);
1678
+ if (!node) {
1679
+ return;
1680
+ }
1681
+ await this.visitFromNode(node, prefix, visitor);
1682
+ }
1287
1683
  async tagsForKey(key) {
1288
1684
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
1289
1685
  }
@@ -1292,6 +1688,12 @@ var TagIndex = class {
1292
1688
  this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1293
1689
  return [...matches];
1294
1690
  }
1691
+ async forEachKeyMatchingPattern(pattern, visitor) {
1692
+ const matches = await this.matchPattern(pattern);
1693
+ for (const key of matches) {
1694
+ await visitor(key);
1695
+ }
1696
+ }
1295
1697
  async clear() {
1296
1698
  this.tagToKeys.clear();
1297
1699
  this.keyToTags.clear();
@@ -1341,6 +1743,14 @@ var TagIndex = class {
1341
1743
  this.collectFromNode(child, `${prefix}${character}`, matches);
1342
1744
  }
1343
1745
  }
1746
+ async visitFromNode(node, prefix, visitor) {
1747
+ if (node.terminal) {
1748
+ await visitor(prefix);
1749
+ }
1750
+ for (const [character, child] of node.children) {
1751
+ await this.visitFromNode(child, `${prefix}${character}`, visitor);
1752
+ }
1753
+ }
1344
1754
  collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1345
1755
  if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1346
1756
  return;
@@ -1458,22 +1868,27 @@ var TagIndex = class {
1458
1868
 
1459
1869
  // ../../src/serialization/JsonSerializer.ts
1460
1870
  var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1871
+ var MAX_SANITIZE_NODES = 1e4;
1461
1872
  var JsonSerializer = class {
1462
1873
  serialize(value) {
1463
1874
  return JSON.stringify(value);
1464
1875
  }
1465
1876
  deserialize(payload) {
1466
1877
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1467
- return sanitizeJsonValue(JSON.parse(normalized), 0);
1878
+ return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
1468
1879
  }
1469
1880
  };
1470
1881
  var MAX_SANITIZE_DEPTH = 200;
1471
- function sanitizeJsonValue(value, depth) {
1882
+ function sanitizeJsonValue(value, depth, state) {
1883
+ state.count += 1;
1884
+ if (state.count > MAX_SANITIZE_NODES) {
1885
+ throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
1886
+ }
1472
1887
  if (depth > MAX_SANITIZE_DEPTH) {
1473
- return value;
1888
+ throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
1474
1889
  }
1475
1890
  if (Array.isArray(value)) {
1476
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
1891
+ return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
1477
1892
  }
1478
1893
  if (!isPlainObject(value)) {
1479
1894
  return value;
@@ -1483,7 +1898,7 @@ function sanitizeJsonValue(value, depth) {
1483
1898
  if (DANGEROUS_JSON_KEYS.has(key)) {
1484
1899
  continue;
1485
1900
  }
1486
- sanitized[key] = sanitizeJsonValue(entry, depth + 1);
1901
+ sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
1487
1902
  }
1488
1903
  return sanitized;
1489
1904
  }
@@ -1532,10 +1947,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1532
1947
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1533
1948
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1534
1949
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1535
- var MAX_CACHE_KEY_LENGTH = 1024;
1536
- var MAX_PATTERN_LENGTH = 1024;
1950
+ var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1951
+ var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1952
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1953
+ var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1537
1954
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1538
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1539
1955
  var DebugLogger = class {
1540
1956
  enabled;
1541
1957
  constructor(enabled) {
@@ -1622,6 +2038,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1622
2038
  snapshotSerializer = new JsonSerializer();
1623
2039
  backgroundRefreshes = /* @__PURE__ */ new Map();
1624
2040
  layerDegradedUntil = /* @__PURE__ */ new Map();
2041
+ keyEpochs = /* @__PURE__ */ new Map();
1625
2042
  ttlResolver;
1626
2043
  circuitBreakerManager;
1627
2044
  currentGeneration;
@@ -1629,6 +2046,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1629
2046
  writeBehindTimer;
1630
2047
  writeBehindFlushPromise;
1631
2048
  generationCleanupPromise;
2049
+ clearEpoch = 0;
1632
2050
  isDisconnecting = false;
1633
2051
  disconnectPromise;
1634
2052
  /**
@@ -1638,7 +2056,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1638
2056
  * and no `fetcher` is provided.
1639
2057
  */
1640
2058
  async get(key, fetcher, options) {
1641
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2059
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1642
2060
  this.validateWriteOptions(options);
1643
2061
  await this.awaitStartup("get");
1644
2062
  return this.getPrepared(normalizedKey, fetcher, options);
@@ -1708,7 +2126,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1708
2126
  * Returns true if the given key exists and is not expired in any layer.
1709
2127
  */
1710
2128
  async has(key) {
1711
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2129
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1712
2130
  await this.awaitStartup("has");
1713
2131
  for (const layer of this.layers) {
1714
2132
  if (this.shouldSkipLayer(layer)) {
@@ -1741,7 +2159,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1741
2159
  * that has it, or null if the key is not found / has no TTL.
1742
2160
  */
1743
2161
  async ttl(key) {
1744
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2162
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1745
2163
  await this.awaitStartup("ttl");
1746
2164
  for (const layer of this.layers) {
1747
2165
  if (this.shouldSkipLayer(layer)) {
@@ -1763,7 +2181,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1763
2181
  * Stores a value in all cache layers. Overwrites any existing value.
1764
2182
  */
1765
2183
  async set(key, value, options) {
1766
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2184
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1767
2185
  this.validateWriteOptions(options);
1768
2186
  await this.awaitStartup("set");
1769
2187
  await this.storeEntry(normalizedKey, "value", value, options);
@@ -1772,7 +2190,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1772
2190
  * Deletes the key from all layers and publishes an invalidation message.
1773
2191
  */
1774
2192
  async delete(key) {
1775
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2193
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1776
2194
  await this.awaitStartup("delete");
1777
2195
  await this.deleteKeys([normalizedKey]);
1778
2196
  await this.publishInvalidation({
@@ -1784,6 +2202,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1784
2202
  }
1785
2203
  async clear() {
1786
2204
  await this.awaitStartup("clear");
2205
+ this.beginClearEpoch();
1787
2206
  await Promise.all(this.layers.map((layer) => layer.clear()));
1788
2207
  await this.tagIndex.clear();
1789
2208
  this.ttlResolver.clearProfiles();
@@ -1800,7 +2219,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1800
2219
  return;
1801
2220
  }
1802
2221
  await this.awaitStartup("mdelete");
1803
- const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
2222
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
1804
2223
  const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1805
2224
  await this.deleteKeys(cacheKeys);
1806
2225
  await this.publishInvalidation({
@@ -1817,7 +2236,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1817
2236
  }
1818
2237
  const normalizedEntries = entries.map((entry) => ({
1819
2238
  ...entry,
1820
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2239
+ key: this.qualifyKey(validateCacheKey(entry.key))
1821
2240
  }));
1822
2241
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1823
2242
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1826,7 +2245,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1826
2245
  const pendingReads = /* @__PURE__ */ new Map();
1827
2246
  return Promise.all(
1828
2247
  normalizedEntries.map((entry) => {
1829
- const optionsSignature = this.serializeOptions(entry.options);
2248
+ const optionsSignature = serializeOptions(entry.options);
1830
2249
  const existing = pendingReads.get(entry.key);
1831
2250
  if (!existing) {
1832
2251
  const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
@@ -1895,7 +2314,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1895
2314
  this.assertActive("mset");
1896
2315
  const normalizedEntries = entries.map((entry) => ({
1897
2316
  ...entry,
1898
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2317
+ key: this.qualifyKey(validateCacheKey(entry.key))
1899
2318
  }));
1900
2319
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1901
2320
  await this.awaitStartup("mset");
@@ -1938,7 +2357,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1938
2357
  */
1939
2358
  wrap(prefix, fetcher, options = {}) {
1940
2359
  return (...args) => {
1941
- const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
2360
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
1942
2361
  const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
1943
2362
  return this.get(key, () => fetcher(...args), options);
1944
2363
  };
@@ -1948,11 +2367,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1948
2367
  * `prefix:`. Useful for multi-tenant or module-level isolation.
1949
2368
  */
1950
2369
  namespace(prefix) {
2370
+ validateNamespaceKey(prefix);
1951
2371
  return new CacheNamespace(this, prefix);
1952
2372
  }
1953
2373
  async invalidateByTag(tag) {
2374
+ validateTag(tag);
1954
2375
  await this.awaitStartup("invalidateByTag");
1955
- const keys = await this.tagIndex.keysForTag(tag);
2376
+ const keys = await this.collectKeysForTag(tag);
1956
2377
  await this.deleteKeys(keys);
1957
2378
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1958
2379
  }
@@ -1960,23 +2381,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1960
2381
  if (tags.length === 0) {
1961
2382
  return;
1962
2383
  }
2384
+ validateTags(tags);
1963
2385
  await this.awaitStartup("invalidateByTags");
1964
- const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
2386
+ const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
1965
2387
  const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2388
+ this.assertWithinInvalidationKeyLimit(keys.length);
1966
2389
  await this.deleteKeys(keys);
1967
2390
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1968
2391
  }
1969
2392
  async invalidateByPattern(pattern) {
1970
- this.validatePattern(pattern);
2393
+ validatePattern(pattern);
1971
2394
  await this.awaitStartup("invalidateByPattern");
1972
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
2395
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2396
+ this.qualifyPattern(pattern),
2397
+ this.invalidationMaxKeys()
2398
+ );
1973
2399
  await this.deleteKeys(keys);
1974
2400
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1975
2401
  }
1976
2402
  async invalidateByPrefix(prefix) {
1977
2403
  await this.awaitStartup("invalidateByPrefix");
1978
- const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1979
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
2404
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2405
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
1980
2406
  await this.deleteKeys(keys);
1981
2407
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1982
2408
  }
@@ -2046,7 +2472,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2046
2472
  * Returns `null` if the key does not exist in any layer.
2047
2473
  */
2048
2474
  async inspect(key) {
2049
- const userKey = this.validateCacheKey(key);
2475
+ const userKey = validateCacheKey(key);
2050
2476
  const normalizedKey = this.qualifyKey(userKey);
2051
2477
  await this.awaitStartup("inspect");
2052
2478
  const foundInLayers = [];
@@ -2083,50 +2509,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
2083
2509
  }
2084
2510
  async exportState() {
2085
2511
  await this.awaitStartup("exportState");
2086
- const exported = /* @__PURE__ */ new Map();
2087
- for (const layer of this.layers) {
2088
- if (!layer.keys) {
2089
- continue;
2090
- }
2091
- const keys = await layer.keys();
2092
- for (const key of keys) {
2093
- const exportedKey = this.stripQualifiedKey(key);
2094
- if (exported.has(exportedKey)) {
2095
- continue;
2096
- }
2097
- const stored = await this.readLayerEntry(layer, key);
2098
- if (stored === null) {
2099
- continue;
2100
- }
2101
- exported.set(exportedKey, {
2102
- key: exportedKey,
2103
- value: stored,
2104
- ttl: remainingStoredTtlSeconds(stored)
2105
- });
2106
- }
2107
- }
2108
- return [...exported.values()];
2512
+ const entries = [];
2513
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2514
+ entries.push(entry);
2515
+ });
2516
+ return entries;
2109
2517
  }
2110
2518
  async importState(entries) {
2111
2519
  await this.awaitStartup("importState");
2112
- await Promise.all(
2113
- entries.map(async (entry) => {
2114
- const qualifiedKey = this.qualifyKey(entry.key);
2115
- await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
2116
- await this.tagIndex.touch(qualifiedKey);
2117
- })
2118
- );
2520
+ const normalizedEntries = entries.map((entry) => ({
2521
+ key: this.qualifyKey(validateCacheKey(entry.key)),
2522
+ value: entry.value,
2523
+ ttl: entry.ttl
2524
+ }));
2525
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2526
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2527
+ await Promise.all(
2528
+ batch.map(async (entry) => {
2529
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2530
+ await this.tagIndex.touch(entry.key);
2531
+ })
2532
+ );
2533
+ }
2119
2534
  }
2120
2535
  async persistToFile(filePath) {
2121
2536
  this.assertActive("persistToFile");
2122
- const snapshot = await this.exportState();
2123
2537
  const { promises: fs } = await import("fs");
2124
- await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
2538
+ const path = await import("path");
2539
+ const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2540
+ const tempPath = path.join(
2541
+ path.dirname(targetPath),
2542
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2543
+ );
2544
+ let handle;
2545
+ try {
2546
+ handle = await fs.open(tempPath, "wx");
2547
+ const openedHandle = handle;
2548
+ await openedHandle.writeFile("[", "utf8");
2549
+ let wroteAny = false;
2550
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2551
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2552
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2553
+ wroteAny = true;
2554
+ });
2555
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2556
+ await openedHandle.close();
2557
+ handle = void 0;
2558
+ await fs.rename(tempPath, targetPath);
2559
+ } catch (error) {
2560
+ await handle?.close().catch(() => void 0);
2561
+ await fs.unlink(tempPath).catch(() => void 0);
2562
+ throw error;
2563
+ }
2125
2564
  }
2126
2565
  async restoreFromFile(filePath) {
2127
2566
  this.assertActive("restoreFromFile");
2128
- const { promises: fs } = await import("fs");
2129
- const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
2567
+ const { promises: fs, constants } = await import("fs");
2568
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2569
+ const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2570
+ const snapshotMaxBytes = this.snapshotMaxBytes();
2571
+ let raw;
2572
+ try {
2573
+ if (snapshotMaxBytes !== false) {
2574
+ const stat = await handle.stat();
2575
+ if (stat.size > snapshotMaxBytes) {
2576
+ throw new Error(
2577
+ `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2578
+ );
2579
+ }
2580
+ }
2581
+ raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2582
+ } finally {
2583
+ await handle.close();
2584
+ }
2130
2585
  let parsed;
2131
2586
  try {
2132
2587
  parsed = JSON.parse(raw);
@@ -2170,14 +2625,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
2170
2625
  await this.handleInvalidationMessage(message);
2171
2626
  });
2172
2627
  }
2173
- async fetchWithGuards(key, fetcher, options) {
2628
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2174
2629
  const fetchTask = async () => {
2175
2630
  const secondHit = await this.readFromLayers(key, options, "fresh-only");
2176
2631
  if (secondHit.found) {
2177
2632
  this.metricsCollector.increment("hits");
2178
2633
  return secondHit.value;
2179
2634
  }
2180
- return this.fetchAndPopulate(key, fetcher, options);
2635
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2181
2636
  };
2182
2637
  const singleFlightTask = async () => {
2183
2638
  if (!this.options.singleFlightCoordinator) {
@@ -2187,7 +2642,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2187
2642
  key,
2188
2643
  this.resolveSingleFlightOptions(),
2189
2644
  fetchTask,
2190
- () => this.waitForFreshValue(key, fetcher, options)
2645
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
2191
2646
  );
2192
2647
  };
2193
2648
  if (this.options.stampedePrevention === false) {
@@ -2195,7 +2650,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2195
2650
  }
2196
2651
  return this.stampedeGuard.execute(key, singleFlightTask);
2197
2652
  }
2198
- async waitForFreshValue(key, fetcher, options) {
2653
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2199
2654
  const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
2200
2655
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
2201
2656
  const deadline = Date.now() + timeoutMs;
@@ -2209,9 +2664,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2209
2664
  }
2210
2665
  await this.sleep(pollIntervalMs);
2211
2666
  }
2212
- return this.fetchAndPopulate(key, fetcher, options);
2667
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2213
2668
  }
2214
- async fetchAndPopulate(key, fetcher, options) {
2669
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2215
2670
  this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
2216
2671
  this.metricsCollector.increment("fetches");
2217
2672
  const fetchStart = Date.now();
@@ -2232,6 +2687,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2232
2687
  if (!this.shouldNegativeCache(options)) {
2233
2688
  return null;
2234
2689
  }
2690
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2691
+ this.logger.debug?.("skip-negative-store-after-invalidation", {
2692
+ key,
2693
+ expectedClearEpoch,
2694
+ clearEpoch: this.clearEpoch,
2695
+ expectedKeyEpoch,
2696
+ keyEpoch: this.currentKeyEpoch(key)
2697
+ });
2698
+ return null;
2699
+ }
2235
2700
  await this.storeEntry(key, "empty", null, options);
2236
2701
  return null;
2237
2702
  }
@@ -2244,11 +2709,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
2244
2709
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2245
2710
  }
2246
2711
  }
2712
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2713
+ this.logger.debug?.("skip-store-after-invalidation", {
2714
+ key,
2715
+ expectedClearEpoch,
2716
+ clearEpoch: this.clearEpoch,
2717
+ expectedKeyEpoch,
2718
+ keyEpoch: this.currentKeyEpoch(key)
2719
+ });
2720
+ return fetched;
2721
+ }
2247
2722
  await this.storeEntry(key, "value", fetched, options);
2248
2723
  return fetched;
2249
2724
  }
2250
2725
  async storeEntry(key, kind, value, options) {
2726
+ const clearEpoch = this.clearEpoch;
2727
+ const keyEpoch = this.currentKeyEpoch(key);
2251
2728
  await this.writeAcrossLayers(key, kind, value, options);
2729
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2730
+ return;
2731
+ }
2252
2732
  if (options?.tags) {
2253
2733
  await this.tagIndex.track(key, options.tags);
2254
2734
  } else {
@@ -2263,6 +2743,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2263
2743
  }
2264
2744
  async writeBatch(entries) {
2265
2745
  const now = Date.now();
2746
+ const clearEpoch = this.clearEpoch;
2747
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2266
2748
  const entriesByLayer = /* @__PURE__ */ new Map();
2267
2749
  const immediateOperations = [];
2268
2750
  const deferredOperations = [];
@@ -2279,12 +2761,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
2279
2761
  }
2280
2762
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2281
2763
  const operation = async () => {
2764
+ if (clearEpoch !== this.clearEpoch) {
2765
+ return;
2766
+ }
2767
+ const activeEntries = layerEntries.filter(
2768
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2769
+ );
2770
+ if (activeEntries.length === 0) {
2771
+ return;
2772
+ }
2282
2773
  try {
2283
2774
  if (layer.setMany) {
2284
- await layer.setMany(layerEntries);
2775
+ await layer.setMany(activeEntries);
2285
2776
  return;
2286
2777
  }
2287
- await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2778
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2288
2779
  } catch (error) {
2289
2780
  await this.handleLayerFailure(layer, "write", error);
2290
2781
  }
@@ -2297,7 +2788,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2297
2788
  }
2298
2789
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2299
2790
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2791
+ if (clearEpoch !== this.clearEpoch) {
2792
+ return;
2793
+ }
2300
2794
  for (const entry of entries) {
2795
+ if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2796
+ continue;
2797
+ }
2301
2798
  if (entry.options?.tags) {
2302
2799
  await this.tagIndex.track(entry.key, entry.options.tags);
2303
2800
  } else {
@@ -2399,10 +2896,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2399
2896
  }
2400
2897
  async writeAcrossLayers(key, kind, value, options) {
2401
2898
  const now = Date.now();
2899
+ const clearEpoch = this.clearEpoch;
2900
+ const keyEpoch = this.currentKeyEpoch(key);
2402
2901
  const immediateOperations = [];
2403
2902
  const deferredOperations = [];
2404
2903
  for (const layer of this.layers) {
2405
2904
  const operation = async () => {
2905
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2906
+ return;
2907
+ }
2406
2908
  if (this.shouldSkipLayer(layer)) {
2407
2909
  return;
2408
2910
  }
@@ -2466,10 +2968,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
2466
2968
  if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
2467
2969
  return;
2468
2970
  }
2971
+ const clearEpoch = this.clearEpoch;
2972
+ const keyEpoch = this.currentKeyEpoch(key);
2469
2973
  const refresh = (async () => {
2470
2974
  this.metricsCollector.increment("refreshes");
2471
2975
  try {
2472
- await this.runBackgroundRefresh(key, fetcher, options);
2976
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2473
2977
  } catch (error) {
2474
2978
  this.metricsCollector.increment("refreshErrors");
2475
2979
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
@@ -2479,14 +2983,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2479
2983
  })();
2480
2984
  this.backgroundRefreshes.set(key, refresh);
2481
2985
  }
2482
- async runBackgroundRefresh(key, fetcher, options) {
2986
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2483
2987
  const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2484
2988
  await this.fetchWithGuards(
2485
2989
  key,
2486
2990
  () => this.withTimeout(fetcher(), timeoutMs, () => {
2487
2991
  return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2488
2992
  }),
2489
- options
2993
+ options,
2994
+ expectedClearEpoch,
2995
+ expectedKeyEpoch
2490
2996
  );
2491
2997
  }
2492
2998
  resolveSingleFlightOptions() {
@@ -2501,6 +3007,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2501
3007
  if (keys.length === 0) {
2502
3008
  return;
2503
3009
  }
3010
+ this.bumpKeyEpochs(keys);
2504
3011
  await this.deleteKeysFromLayers(this.layers, keys);
2505
3012
  for (const key of keys) {
2506
3013
  await this.tagIndex.remove(key);
@@ -2523,21 +3030,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
2523
3030
  return;
2524
3031
  }
2525
3032
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2526
- if (localLayers.length === 0) {
2527
- return;
2528
- }
2529
3033
  if (message.scope === "clear") {
3034
+ this.beginClearEpoch();
2530
3035
  await Promise.all(localLayers.map((layer) => layer.clear()));
2531
3036
  await this.tagIndex.clear();
2532
3037
  this.ttlResolver.clearProfiles();
3038
+ this.circuitBreakerManager.clear();
2533
3039
  return;
2534
3040
  }
2535
3041
  const keys = message.keys ?? [];
3042
+ this.bumpKeyEpochs(keys);
2536
3043
  await this.deleteKeysFromLayers(localLayers, keys);
2537
3044
  if (message.operation !== "write") {
2538
3045
  for (const key of keys) {
2539
3046
  await this.tagIndex.remove(key);
2540
3047
  this.ttlResolver.deleteProfile(key);
3048
+ this.circuitBreakerManager.delete(key);
2541
3049
  }
2542
3050
  }
2543
3051
  }
@@ -2643,6 +3151,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
2643
3151
  shouldWriteBehind(layer) {
2644
3152
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2645
3153
  }
3154
+ beginClearEpoch() {
3155
+ this.clearEpoch += 1;
3156
+ this.keyEpochs.clear();
3157
+ this.writeBehindQueue.length = 0;
3158
+ }
3159
+ currentKeyEpoch(key) {
3160
+ return this.keyEpochs.get(key) ?? 0;
3161
+ }
3162
+ bumpKeyEpochs(keys) {
3163
+ for (const key of keys) {
3164
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
3165
+ }
3166
+ }
3167
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
3168
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
3169
+ return true;
3170
+ }
3171
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
3172
+ return true;
3173
+ }
3174
+ return false;
3175
+ }
2646
3176
  async enqueueWriteBehind(operation) {
2647
3177
  this.writeBehindQueue.push(operation);
2648
3178
  const batchSize = this.options.writeBehind?.batchSize ?? 100;
@@ -2769,118 +3299,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
2769
3299
  if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
2770
3300
  throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
2771
3301
  }
2772
- this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
2773
- this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
2774
- this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
2775
- this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
2776
- this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
2777
- this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2778
- this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2779
- this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2780
- this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2781
- this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2782
- this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2783
- this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2784
- this.validateCircuitBreakerOptions(this.options.circuitBreaker);
3302
+ validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
3303
+ validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
3304
+ validateLayerNumberOption("staleIfError", this.options.staleIfError);
3305
+ validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
3306
+ validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
3307
+ validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
3308
+ validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
3309
+ validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
3310
+ validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
3311
+ validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
3312
+ if (this.options.snapshotMaxBytes !== false) {
3313
+ validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
3314
+ }
3315
+ if (this.options.snapshotMaxEntries !== false) {
3316
+ validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
3317
+ }
3318
+ if (this.options.invalidationMaxKeys !== false) {
3319
+ validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
3320
+ }
3321
+ validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
3322
+ validateAdaptiveTtlOptions(this.options.adaptiveTtl);
3323
+ validateCircuitBreakerOptions(this.options.circuitBreaker);
2785
3324
  if (typeof this.options.generationCleanup === "object") {
2786
- this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
3325
+ validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2787
3326
  }
2788
3327
  if (this.options.generation !== void 0) {
2789
- this.validateNonNegativeNumber("generation", this.options.generation);
3328
+ validateNonNegativeNumber("generation", this.options.generation);
2790
3329
  }
2791
3330
  }
2792
3331
  validateWriteOptions(options) {
2793
3332
  if (!options) {
2794
3333
  return;
2795
3334
  }
2796
- this.validateLayerNumberOption("options.ttl", options.ttl);
2797
- this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
2798
- this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
2799
- this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
2800
- this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
2801
- this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2802
- this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2803
- this.validateAdaptiveTtlOptions(options.adaptiveTtl);
2804
- this.validateCircuitBreakerOptions(options.circuitBreaker);
2805
- this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2806
- }
2807
- validateLayerNumberOption(name, value) {
2808
- if (value === void 0) {
2809
- return;
2810
- }
2811
- if (typeof value === "number") {
2812
- this.validateNonNegativeNumber(name, value);
2813
- return;
2814
- }
2815
- for (const [layerName, layerValue] of Object.entries(value)) {
2816
- if (layerValue === void 0) {
2817
- continue;
2818
- }
2819
- this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
2820
- }
2821
- }
2822
- validatePositiveNumber(name, value) {
2823
- if (value === void 0) {
2824
- return;
2825
- }
2826
- if (!Number.isFinite(value) || value <= 0) {
2827
- throw new Error(`${name} must be a positive finite number.`);
2828
- }
2829
- }
2830
- validateRateLimitOptions(name, options) {
2831
- if (!options) {
2832
- return;
2833
- }
2834
- this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2835
- this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2836
- this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2837
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2838
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2839
- }
2840
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2841
- throw new Error(`${name}.bucketKey must not be empty.`);
2842
- }
2843
- }
2844
- validateNonNegativeNumber(name, value) {
2845
- if (!Number.isFinite(value) || value < 0) {
2846
- throw new Error(`${name} must be a non-negative finite number.`);
2847
- }
2848
- }
2849
- validateCacheKey(key) {
2850
- if (key.length === 0) {
2851
- throw new Error("Cache key must not be empty.");
2852
- }
2853
- if (key.length > MAX_CACHE_KEY_LENGTH) {
2854
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
2855
- }
2856
- if (/[\u0000-\u001F\u007F]/.test(key)) {
2857
- throw new Error("Cache key contains unsupported control characters.");
2858
- }
2859
- if (/[\uD800-\uDFFF]/.test(key)) {
2860
- throw new Error("Cache key contains unsupported surrogate code points.");
2861
- }
2862
- return key;
2863
- }
2864
- validatePattern(pattern) {
2865
- if (pattern.length === 0) {
2866
- throw new Error("Pattern must not be empty.");
2867
- }
2868
- if (pattern.length > MAX_PATTERN_LENGTH) {
2869
- throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2870
- }
2871
- if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2872
- throw new Error("Pattern contains unsupported control characters.");
2873
- }
2874
- }
2875
- validateTtlPolicy(name, policy) {
2876
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2877
- return;
2878
- }
2879
- if ("alignTo" in policy) {
2880
- this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2881
- return;
2882
- }
2883
- throw new Error(`${name} is invalid.`);
3335
+ validateLayerNumberOption("options.ttl", options.ttl);
3336
+ validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
3337
+ validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
3338
+ validateLayerNumberOption("options.staleIfError", options.staleIfError);
3339
+ validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
3340
+ validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
3341
+ validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
3342
+ validateAdaptiveTtlOptions(options.adaptiveTtl);
3343
+ validateCircuitBreakerOptions(options.circuitBreaker);
3344
+ validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3345
+ validateTags(options.tags);
2884
3346
  }
2885
3347
  assertActive(operation) {
2886
3348
  if (this.isDisconnecting) {
@@ -2892,24 +3354,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2892
3354
  await this.startup;
2893
3355
  this.assertActive(operation);
2894
3356
  }
2895
- serializeOptions(options) {
2896
- return JSON.stringify(this.normalizeForSerialization(options) ?? null);
2897
- }
2898
- validateAdaptiveTtlOptions(options) {
2899
- if (!options || options === true) {
2900
- return;
2901
- }
2902
- this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
2903
- this.validateLayerNumberOption("adaptiveTtl.step", options.step);
2904
- this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
2905
- }
2906
- validateCircuitBreakerOptions(options) {
2907
- if (!options) {
2908
- return;
2909
- }
2910
- this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
2911
- this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
2912
- }
2913
3357
  async applyFreshReadPolicies(key, hit, options, fetcher) {
2914
3358
  const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
2915
3359
  const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
@@ -2977,18 +3421,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2977
3421
  this.emit("error", { operation, ...context });
2978
3422
  }
2979
3423
  }
2980
- serializeKeyPart(value) {
2981
- if (typeof value === "string") {
2982
- return `s:${value}`;
2983
- }
2984
- if (typeof value === "number") {
2985
- return `n:${value}`;
2986
- }
2987
- if (typeof value === "boolean") {
2988
- return `b:${value}`;
2989
- }
2990
- return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
2991
- }
2992
3424
  isCacheSnapshotEntries(value) {
2993
3425
  return Array.isArray(value) && value.every((entry) => {
2994
3426
  if (!entry || typeof entry !== "object") {
@@ -3001,54 +3433,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
3001
3433
  sanitizeSnapshotValue(value) {
3002
3434
  return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
3003
3435
  }
3004
- async validateSnapshotFilePath(filePath) {
3005
- if (filePath.length === 0) {
3006
- throw new Error("filePath must not be empty.");
3007
- }
3008
- if (filePath.includes("\0")) {
3009
- throw new Error("filePath must not contain null bytes.");
3436
+ snapshotMaxBytes() {
3437
+ return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3438
+ }
3439
+ snapshotMaxEntries() {
3440
+ return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
3441
+ }
3442
+ invalidationMaxKeys() {
3443
+ return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3444
+ }
3445
+ async collectKeysForTag(tag) {
3446
+ const keys = /* @__PURE__ */ new Set();
3447
+ if (this.tagIndex.forEachKeyForTag) {
3448
+ await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3449
+ keys.add(key);
3450
+ this.assertWithinInvalidationKeyLimit(keys.size);
3451
+ });
3452
+ return [...keys];
3010
3453
  }
3011
- const path = await import("path");
3012
- const resolved = path.resolve(filePath);
3013
- const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
3014
- if (baseDir !== false) {
3015
- const relative = path.relative(baseDir, resolved);
3016
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
3017
- throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
3018
- }
3454
+ for (const key of await this.tagIndex.keysForTag(tag)) {
3455
+ keys.add(key);
3456
+ this.assertWithinInvalidationKeyLimit(keys.size);
3019
3457
  }
3020
- return resolved;
3458
+ return [...keys];
3021
3459
  }
3022
- normalizeForSerialization(value) {
3023
- if (Array.isArray(value)) {
3024
- return value.map((entry) => this.normalizeForSerialization(entry));
3460
+ assertWithinInvalidationKeyLimit(size) {
3461
+ const maxKeys = this.invalidationMaxKeys();
3462
+ if (maxKeys !== false && size > maxKeys) {
3463
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
3025
3464
  }
3026
- if (value && typeof value === "object") {
3027
- return Object.keys(value).sort().reduce((normalized, key) => {
3028
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
3029
- return normalized;
3465
+ }
3466
+ async visitExportEntries(maxEntries, visitor) {
3467
+ const exported = /* @__PURE__ */ new Set();
3468
+ for (const layer of this.layers) {
3469
+ if (!layer.keys && !layer.forEachKey) {
3470
+ continue;
3471
+ }
3472
+ const visitKey = async (key) => {
3473
+ const exportedKey = this.stripQualifiedKey(key);
3474
+ if (exported.has(exportedKey)) {
3475
+ return;
3030
3476
  }
3031
- normalized[key] = this.normalizeForSerialization(value[key]);
3032
- return normalized;
3033
- }, {});
3477
+ const stored = await this.readLayerEntry(layer, key);
3478
+ if (stored === null) {
3479
+ return;
3480
+ }
3481
+ exported.add(exportedKey);
3482
+ if (maxEntries !== false && exported.size > maxEntries) {
3483
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3484
+ }
3485
+ await visitor({
3486
+ key: exportedKey,
3487
+ value: stored,
3488
+ ttl: remainingStoredTtlSeconds(stored)
3489
+ });
3490
+ };
3491
+ if (layer.forEachKey) {
3492
+ await layer.forEachKey(visitKey);
3493
+ continue;
3494
+ }
3495
+ const keys = await layer.keys?.();
3496
+ for (const key of keys ?? []) {
3497
+ await visitKey(key);
3498
+ }
3034
3499
  }
3035
- return value;
3036
3500
  }
3037
3501
  };
3038
- function createInstanceId() {
3039
- if (globalThis.crypto?.randomUUID) {
3040
- return globalThis.crypto.randomUUID();
3041
- }
3042
- const bytes = new Uint8Array(16);
3043
- if (globalThis.crypto?.getRandomValues) {
3044
- globalThis.crypto.getRandomValues(bytes);
3045
- } else {
3046
- for (let i = 0; i < bytes.length; i++) {
3047
- bytes[i] = Math.floor(Math.random() * 256);
3048
- }
3049
- }
3050
- return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
3051
- }
3052
3502
 
3053
3503
  // src/module.ts
3054
3504
  var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);