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.
@@ -228,22 +228,23 @@ var CacheNamespace = class _CacheNamespace {
228
228
  constructor(cache, prefix) {
229
229
  this.cache = cache;
230
230
  this.prefix = prefix;
231
+ validateNamespaceKey(prefix);
231
232
  }
232
233
  cache;
233
234
  prefix;
234
235
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
235
236
  metrics = emptyMetrics();
236
237
  async get(key, fetcher, options) {
237
- return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
238
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
238
239
  }
239
240
  async getOrSet(key, fetcher, options) {
240
- return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
241
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
241
242
  }
242
243
  /**
243
244
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
244
245
  */
245
246
  async getOrThrow(key, fetcher, options) {
246
- return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
247
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
247
248
  }
248
249
  async has(key) {
249
250
  return this.trackMetrics(() => this.cache.has(this.qualify(key)));
@@ -252,7 +253,7 @@ var CacheNamespace = class _CacheNamespace {
252
253
  return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
253
254
  }
254
255
  async set(key, value, options) {
255
- await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
256
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
256
257
  }
257
258
  async delete(key) {
258
259
  await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
@@ -268,7 +269,8 @@ var CacheNamespace = class _CacheNamespace {
268
269
  () => this.cache.mget(
269
270
  entries.map((entry) => ({
270
271
  ...entry,
271
- key: this.qualify(entry.key)
272
+ key: this.qualify(entry.key),
273
+ options: this.qualifyGetOptions(entry.options)
272
274
  }))
273
275
  )
274
276
  );
@@ -278,16 +280,22 @@ var CacheNamespace = class _CacheNamespace {
278
280
  () => this.cache.mset(
279
281
  entries.map((entry) => ({
280
282
  ...entry,
281
- key: this.qualify(entry.key)
283
+ key: this.qualify(entry.key),
284
+ options: this.qualifyWriteOptions(entry.options)
282
285
  }))
283
286
  )
284
287
  );
285
288
  }
286
289
  async invalidateByTag(tag) {
287
- await this.trackMetrics(() => this.cache.invalidateByTag(tag));
290
+ await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
288
291
  }
289
292
  async invalidateByTags(tags, mode = "any") {
290
- await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
293
+ await this.trackMetrics(
294
+ () => this.cache.invalidateByTags(
295
+ tags.map((tag) => this.qualifyTag(tag)),
296
+ mode
297
+ )
298
+ );
291
299
  }
292
300
  async invalidateByPattern(pattern) {
293
301
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
@@ -299,16 +307,24 @@ var CacheNamespace = class _CacheNamespace {
299
307
  * Returns detailed metadata about a single cache key within this namespace.
300
308
  */
301
309
  async inspect(key) {
302
- return this.cache.inspect(this.qualify(key));
310
+ const result = await this.cache.inspect(this.qualify(key));
311
+ if (result === null) {
312
+ return null;
313
+ }
314
+ return {
315
+ ...result,
316
+ tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
317
+ };
303
318
  }
304
319
  wrap(keyPrefix, fetcher, options) {
305
- return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
320
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
306
321
  }
307
322
  warm(entries, options) {
308
323
  return this.cache.warm(
309
324
  entries.map((entry) => ({
310
325
  ...entry,
311
- key: this.qualify(entry.key)
326
+ key: this.qualify(entry.key),
327
+ options: this.qualifyGetOptions(entry.options)
312
328
  })),
313
329
  options
314
330
  );
@@ -344,6 +360,24 @@ var CacheNamespace = class _CacheNamespace {
344
360
  qualify(key) {
345
361
  return `${this.prefix}:${key}`;
346
362
  }
363
+ qualifyTag(tag) {
364
+ return `${this.prefix}:${tag}`;
365
+ }
366
+ qualifyGetOptions(options) {
367
+ return this.qualifyWriteOptions(options);
368
+ }
369
+ qualifyWrapOptions(options) {
370
+ return this.qualifyWriteOptions(options);
371
+ }
372
+ qualifyWriteOptions(options) {
373
+ if (!options?.tags || options.tags.length === 0) {
374
+ return options;
375
+ }
376
+ return {
377
+ ...options,
378
+ tags: options.tags.map((tag) => this.qualifyTag(tag))
379
+ };
380
+ }
347
381
  async trackMetrics(operation) {
348
382
  return this.getMetricsMutex().runExclusive(async () => {
349
383
  const before = this.cache.getMetrics();
@@ -478,6 +512,9 @@ function validateNamespaceKey(key) {
478
512
  if (/[\u0000-\u001F\u007F]/.test(key)) {
479
513
  throw new Error("Namespace prefix contains unsupported control characters.");
480
514
  }
515
+ if (/[\uD800-\uDFFF]/.test(key)) {
516
+ throw new Error("Namespace prefix contains unsupported surrogate code points.");
517
+ }
481
518
  }
482
519
 
483
520
  // ../../src/invalidation/PatternMatcher.ts
@@ -534,21 +571,41 @@ var CacheKeyDiscovery = class {
534
571
  this.options = options;
535
572
  }
536
573
  options;
537
- async collectKeysWithPrefix(prefix) {
574
+ async collectKeysWithPrefix(prefix, maxMatches = false) {
538
575
  const { tagIndex } = this.options;
539
- const matches = new Set(
540
- tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
541
- );
576
+ const matches = /* @__PURE__ */ new Set();
577
+ if (tagIndex.forEachKeyForPrefix) {
578
+ await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
579
+ matches.add(key);
580
+ this.assertWithinMatchLimit(matches, maxMatches);
581
+ });
582
+ } else {
583
+ const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
584
+ for (const key of initialMatches) {
585
+ matches.add(key);
586
+ this.assertWithinMatchLimit(matches, maxMatches);
587
+ }
588
+ }
542
589
  await Promise.all(
543
590
  this.options.layers.map(async (layer) => {
544
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
591
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
545
592
  return;
546
593
  }
547
594
  try {
548
- const keys = await layer.keys();
549
- for (const key of keys) {
595
+ if (layer.forEachKey) {
596
+ await layer.forEachKey(async (key) => {
597
+ if (key.startsWith(prefix)) {
598
+ matches.add(key);
599
+ this.assertWithinMatchLimit(matches, maxMatches);
600
+ }
601
+ });
602
+ return;
603
+ }
604
+ const keys = await layer.keys?.();
605
+ for (const key of keys ?? []) {
550
606
  if (key.startsWith(prefix)) {
551
607
  matches.add(key);
608
+ this.assertWithinMatchLimit(matches, maxMatches);
552
609
  }
553
610
  }
554
611
  } catch (error) {
@@ -558,18 +615,39 @@ var CacheKeyDiscovery = class {
558
615
  );
559
616
  return [...matches];
560
617
  }
561
- async collectKeysMatchingPattern(pattern) {
562
- const matches = new Set(await this.options.tagIndex.matchPattern(pattern));
618
+ async collectKeysMatchingPattern(pattern, maxMatches = false) {
619
+ const matches = /* @__PURE__ */ new Set();
620
+ if (this.options.tagIndex.forEachKeyMatchingPattern) {
621
+ await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
622
+ matches.add(key);
623
+ this.assertWithinMatchLimit(matches, maxMatches);
624
+ });
625
+ } else {
626
+ for (const key of await this.options.tagIndex.matchPattern(pattern)) {
627
+ matches.add(key);
628
+ this.assertWithinMatchLimit(matches, maxMatches);
629
+ }
630
+ }
563
631
  await Promise.all(
564
632
  this.options.layers.map(async (layer) => {
565
- if (!layer.keys || this.options.shouldSkipLayer(layer)) {
633
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
566
634
  return;
567
635
  }
568
636
  try {
569
- const keys = await layer.keys();
570
- for (const key of keys) {
637
+ if (layer.forEachKey) {
638
+ await layer.forEachKey(async (key) => {
639
+ if (PatternMatcher.matches(pattern, key)) {
640
+ matches.add(key);
641
+ this.assertWithinMatchLimit(matches, maxMatches);
642
+ }
643
+ });
644
+ return;
645
+ }
646
+ const keys = await layer.keys?.();
647
+ for (const key of keys ?? []) {
571
648
  if (PatternMatcher.matches(pattern, key)) {
572
649
  matches.add(key);
650
+ this.assertWithinMatchLimit(matches, maxMatches);
573
651
  }
574
652
  }
575
653
  } catch (error) {
@@ -579,8 +657,280 @@ var CacheKeyDiscovery = class {
579
657
  );
580
658
  return [...matches];
581
659
  }
660
+ assertWithinMatchLimit(matches, maxMatches) {
661
+ if (maxMatches !== false && matches.size > maxMatches) {
662
+ throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
663
+ }
664
+ }
582
665
  };
583
666
 
667
+ // ../../src/internal/CacheKeySerialization.ts
668
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
669
+ function normalizeForSerialization(value) {
670
+ if (Array.isArray(value)) {
671
+ return value.map((entry) => normalizeForSerialization(entry));
672
+ }
673
+ if (value && typeof value === "object") {
674
+ return Object.keys(value).sort().reduce((normalized, key) => {
675
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
676
+ return normalized;
677
+ }
678
+ normalized[key] = normalizeForSerialization(value[key]);
679
+ return normalized;
680
+ }, {});
681
+ }
682
+ return value;
683
+ }
684
+ function serializeKeyPart(value) {
685
+ if (typeof value === "string") {
686
+ return `s:${value}`;
687
+ }
688
+ if (typeof value === "number") {
689
+ return `n:${value}`;
690
+ }
691
+ if (typeof value === "boolean") {
692
+ return `b:${value}`;
693
+ }
694
+ return `j:${JSON.stringify(normalizeForSerialization(value))}`;
695
+ }
696
+ function serializeOptions(options) {
697
+ return JSON.stringify(normalizeForSerialization(options) ?? null);
698
+ }
699
+ function createInstanceId() {
700
+ if (globalThis.crypto?.randomUUID) {
701
+ return globalThis.crypto.randomUUID();
702
+ }
703
+ const bytes = new Uint8Array(16);
704
+ if (globalThis.crypto?.getRandomValues) {
705
+ globalThis.crypto.getRandomValues(bytes);
706
+ } else {
707
+ for (let i = 0; i < bytes.length; i += 1) {
708
+ bytes[i] = Math.floor(Math.random() * 256);
709
+ }
710
+ }
711
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
712
+ }
713
+
714
+ // ../../src/internal/CacheSnapshotFile.ts
715
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
716
+ const relative = path.relative(realBaseDir, candidatePath);
717
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
718
+ }
719
+ async function findExistingAncestor(directory, fs, path) {
720
+ let current = directory;
721
+ while (true) {
722
+ try {
723
+ await fs.lstat(current);
724
+ return current;
725
+ } catch (error) {
726
+ if (error.code !== "ENOENT") {
727
+ throw error;
728
+ }
729
+ }
730
+ const parent = path.dirname(current);
731
+ if (parent === current) {
732
+ return current;
733
+ }
734
+ current = parent;
735
+ }
736
+ }
737
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
738
+ if (filePath.length === 0) {
739
+ throw new Error("filePath must not be empty.");
740
+ }
741
+ if (filePath.includes("\0")) {
742
+ throw new Error("filePath must not contain null bytes.");
743
+ }
744
+ const { promises: fs } = await import("fs");
745
+ const path = await import("path");
746
+ const resolved = path.resolve(filePath);
747
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
748
+ if (baseDir === false) {
749
+ return resolved;
750
+ }
751
+ await fs.mkdir(baseDir, { recursive: true });
752
+ const realBaseDir = await fs.realpath(baseDir);
753
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
754
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
755
+ }
756
+ if (mode === "read") {
757
+ const realTarget = await fs.realpath(resolved);
758
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
759
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
760
+ }
761
+ return realTarget;
762
+ }
763
+ const parentDir = path.dirname(resolved);
764
+ const existingAncestor = await findExistingAncestor(parentDir, fs, path);
765
+ const realExistingAncestor = await fs.realpath(existingAncestor);
766
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
767
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
768
+ }
769
+ await fs.mkdir(parentDir, { recursive: true });
770
+ const realParentDir = await fs.realpath(parentDir);
771
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
772
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
773
+ }
774
+ const targetPath = path.join(realParentDir, path.basename(resolved));
775
+ try {
776
+ const existing = await fs.lstat(targetPath);
777
+ if (existing.isSymbolicLink()) {
778
+ throw new Error("filePath must not point to a symbolic link.");
779
+ }
780
+ } catch (error) {
781
+ if (error.code !== "ENOENT") {
782
+ throw error;
783
+ }
784
+ }
785
+ return targetPath;
786
+ }
787
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
788
+ if (byteLimit === false) {
789
+ return handle.readFile({ encoding: "utf8" });
790
+ }
791
+ const chunks = [];
792
+ let totalBytes = 0;
793
+ let position = 0;
794
+ while (true) {
795
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
796
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
797
+ if (bytesRead === 0) {
798
+ break;
799
+ }
800
+ totalBytes += bytesRead;
801
+ if (totalBytes > byteLimit) {
802
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
803
+ }
804
+ chunks.push(buffer.subarray(0, bytesRead));
805
+ position += bytesRead;
806
+ }
807
+ return Buffer.concat(chunks).toString("utf8");
808
+ }
809
+
810
+ // ../../src/internal/CacheStackValidation.ts
811
+ var MAX_CACHE_KEY_LENGTH = 1024;
812
+ var MAX_PATTERN_LENGTH = 1024;
813
+ var MAX_TAGS_PER_OPERATION = 128;
814
+ function validatePositiveNumber(name, value) {
815
+ if (value === void 0) {
816
+ return;
817
+ }
818
+ if (!Number.isFinite(value) || value <= 0) {
819
+ throw new Error(`${name} must be a positive finite number.`);
820
+ }
821
+ }
822
+ function validateNonNegativeNumber(name, value) {
823
+ if (!Number.isFinite(value) || value < 0) {
824
+ throw new Error(`${name} must be a non-negative finite number.`);
825
+ }
826
+ }
827
+ function validateLayerNumberOption(name, value) {
828
+ if (value === void 0) {
829
+ return;
830
+ }
831
+ if (typeof value === "number") {
832
+ validateNonNegativeNumber(name, value);
833
+ return;
834
+ }
835
+ for (const [layerName, layerValue] of Object.entries(value)) {
836
+ if (layerValue === void 0) {
837
+ continue;
838
+ }
839
+ validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
840
+ }
841
+ }
842
+ function validateRateLimitOptions(name, options) {
843
+ if (!options) {
844
+ return;
845
+ }
846
+ validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
847
+ validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
848
+ validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
849
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
850
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
851
+ }
852
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
853
+ throw new Error(`${name}.bucketKey must not be empty.`);
854
+ }
855
+ }
856
+ function validateCacheKey(key) {
857
+ if (key.length === 0) {
858
+ throw new Error("Cache key must not be empty.");
859
+ }
860
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
861
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
862
+ }
863
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
864
+ throw new Error("Cache key contains unsupported control characters.");
865
+ }
866
+ if (/[\uD800-\uDFFF]/.test(key)) {
867
+ throw new Error("Cache key contains unsupported surrogate code points.");
868
+ }
869
+ return key;
870
+ }
871
+ function validateTag(tag) {
872
+ if (tag.length === 0) {
873
+ throw new Error("Cache tag must not be empty.");
874
+ }
875
+ if (tag.length > MAX_CACHE_KEY_LENGTH) {
876
+ throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
877
+ }
878
+ if (/[\u0000-\u001F\u007F]/.test(tag)) {
879
+ throw new Error("Cache tag contains unsupported control characters.");
880
+ }
881
+ if (/[\uD800-\uDFFF]/.test(tag)) {
882
+ throw new Error("Cache tag contains unsupported surrogate code points.");
883
+ }
884
+ return tag;
885
+ }
886
+ function validateTags(tags) {
887
+ if (!tags) {
888
+ return;
889
+ }
890
+ if (tags.length > MAX_TAGS_PER_OPERATION) {
891
+ throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
892
+ }
893
+ for (const tag of tags) {
894
+ validateTag(tag);
895
+ }
896
+ }
897
+ function validatePattern(pattern) {
898
+ if (pattern.length === 0) {
899
+ throw new Error("Pattern must not be empty.");
900
+ }
901
+ if (pattern.length > MAX_PATTERN_LENGTH) {
902
+ throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
903
+ }
904
+ if (/[\u0000-\u001F\u007F]/.test(pattern)) {
905
+ throw new Error("Pattern contains unsupported control characters.");
906
+ }
907
+ }
908
+ function validateTtlPolicy(name, policy) {
909
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
910
+ return;
911
+ }
912
+ if ("alignTo" in policy) {
913
+ validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
914
+ return;
915
+ }
916
+ throw new Error(`${name} is invalid.`);
917
+ }
918
+ function validateAdaptiveTtlOptions(options) {
919
+ if (!options || options === true) {
920
+ return;
921
+ }
922
+ validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
923
+ validateLayerNumberOption("adaptiveTtl.step", options.step);
924
+ validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
925
+ }
926
+ function validateCircuitBreakerOptions(options) {
927
+ if (!options) {
928
+ return;
929
+ }
930
+ validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
931
+ validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
932
+ }
933
+
584
934
  // ../../src/internal/CircuitBreakerManager.ts
585
935
  var CircuitBreakerManager = class {
586
936
  breakers = /* @__PURE__ */ new Map();
@@ -974,19 +1324,47 @@ function isStoredValueEnvelope(value) {
974
1324
  if (v.kind !== "value" && v.kind !== "empty") {
975
1325
  return false;
976
1326
  }
977
- if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
1327
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
978
1328
  return false;
979
1329
  }
980
- if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
1330
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
981
1331
  return false;
982
1332
  }
983
- if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
1333
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
984
1334
  return false;
985
1335
  }
986
1336
  const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
987
1337
  if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
988
1338
  return false;
989
1339
  }
1340
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1341
+ return false;
1342
+ }
1343
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1344
+ return false;
1345
+ }
1346
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1347
+ return false;
1348
+ }
1349
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1350
+ return false;
1351
+ }
1352
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1353
+ return false;
1354
+ }
1355
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1356
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1357
+ return false;
1358
+ }
1359
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1360
+ return false;
1361
+ }
1362
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1363
+ return false;
1364
+ }
1365
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1366
+ return false;
1367
+ }
990
1368
  return true;
991
1369
  }
992
1370
  function createStoredValueEnvelope(options) {
@@ -1085,6 +1463,12 @@ function normalizePositiveSeconds(value) {
1085
1463
  }
1086
1464
  return value;
1087
1465
  }
1466
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1467
+ if (value == null) {
1468
+ return true;
1469
+ }
1470
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1471
+ }
1088
1472
 
1089
1473
  // ../../src/internal/TtlResolver.ts
1090
1474
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
@@ -1239,6 +1623,11 @@ var TagIndex = class {
1239
1623
  async keysForTag(tag) {
1240
1624
  return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
1241
1625
  }
1626
+ async forEachKeyForTag(tag, visitor) {
1627
+ for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
1628
+ await visitor(key);
1629
+ }
1630
+ }
1242
1631
  async keysForPrefix(prefix) {
1243
1632
  const node = this.findNode(prefix);
1244
1633
  if (!node) {
@@ -1248,6 +1637,13 @@ var TagIndex = class {
1248
1637
  this.collectFromNode(node, prefix, matches);
1249
1638
  return matches;
1250
1639
  }
1640
+ async forEachKeyForPrefix(prefix, visitor) {
1641
+ const node = this.findNode(prefix);
1642
+ if (!node) {
1643
+ return;
1644
+ }
1645
+ await this.visitFromNode(node, prefix, visitor);
1646
+ }
1251
1647
  async tagsForKey(key) {
1252
1648
  return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
1253
1649
  }
@@ -1256,6 +1652,12 @@ var TagIndex = class {
1256
1652
  this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
1257
1653
  return [...matches];
1258
1654
  }
1655
+ async forEachKeyMatchingPattern(pattern, visitor) {
1656
+ const matches = await this.matchPattern(pattern);
1657
+ for (const key of matches) {
1658
+ await visitor(key);
1659
+ }
1660
+ }
1259
1661
  async clear() {
1260
1662
  this.tagToKeys.clear();
1261
1663
  this.keyToTags.clear();
@@ -1305,6 +1707,14 @@ var TagIndex = class {
1305
1707
  this.collectFromNode(child, `${prefix}${character}`, matches);
1306
1708
  }
1307
1709
  }
1710
+ async visitFromNode(node, prefix, visitor) {
1711
+ if (node.terminal) {
1712
+ await visitor(prefix);
1713
+ }
1714
+ for (const [character, child] of node.children) {
1715
+ await this.visitFromNode(child, `${prefix}${character}`, visitor);
1716
+ }
1717
+ }
1308
1718
  collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
1309
1719
  if (depth > MAX_PATTERN_RECURSION_DEPTH) {
1310
1720
  return;
@@ -1422,22 +1832,27 @@ var TagIndex = class {
1422
1832
 
1423
1833
  // ../../src/serialization/JsonSerializer.ts
1424
1834
  var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1835
+ var MAX_SANITIZE_NODES = 1e4;
1425
1836
  var JsonSerializer = class {
1426
1837
  serialize(value) {
1427
1838
  return JSON.stringify(value);
1428
1839
  }
1429
1840
  deserialize(payload) {
1430
1841
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1431
- return sanitizeJsonValue(JSON.parse(normalized), 0);
1842
+ return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
1432
1843
  }
1433
1844
  };
1434
1845
  var MAX_SANITIZE_DEPTH = 200;
1435
- function sanitizeJsonValue(value, depth) {
1846
+ function sanitizeJsonValue(value, depth, state) {
1847
+ state.count += 1;
1848
+ if (state.count > MAX_SANITIZE_NODES) {
1849
+ throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
1850
+ }
1436
1851
  if (depth > MAX_SANITIZE_DEPTH) {
1437
- return value;
1852
+ throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
1438
1853
  }
1439
1854
  if (Array.isArray(value)) {
1440
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
1855
+ return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
1441
1856
  }
1442
1857
  if (!isPlainObject(value)) {
1443
1858
  return value;
@@ -1447,7 +1862,7 @@ function sanitizeJsonValue(value, depth) {
1447
1862
  if (DANGEROUS_JSON_KEYS.has(key)) {
1448
1863
  continue;
1449
1864
  }
1450
- sanitized[key] = sanitizeJsonValue(entry, depth + 1);
1865
+ sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
1451
1866
  }
1452
1867
  return sanitized;
1453
1868
  }
@@ -1496,10 +1911,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1496
1911
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1497
1912
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1498
1913
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1499
- var MAX_CACHE_KEY_LENGTH = 1024;
1500
- var MAX_PATTERN_LENGTH = 1024;
1914
+ var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1915
+ var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1916
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1917
+ var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1501
1918
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1502
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1503
1919
  var DebugLogger = class {
1504
1920
  enabled;
1505
1921
  constructor(enabled) {
@@ -1586,6 +2002,7 @@ var CacheStack = class extends EventEmitter {
1586
2002
  snapshotSerializer = new JsonSerializer();
1587
2003
  backgroundRefreshes = /* @__PURE__ */ new Map();
1588
2004
  layerDegradedUntil = /* @__PURE__ */ new Map();
2005
+ keyEpochs = /* @__PURE__ */ new Map();
1589
2006
  ttlResolver;
1590
2007
  circuitBreakerManager;
1591
2008
  currentGeneration;
@@ -1593,6 +2010,7 @@ var CacheStack = class extends EventEmitter {
1593
2010
  writeBehindTimer;
1594
2011
  writeBehindFlushPromise;
1595
2012
  generationCleanupPromise;
2013
+ clearEpoch = 0;
1596
2014
  isDisconnecting = false;
1597
2015
  disconnectPromise;
1598
2016
  /**
@@ -1602,7 +2020,7 @@ var CacheStack = class extends EventEmitter {
1602
2020
  * and no `fetcher` is provided.
1603
2021
  */
1604
2022
  async get(key, fetcher, options) {
1605
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2023
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1606
2024
  this.validateWriteOptions(options);
1607
2025
  await this.awaitStartup("get");
1608
2026
  return this.getPrepared(normalizedKey, fetcher, options);
@@ -1672,7 +2090,7 @@ var CacheStack = class extends EventEmitter {
1672
2090
  * Returns true if the given key exists and is not expired in any layer.
1673
2091
  */
1674
2092
  async has(key) {
1675
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2093
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1676
2094
  await this.awaitStartup("has");
1677
2095
  for (const layer of this.layers) {
1678
2096
  if (this.shouldSkipLayer(layer)) {
@@ -1705,7 +2123,7 @@ var CacheStack = class extends EventEmitter {
1705
2123
  * that has it, or null if the key is not found / has no TTL.
1706
2124
  */
1707
2125
  async ttl(key) {
1708
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2126
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1709
2127
  await this.awaitStartup("ttl");
1710
2128
  for (const layer of this.layers) {
1711
2129
  if (this.shouldSkipLayer(layer)) {
@@ -1727,7 +2145,7 @@ var CacheStack = class extends EventEmitter {
1727
2145
  * Stores a value in all cache layers. Overwrites any existing value.
1728
2146
  */
1729
2147
  async set(key, value, options) {
1730
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2148
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1731
2149
  this.validateWriteOptions(options);
1732
2150
  await this.awaitStartup("set");
1733
2151
  await this.storeEntry(normalizedKey, "value", value, options);
@@ -1736,7 +2154,7 @@ var CacheStack = class extends EventEmitter {
1736
2154
  * Deletes the key from all layers and publishes an invalidation message.
1737
2155
  */
1738
2156
  async delete(key) {
1739
- const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
2157
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1740
2158
  await this.awaitStartup("delete");
1741
2159
  await this.deleteKeys([normalizedKey]);
1742
2160
  await this.publishInvalidation({
@@ -1748,6 +2166,7 @@ var CacheStack = class extends EventEmitter {
1748
2166
  }
1749
2167
  async clear() {
1750
2168
  await this.awaitStartup("clear");
2169
+ this.beginClearEpoch();
1751
2170
  await Promise.all(this.layers.map((layer) => layer.clear()));
1752
2171
  await this.tagIndex.clear();
1753
2172
  this.ttlResolver.clearProfiles();
@@ -1764,7 +2183,7 @@ var CacheStack = class extends EventEmitter {
1764
2183
  return;
1765
2184
  }
1766
2185
  await this.awaitStartup("mdelete");
1767
- const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
2186
+ const normalizedKeys = keys.map((k) => validateCacheKey(k));
1768
2187
  const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1769
2188
  await this.deleteKeys(cacheKeys);
1770
2189
  await this.publishInvalidation({
@@ -1781,7 +2200,7 @@ var CacheStack = class extends EventEmitter {
1781
2200
  }
1782
2201
  const normalizedEntries = entries.map((entry) => ({
1783
2202
  ...entry,
1784
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2203
+ key: this.qualifyKey(validateCacheKey(entry.key))
1785
2204
  }));
1786
2205
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1787
2206
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1790,7 +2209,7 @@ var CacheStack = class extends EventEmitter {
1790
2209
  const pendingReads = /* @__PURE__ */ new Map();
1791
2210
  return Promise.all(
1792
2211
  normalizedEntries.map((entry) => {
1793
- const optionsSignature = this.serializeOptions(entry.options);
2212
+ const optionsSignature = serializeOptions(entry.options);
1794
2213
  const existing = pendingReads.get(entry.key);
1795
2214
  if (!existing) {
1796
2215
  const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
@@ -1859,7 +2278,7 @@ var CacheStack = class extends EventEmitter {
1859
2278
  this.assertActive("mset");
1860
2279
  const normalizedEntries = entries.map((entry) => ({
1861
2280
  ...entry,
1862
- key: this.qualifyKey(this.validateCacheKey(entry.key))
2281
+ key: this.qualifyKey(validateCacheKey(entry.key))
1863
2282
  }));
1864
2283
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1865
2284
  await this.awaitStartup("mset");
@@ -1902,7 +2321,7 @@ var CacheStack = class extends EventEmitter {
1902
2321
  */
1903
2322
  wrap(prefix, fetcher, options = {}) {
1904
2323
  return (...args) => {
1905
- const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
2324
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
1906
2325
  const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
1907
2326
  return this.get(key, () => fetcher(...args), options);
1908
2327
  };
@@ -1912,11 +2331,13 @@ var CacheStack = class extends EventEmitter {
1912
2331
  * `prefix:`. Useful for multi-tenant or module-level isolation.
1913
2332
  */
1914
2333
  namespace(prefix) {
2334
+ validateNamespaceKey(prefix);
1915
2335
  return new CacheNamespace(this, prefix);
1916
2336
  }
1917
2337
  async invalidateByTag(tag) {
2338
+ validateTag(tag);
1918
2339
  await this.awaitStartup("invalidateByTag");
1919
- const keys = await this.tagIndex.keysForTag(tag);
2340
+ const keys = await this.collectKeysForTag(tag);
1920
2341
  await this.deleteKeys(keys);
1921
2342
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1922
2343
  }
@@ -1924,23 +2345,28 @@ var CacheStack = class extends EventEmitter {
1924
2345
  if (tags.length === 0) {
1925
2346
  return;
1926
2347
  }
2348
+ validateTags(tags);
1927
2349
  await this.awaitStartup("invalidateByTags");
1928
- const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
2350
+ const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
1929
2351
  const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2352
+ this.assertWithinInvalidationKeyLimit(keys.length);
1930
2353
  await this.deleteKeys(keys);
1931
2354
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1932
2355
  }
1933
2356
  async invalidateByPattern(pattern) {
1934
- this.validatePattern(pattern);
2357
+ validatePattern(pattern);
1935
2358
  await this.awaitStartup("invalidateByPattern");
1936
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(this.qualifyPattern(pattern));
2359
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2360
+ this.qualifyPattern(pattern),
2361
+ this.invalidationMaxKeys()
2362
+ );
1937
2363
  await this.deleteKeys(keys);
1938
2364
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1939
2365
  }
1940
2366
  async invalidateByPrefix(prefix) {
1941
2367
  await this.awaitStartup("invalidateByPrefix");
1942
- const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1943
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
2368
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2369
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
1944
2370
  await this.deleteKeys(keys);
1945
2371
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1946
2372
  }
@@ -2010,7 +2436,7 @@ var CacheStack = class extends EventEmitter {
2010
2436
  * Returns `null` if the key does not exist in any layer.
2011
2437
  */
2012
2438
  async inspect(key) {
2013
- const userKey = this.validateCacheKey(key);
2439
+ const userKey = validateCacheKey(key);
2014
2440
  const normalizedKey = this.qualifyKey(userKey);
2015
2441
  await this.awaitStartup("inspect");
2016
2442
  const foundInLayers = [];
@@ -2047,50 +2473,79 @@ var CacheStack = class extends EventEmitter {
2047
2473
  }
2048
2474
  async exportState() {
2049
2475
  await this.awaitStartup("exportState");
2050
- const exported = /* @__PURE__ */ new Map();
2051
- for (const layer of this.layers) {
2052
- if (!layer.keys) {
2053
- continue;
2054
- }
2055
- const keys = await layer.keys();
2056
- for (const key of keys) {
2057
- const exportedKey = this.stripQualifiedKey(key);
2058
- if (exported.has(exportedKey)) {
2059
- continue;
2060
- }
2061
- const stored = await this.readLayerEntry(layer, key);
2062
- if (stored === null) {
2063
- continue;
2064
- }
2065
- exported.set(exportedKey, {
2066
- key: exportedKey,
2067
- value: stored,
2068
- ttl: remainingStoredTtlSeconds(stored)
2069
- });
2070
- }
2071
- }
2072
- return [...exported.values()];
2476
+ const entries = [];
2477
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2478
+ entries.push(entry);
2479
+ });
2480
+ return entries;
2073
2481
  }
2074
2482
  async importState(entries) {
2075
2483
  await this.awaitStartup("importState");
2076
- await Promise.all(
2077
- entries.map(async (entry) => {
2078
- const qualifiedKey = this.qualifyKey(entry.key);
2079
- await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
2080
- await this.tagIndex.touch(qualifiedKey);
2081
- })
2082
- );
2484
+ const normalizedEntries = entries.map((entry) => ({
2485
+ key: this.qualifyKey(validateCacheKey(entry.key)),
2486
+ value: entry.value,
2487
+ ttl: entry.ttl
2488
+ }));
2489
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2490
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2491
+ await Promise.all(
2492
+ batch.map(async (entry) => {
2493
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2494
+ await this.tagIndex.touch(entry.key);
2495
+ })
2496
+ );
2497
+ }
2083
2498
  }
2084
2499
  async persistToFile(filePath) {
2085
2500
  this.assertActive("persistToFile");
2086
- const snapshot = await this.exportState();
2087
2501
  const { promises: fs } = await import("fs");
2088
- await fs.writeFile(await this.validateSnapshotFilePath(filePath), JSON.stringify(snapshot, null, 2), "utf8");
2502
+ const path = await import("path");
2503
+ const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2504
+ const tempPath = path.join(
2505
+ path.dirname(targetPath),
2506
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2507
+ );
2508
+ let handle;
2509
+ try {
2510
+ handle = await fs.open(tempPath, "wx");
2511
+ const openedHandle = handle;
2512
+ await openedHandle.writeFile("[", "utf8");
2513
+ let wroteAny = false;
2514
+ await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2515
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2516
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2517
+ wroteAny = true;
2518
+ });
2519
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2520
+ await openedHandle.close();
2521
+ handle = void 0;
2522
+ await fs.rename(tempPath, targetPath);
2523
+ } catch (error) {
2524
+ await handle?.close().catch(() => void 0);
2525
+ await fs.unlink(tempPath).catch(() => void 0);
2526
+ throw error;
2527
+ }
2089
2528
  }
2090
2529
  async restoreFromFile(filePath) {
2091
2530
  this.assertActive("restoreFromFile");
2092
- const { promises: fs } = await import("fs");
2093
- const raw = await fs.readFile(await this.validateSnapshotFilePath(filePath), "utf8");
2531
+ const { promises: fs, constants } = await import("fs");
2532
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2533
+ const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2534
+ const snapshotMaxBytes = this.snapshotMaxBytes();
2535
+ let raw;
2536
+ try {
2537
+ if (snapshotMaxBytes !== false) {
2538
+ const stat = await handle.stat();
2539
+ if (stat.size > snapshotMaxBytes) {
2540
+ throw new Error(
2541
+ `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2542
+ );
2543
+ }
2544
+ }
2545
+ raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2546
+ } finally {
2547
+ await handle.close();
2548
+ }
2094
2549
  let parsed;
2095
2550
  try {
2096
2551
  parsed = JSON.parse(raw);
@@ -2134,14 +2589,14 @@ var CacheStack = class extends EventEmitter {
2134
2589
  await this.handleInvalidationMessage(message);
2135
2590
  });
2136
2591
  }
2137
- async fetchWithGuards(key, fetcher, options) {
2592
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2138
2593
  const fetchTask = async () => {
2139
2594
  const secondHit = await this.readFromLayers(key, options, "fresh-only");
2140
2595
  if (secondHit.found) {
2141
2596
  this.metricsCollector.increment("hits");
2142
2597
  return secondHit.value;
2143
2598
  }
2144
- return this.fetchAndPopulate(key, fetcher, options);
2599
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2145
2600
  };
2146
2601
  const singleFlightTask = async () => {
2147
2602
  if (!this.options.singleFlightCoordinator) {
@@ -2151,7 +2606,7 @@ var CacheStack = class extends EventEmitter {
2151
2606
  key,
2152
2607
  this.resolveSingleFlightOptions(),
2153
2608
  fetchTask,
2154
- () => this.waitForFreshValue(key, fetcher, options)
2609
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
2155
2610
  );
2156
2611
  };
2157
2612
  if (this.options.stampedePrevention === false) {
@@ -2159,7 +2614,7 @@ var CacheStack = class extends EventEmitter {
2159
2614
  }
2160
2615
  return this.stampedeGuard.execute(key, singleFlightTask);
2161
2616
  }
2162
- async waitForFreshValue(key, fetcher, options) {
2617
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2163
2618
  const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
2164
2619
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
2165
2620
  const deadline = Date.now() + timeoutMs;
@@ -2173,9 +2628,9 @@ var CacheStack = class extends EventEmitter {
2173
2628
  }
2174
2629
  await this.sleep(pollIntervalMs);
2175
2630
  }
2176
- return this.fetchAndPopulate(key, fetcher, options);
2631
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2177
2632
  }
2178
- async fetchAndPopulate(key, fetcher, options) {
2633
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2179
2634
  this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
2180
2635
  this.metricsCollector.increment("fetches");
2181
2636
  const fetchStart = Date.now();
@@ -2196,6 +2651,16 @@ var CacheStack = class extends EventEmitter {
2196
2651
  if (!this.shouldNegativeCache(options)) {
2197
2652
  return null;
2198
2653
  }
2654
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2655
+ this.logger.debug?.("skip-negative-store-after-invalidation", {
2656
+ key,
2657
+ expectedClearEpoch,
2658
+ clearEpoch: this.clearEpoch,
2659
+ expectedKeyEpoch,
2660
+ keyEpoch: this.currentKeyEpoch(key)
2661
+ });
2662
+ return null;
2663
+ }
2199
2664
  await this.storeEntry(key, "empty", null, options);
2200
2665
  return null;
2201
2666
  }
@@ -2208,11 +2673,26 @@ var CacheStack = class extends EventEmitter {
2208
2673
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2209
2674
  }
2210
2675
  }
2676
+ if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2677
+ this.logger.debug?.("skip-store-after-invalidation", {
2678
+ key,
2679
+ expectedClearEpoch,
2680
+ clearEpoch: this.clearEpoch,
2681
+ expectedKeyEpoch,
2682
+ keyEpoch: this.currentKeyEpoch(key)
2683
+ });
2684
+ return fetched;
2685
+ }
2211
2686
  await this.storeEntry(key, "value", fetched, options);
2212
2687
  return fetched;
2213
2688
  }
2214
2689
  async storeEntry(key, kind, value, options) {
2690
+ const clearEpoch = this.clearEpoch;
2691
+ const keyEpoch = this.currentKeyEpoch(key);
2215
2692
  await this.writeAcrossLayers(key, kind, value, options);
2693
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2694
+ return;
2695
+ }
2216
2696
  if (options?.tags) {
2217
2697
  await this.tagIndex.track(key, options.tags);
2218
2698
  } else {
@@ -2227,6 +2707,8 @@ var CacheStack = class extends EventEmitter {
2227
2707
  }
2228
2708
  async writeBatch(entries) {
2229
2709
  const now = Date.now();
2710
+ const clearEpoch = this.clearEpoch;
2711
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2230
2712
  const entriesByLayer = /* @__PURE__ */ new Map();
2231
2713
  const immediateOperations = [];
2232
2714
  const deferredOperations = [];
@@ -2243,12 +2725,21 @@ var CacheStack = class extends EventEmitter {
2243
2725
  }
2244
2726
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2245
2727
  const operation = async () => {
2728
+ if (clearEpoch !== this.clearEpoch) {
2729
+ return;
2730
+ }
2731
+ const activeEntries = layerEntries.filter(
2732
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2733
+ );
2734
+ if (activeEntries.length === 0) {
2735
+ return;
2736
+ }
2246
2737
  try {
2247
2738
  if (layer.setMany) {
2248
- await layer.setMany(layerEntries);
2739
+ await layer.setMany(activeEntries);
2249
2740
  return;
2250
2741
  }
2251
- await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2742
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2252
2743
  } catch (error) {
2253
2744
  await this.handleLayerFailure(layer, "write", error);
2254
2745
  }
@@ -2261,7 +2752,13 @@ var CacheStack = class extends EventEmitter {
2261
2752
  }
2262
2753
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2263
2754
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2755
+ if (clearEpoch !== this.clearEpoch) {
2756
+ return;
2757
+ }
2264
2758
  for (const entry of entries) {
2759
+ if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2760
+ continue;
2761
+ }
2265
2762
  if (entry.options?.tags) {
2266
2763
  await this.tagIndex.track(entry.key, entry.options.tags);
2267
2764
  } else {
@@ -2363,10 +2860,15 @@ var CacheStack = class extends EventEmitter {
2363
2860
  }
2364
2861
  async writeAcrossLayers(key, kind, value, options) {
2365
2862
  const now = Date.now();
2863
+ const clearEpoch = this.clearEpoch;
2864
+ const keyEpoch = this.currentKeyEpoch(key);
2366
2865
  const immediateOperations = [];
2367
2866
  const deferredOperations = [];
2368
2867
  for (const layer of this.layers) {
2369
2868
  const operation = async () => {
2869
+ if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2870
+ return;
2871
+ }
2370
2872
  if (this.shouldSkipLayer(layer)) {
2371
2873
  return;
2372
2874
  }
@@ -2430,10 +2932,12 @@ var CacheStack = class extends EventEmitter {
2430
2932
  if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
2431
2933
  return;
2432
2934
  }
2935
+ const clearEpoch = this.clearEpoch;
2936
+ const keyEpoch = this.currentKeyEpoch(key);
2433
2937
  const refresh = (async () => {
2434
2938
  this.metricsCollector.increment("refreshes");
2435
2939
  try {
2436
- await this.runBackgroundRefresh(key, fetcher, options);
2940
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2437
2941
  } catch (error) {
2438
2942
  this.metricsCollector.increment("refreshErrors");
2439
2943
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
@@ -2443,14 +2947,16 @@ var CacheStack = class extends EventEmitter {
2443
2947
  })();
2444
2948
  this.backgroundRefreshes.set(key, refresh);
2445
2949
  }
2446
- async runBackgroundRefresh(key, fetcher, options) {
2950
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2447
2951
  const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2448
2952
  await this.fetchWithGuards(
2449
2953
  key,
2450
2954
  () => this.withTimeout(fetcher(), timeoutMs, () => {
2451
2955
  return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2452
2956
  }),
2453
- options
2957
+ options,
2958
+ expectedClearEpoch,
2959
+ expectedKeyEpoch
2454
2960
  );
2455
2961
  }
2456
2962
  resolveSingleFlightOptions() {
@@ -2465,6 +2971,7 @@ var CacheStack = class extends EventEmitter {
2465
2971
  if (keys.length === 0) {
2466
2972
  return;
2467
2973
  }
2974
+ this.bumpKeyEpochs(keys);
2468
2975
  await this.deleteKeysFromLayers(this.layers, keys);
2469
2976
  for (const key of keys) {
2470
2977
  await this.tagIndex.remove(key);
@@ -2487,21 +2994,22 @@ var CacheStack = class extends EventEmitter {
2487
2994
  return;
2488
2995
  }
2489
2996
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2490
- if (localLayers.length === 0) {
2491
- return;
2492
- }
2493
2997
  if (message.scope === "clear") {
2998
+ this.beginClearEpoch();
2494
2999
  await Promise.all(localLayers.map((layer) => layer.clear()));
2495
3000
  await this.tagIndex.clear();
2496
3001
  this.ttlResolver.clearProfiles();
3002
+ this.circuitBreakerManager.clear();
2497
3003
  return;
2498
3004
  }
2499
3005
  const keys = message.keys ?? [];
3006
+ this.bumpKeyEpochs(keys);
2500
3007
  await this.deleteKeysFromLayers(localLayers, keys);
2501
3008
  if (message.operation !== "write") {
2502
3009
  for (const key of keys) {
2503
3010
  await this.tagIndex.remove(key);
2504
3011
  this.ttlResolver.deleteProfile(key);
3012
+ this.circuitBreakerManager.delete(key);
2505
3013
  }
2506
3014
  }
2507
3015
  }
@@ -2607,6 +3115,28 @@ var CacheStack = class extends EventEmitter {
2607
3115
  shouldWriteBehind(layer) {
2608
3116
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2609
3117
  }
3118
+ beginClearEpoch() {
3119
+ this.clearEpoch += 1;
3120
+ this.keyEpochs.clear();
3121
+ this.writeBehindQueue.length = 0;
3122
+ }
3123
+ currentKeyEpoch(key) {
3124
+ return this.keyEpochs.get(key) ?? 0;
3125
+ }
3126
+ bumpKeyEpochs(keys) {
3127
+ for (const key of keys) {
3128
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
3129
+ }
3130
+ }
3131
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
3132
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
3133
+ return true;
3134
+ }
3135
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
3136
+ return true;
3137
+ }
3138
+ return false;
3139
+ }
2610
3140
  async enqueueWriteBehind(operation) {
2611
3141
  this.writeBehindQueue.push(operation);
2612
3142
  const batchSize = this.options.writeBehind?.batchSize ?? 100;
@@ -2733,118 +3263,50 @@ var CacheStack = class extends EventEmitter {
2733
3263
  if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
2734
3264
  throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
2735
3265
  }
2736
- this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
2737
- this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
2738
- this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
2739
- this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
2740
- this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
2741
- this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
2742
- this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
2743
- this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2744
- this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2745
- this.validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
2746
- this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
2747
- this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
2748
- this.validateCircuitBreakerOptions(this.options.circuitBreaker);
3266
+ validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
3267
+ validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
3268
+ validateLayerNumberOption("staleIfError", this.options.staleIfError);
3269
+ validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
3270
+ validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
3271
+ validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
3272
+ validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
3273
+ validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
3274
+ validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
3275
+ validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
3276
+ if (this.options.snapshotMaxBytes !== false) {
3277
+ validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
3278
+ }
3279
+ if (this.options.snapshotMaxEntries !== false) {
3280
+ validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
3281
+ }
3282
+ if (this.options.invalidationMaxKeys !== false) {
3283
+ validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
3284
+ }
3285
+ validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
3286
+ validateAdaptiveTtlOptions(this.options.adaptiveTtl);
3287
+ validateCircuitBreakerOptions(this.options.circuitBreaker);
2749
3288
  if (typeof this.options.generationCleanup === "object") {
2750
- this.validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
3289
+ validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
2751
3290
  }
2752
3291
  if (this.options.generation !== void 0) {
2753
- this.validateNonNegativeNumber("generation", this.options.generation);
3292
+ validateNonNegativeNumber("generation", this.options.generation);
2754
3293
  }
2755
3294
  }
2756
3295
  validateWriteOptions(options) {
2757
3296
  if (!options) {
2758
3297
  return;
2759
3298
  }
2760
- this.validateLayerNumberOption("options.ttl", options.ttl);
2761
- this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
2762
- this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
2763
- this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
2764
- this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
2765
- this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2766
- this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
2767
- this.validateAdaptiveTtlOptions(options.adaptiveTtl);
2768
- this.validateCircuitBreakerOptions(options.circuitBreaker);
2769
- this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
2770
- }
2771
- validateLayerNumberOption(name, value) {
2772
- if (value === void 0) {
2773
- return;
2774
- }
2775
- if (typeof value === "number") {
2776
- this.validateNonNegativeNumber(name, value);
2777
- return;
2778
- }
2779
- for (const [layerName, layerValue] of Object.entries(value)) {
2780
- if (layerValue === void 0) {
2781
- continue;
2782
- }
2783
- this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
2784
- }
2785
- }
2786
- validatePositiveNumber(name, value) {
2787
- if (value === void 0) {
2788
- return;
2789
- }
2790
- if (!Number.isFinite(value) || value <= 0) {
2791
- throw new Error(`${name} must be a positive finite number.`);
2792
- }
2793
- }
2794
- validateRateLimitOptions(name, options) {
2795
- if (!options) {
2796
- return;
2797
- }
2798
- this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2799
- this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2800
- this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2801
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2802
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2803
- }
2804
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2805
- throw new Error(`${name}.bucketKey must not be empty.`);
2806
- }
2807
- }
2808
- validateNonNegativeNumber(name, value) {
2809
- if (!Number.isFinite(value) || value < 0) {
2810
- throw new Error(`${name} must be a non-negative finite number.`);
2811
- }
2812
- }
2813
- validateCacheKey(key) {
2814
- if (key.length === 0) {
2815
- throw new Error("Cache key must not be empty.");
2816
- }
2817
- if (key.length > MAX_CACHE_KEY_LENGTH) {
2818
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
2819
- }
2820
- if (/[\u0000-\u001F\u007F]/.test(key)) {
2821
- throw new Error("Cache key contains unsupported control characters.");
2822
- }
2823
- if (/[\uD800-\uDFFF]/.test(key)) {
2824
- throw new Error("Cache key contains unsupported surrogate code points.");
2825
- }
2826
- return key;
2827
- }
2828
- validatePattern(pattern) {
2829
- if (pattern.length === 0) {
2830
- throw new Error("Pattern must not be empty.");
2831
- }
2832
- if (pattern.length > MAX_PATTERN_LENGTH) {
2833
- throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
2834
- }
2835
- if (/[\u0000-\u001F\u007F]/.test(pattern)) {
2836
- throw new Error("Pattern contains unsupported control characters.");
2837
- }
2838
- }
2839
- validateTtlPolicy(name, policy) {
2840
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2841
- return;
2842
- }
2843
- if ("alignTo" in policy) {
2844
- this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2845
- return;
2846
- }
2847
- throw new Error(`${name} is invalid.`);
3299
+ validateLayerNumberOption("options.ttl", options.ttl);
3300
+ validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
3301
+ validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
3302
+ validateLayerNumberOption("options.staleIfError", options.staleIfError);
3303
+ validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
3304
+ validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
3305
+ validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
3306
+ validateAdaptiveTtlOptions(options.adaptiveTtl);
3307
+ validateCircuitBreakerOptions(options.circuitBreaker);
3308
+ validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3309
+ validateTags(options.tags);
2848
3310
  }
2849
3311
  assertActive(operation) {
2850
3312
  if (this.isDisconnecting) {
@@ -2856,24 +3318,6 @@ var CacheStack = class extends EventEmitter {
2856
3318
  await this.startup;
2857
3319
  this.assertActive(operation);
2858
3320
  }
2859
- serializeOptions(options) {
2860
- return JSON.stringify(this.normalizeForSerialization(options) ?? null);
2861
- }
2862
- validateAdaptiveTtlOptions(options) {
2863
- if (!options || options === true) {
2864
- return;
2865
- }
2866
- this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
2867
- this.validateLayerNumberOption("adaptiveTtl.step", options.step);
2868
- this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
2869
- }
2870
- validateCircuitBreakerOptions(options) {
2871
- if (!options) {
2872
- return;
2873
- }
2874
- this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
2875
- this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
2876
- }
2877
3321
  async applyFreshReadPolicies(key, hit, options, fetcher) {
2878
3322
  const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
2879
3323
  const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
@@ -2941,18 +3385,6 @@ var CacheStack = class extends EventEmitter {
2941
3385
  this.emit("error", { operation, ...context });
2942
3386
  }
2943
3387
  }
2944
- serializeKeyPart(value) {
2945
- if (typeof value === "string") {
2946
- return `s:${value}`;
2947
- }
2948
- if (typeof value === "number") {
2949
- return `n:${value}`;
2950
- }
2951
- if (typeof value === "boolean") {
2952
- return `b:${value}`;
2953
- }
2954
- return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
2955
- }
2956
3388
  isCacheSnapshotEntries(value) {
2957
3389
  return Array.isArray(value) && value.every((entry) => {
2958
3390
  if (!entry || typeof entry !== "object") {
@@ -2965,54 +3397,72 @@ var CacheStack = class extends EventEmitter {
2965
3397
  sanitizeSnapshotValue(value) {
2966
3398
  return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
2967
3399
  }
2968
- async validateSnapshotFilePath(filePath) {
2969
- if (filePath.length === 0) {
2970
- throw new Error("filePath must not be empty.");
2971
- }
2972
- if (filePath.includes("\0")) {
2973
- throw new Error("filePath must not contain null bytes.");
3400
+ snapshotMaxBytes() {
3401
+ return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3402
+ }
3403
+ snapshotMaxEntries() {
3404
+ return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
3405
+ }
3406
+ invalidationMaxKeys() {
3407
+ return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3408
+ }
3409
+ async collectKeysForTag(tag) {
3410
+ const keys = /* @__PURE__ */ new Set();
3411
+ if (this.tagIndex.forEachKeyForTag) {
3412
+ await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3413
+ keys.add(key);
3414
+ this.assertWithinInvalidationKeyLimit(keys.size);
3415
+ });
3416
+ return [...keys];
2974
3417
  }
2975
- const path = await import("path");
2976
- const resolved = path.resolve(filePath);
2977
- const baseDir = this.options.snapshotBaseDir === false ? false : path.resolve(this.options.snapshotBaseDir ?? process.cwd());
2978
- if (baseDir !== false) {
2979
- const relative = path.relative(baseDir, resolved);
2980
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
2981
- throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
2982
- }
3418
+ for (const key of await this.tagIndex.keysForTag(tag)) {
3419
+ keys.add(key);
3420
+ this.assertWithinInvalidationKeyLimit(keys.size);
2983
3421
  }
2984
- return resolved;
3422
+ return [...keys];
2985
3423
  }
2986
- normalizeForSerialization(value) {
2987
- if (Array.isArray(value)) {
2988
- return value.map((entry) => this.normalizeForSerialization(entry));
3424
+ assertWithinInvalidationKeyLimit(size) {
3425
+ const maxKeys = this.invalidationMaxKeys();
3426
+ if (maxKeys !== false && size > maxKeys) {
3427
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
2989
3428
  }
2990
- if (value && typeof value === "object") {
2991
- return Object.keys(value).sort().reduce((normalized, key) => {
2992
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
2993
- return normalized;
3429
+ }
3430
+ async visitExportEntries(maxEntries, visitor) {
3431
+ const exported = /* @__PURE__ */ new Set();
3432
+ for (const layer of this.layers) {
3433
+ if (!layer.keys && !layer.forEachKey) {
3434
+ continue;
3435
+ }
3436
+ const visitKey = async (key) => {
3437
+ const exportedKey = this.stripQualifiedKey(key);
3438
+ if (exported.has(exportedKey)) {
3439
+ return;
2994
3440
  }
2995
- normalized[key] = this.normalizeForSerialization(value[key]);
2996
- return normalized;
2997
- }, {});
3441
+ const stored = await this.readLayerEntry(layer, key);
3442
+ if (stored === null) {
3443
+ return;
3444
+ }
3445
+ exported.add(exportedKey);
3446
+ if (maxEntries !== false && exported.size > maxEntries) {
3447
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3448
+ }
3449
+ await visitor({
3450
+ key: exportedKey,
3451
+ value: stored,
3452
+ ttl: remainingStoredTtlSeconds(stored)
3453
+ });
3454
+ };
3455
+ if (layer.forEachKey) {
3456
+ await layer.forEachKey(visitKey);
3457
+ continue;
3458
+ }
3459
+ const keys = await layer.keys?.();
3460
+ for (const key of keys ?? []) {
3461
+ await visitKey(key);
3462
+ }
2998
3463
  }
2999
- return value;
3000
3464
  }
3001
3465
  };
3002
- function createInstanceId() {
3003
- if (globalThis.crypto?.randomUUID) {
3004
- return globalThis.crypto.randomUUID();
3005
- }
3006
- const bytes = new Uint8Array(16);
3007
- if (globalThis.crypto?.getRandomValues) {
3008
- globalThis.crypto.getRandomValues(bytes);
3009
- } else {
3010
- for (let i = 0; i < bytes.length; i++) {
3011
- bytes[i] = Math.floor(Math.random() * 256);
3012
- }
3013
- }
3014
- return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
3015
- }
3016
3466
 
3017
3467
  // src/module.ts
3018
3468
  var InjectCacheStack = () => Inject(CACHE_STACK);