layercache 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -556,102 +556,6 @@ function createInstanceId() {
556
556
  return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
557
557
  }
558
558
 
559
- // src/internal/CacheSnapshotFile.ts
560
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
561
- const relative = path.relative(realBaseDir, candidatePath);
562
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
563
- }
564
- async function findExistingAncestor(directory, fs2, path) {
565
- let current = directory;
566
- while (true) {
567
- try {
568
- await fs2.lstat(current);
569
- return current;
570
- } catch (error) {
571
- if (error.code !== "ENOENT") {
572
- throw error;
573
- }
574
- }
575
- const parent = path.dirname(current);
576
- if (parent === current) {
577
- return current;
578
- }
579
- current = parent;
580
- }
581
- }
582
- async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
583
- if (filePath.length === 0) {
584
- throw new Error("filePath must not be empty.");
585
- }
586
- if (filePath.includes("\0")) {
587
- throw new Error("filePath must not contain null bytes.");
588
- }
589
- const { promises: fs2 } = await import("fs");
590
- const path = await import("path");
591
- const resolved = path.resolve(filePath);
592
- const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
593
- if (baseDir === false) {
594
- return resolved;
595
- }
596
- await fs2.mkdir(baseDir, { recursive: true });
597
- const realBaseDir = await fs2.realpath(baseDir);
598
- if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
599
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
600
- }
601
- if (mode === "read") {
602
- const realTarget = await fs2.realpath(resolved);
603
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
604
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
605
- }
606
- return realTarget;
607
- }
608
- const parentDir = path.dirname(resolved);
609
- const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
610
- const realExistingAncestor = await fs2.realpath(existingAncestor);
611
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
612
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
613
- }
614
- await fs2.mkdir(parentDir, { recursive: true });
615
- const realParentDir = await fs2.realpath(parentDir);
616
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
617
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
618
- }
619
- const targetPath = path.join(realParentDir, path.basename(resolved));
620
- try {
621
- const existing = await fs2.lstat(targetPath);
622
- if (existing.isSymbolicLink()) {
623
- throw new Error("filePath must not point to a symbolic link.");
624
- }
625
- } catch (error) {
626
- if (error.code !== "ENOENT") {
627
- throw error;
628
- }
629
- }
630
- return targetPath;
631
- }
632
- async function readUtf8HandleWithLimit(handle, byteLimit) {
633
- if (byteLimit === false) {
634
- return handle.readFile({ encoding: "utf8" });
635
- }
636
- const chunks = [];
637
- let totalBytes = 0;
638
- let position = 0;
639
- while (true) {
640
- const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
641
- const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
642
- if (bytesRead === 0) {
643
- break;
644
- }
645
- totalBytes += bytesRead;
646
- if (totalBytes > byteLimit) {
647
- throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
648
- }
649
- chunks.push(buffer.subarray(0, bytesRead));
650
- position += bytesRead;
651
- }
652
- return Buffer.concat(chunks).toString("utf8");
653
- }
654
-
655
559
  // src/internal/CacheStackGeneration.ts
656
560
  var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
657
561
  function generationPrefix(generation) {
@@ -699,102 +603,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
699
603
  return batches;
700
604
  }
701
605
 
702
- // src/internal/CacheStackMaintenance.ts
703
- var CacheStackMaintenance = class {
704
- keyEpochs = /* @__PURE__ */ new Map();
705
- writeBehindQueue = [];
706
- writeBehindTimer;
707
- writeBehindFlushPromise;
708
- generationCleanupPromise;
709
- clearEpoch = 0;
710
- initializeWriteBehindTimer(writeStrategy, options, flush) {
711
- if (writeStrategy !== "write-behind") {
712
- return;
713
- }
714
- const flushIntervalMs = options?.flushIntervalMs;
715
- if (!flushIntervalMs || flushIntervalMs <= 0) {
716
- return;
717
- }
718
- this.disposeWriteBehindTimer();
719
- this.writeBehindTimer = setInterval(() => {
720
- void flush();
721
- }, flushIntervalMs);
722
- this.writeBehindTimer.unref?.();
606
+ // src/internal/CacheStackInvalidationSupport.ts
607
+ var CacheStackInvalidationSupport = class {
608
+ constructor(options) {
609
+ this.options = options;
723
610
  }
724
- disposeWriteBehindTimer() {
725
- if (!this.writeBehindTimer) {
726
- return;
611
+ options;
612
+ async collectKeysForTag(tag, maxKeys) {
613
+ const keys = /* @__PURE__ */ new Set();
614
+ if (this.options.tagIndex.forEachKeyForTag) {
615
+ await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
616
+ keys.add(key);
617
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
618
+ });
619
+ return [...keys];
727
620
  }
728
- clearInterval(this.writeBehindTimer);
729
- this.writeBehindTimer = void 0;
730
- }
731
- beginClearEpoch() {
732
- this.clearEpoch += 1;
733
- this.keyEpochs.clear();
734
- this.writeBehindQueue.length = 0;
735
- }
736
- currentClearEpoch() {
737
- return this.clearEpoch;
738
- }
739
- currentKeyEpoch(key) {
740
- return this.keyEpochs.get(key) ?? 0;
741
- }
742
- bumpKeyEpochs(keys) {
743
- for (const key of keys) {
744
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
621
+ for (const key of await this.options.tagIndex.keysForTag(tag)) {
622
+ keys.add(key);
623
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
745
624
  }
625
+ return [...keys];
746
626
  }
747
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
748
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
749
- return true;
750
- }
751
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
752
- return true;
627
+ intersectKeys(groups) {
628
+ if (groups.length === 0) {
629
+ return [];
753
630
  }
754
- return false;
631
+ const [firstGroup, ...rest] = groups;
632
+ const restSets = rest.map((group) => new Set(group));
633
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
755
634
  }
756
- async enqueueWriteBehind(operation, options, flushBatch) {
757
- this.writeBehindQueue.push(operation);
758
- const batchSize = options?.batchSize ?? 100;
759
- const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
760
- if (this.writeBehindQueue.length >= batchSize) {
761
- await this.flushWriteBehindQueue(options, flushBatch);
762
- return;
763
- }
764
- if (this.writeBehindQueue.length >= maxQueueSize) {
765
- await this.flushWriteBehindQueue(options, flushBatch);
766
- }
635
+ async deleteKeysFromLayers(layers, keys) {
636
+ await Promise.all(
637
+ layers.map(async (layer) => {
638
+ if (this.options.shouldSkipLayer(layer)) {
639
+ return;
640
+ }
641
+ if (layer.deleteMany) {
642
+ try {
643
+ await layer.deleteMany(keys);
644
+ } catch (error) {
645
+ await this.options.handleLayerFailure(layer, "delete", error);
646
+ }
647
+ return;
648
+ }
649
+ await Promise.all(
650
+ keys.map(async (key) => {
651
+ try {
652
+ await layer.delete(key);
653
+ } catch (error) {
654
+ await this.options.handleLayerFailure(layer, "delete", error);
655
+ }
656
+ })
657
+ );
658
+ })
659
+ );
767
660
  }
768
- async flushWriteBehindQueue(options, flushBatch) {
769
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
770
- await this.writeBehindFlushPromise;
771
- return;
772
- }
773
- const batchSize = options?.batchSize ?? 100;
774
- const batch = this.writeBehindQueue.splice(0, batchSize);
775
- this.writeBehindFlushPromise = flushBatch(batch);
776
- try {
777
- await this.writeBehindFlushPromise;
778
- } finally {
779
- this.writeBehindFlushPromise = void 0;
780
- }
781
- if (this.writeBehindQueue.length > 0) {
782
- await this.flushWriteBehindQueue(options, flushBatch);
661
+ assertWithinInvalidationKeyLimit(size, maxKeys) {
662
+ if (maxKeys !== false && size > maxKeys) {
663
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
783
664
  }
784
665
  }
785
- scheduleGenerationCleanup(generation, task, onError) {
786
- const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
787
- onError(generation, error);
788
- });
789
- this.generationCleanupPromise = scheduledTask.finally(() => {
790
- if (this.generationCleanupPromise === scheduledTask) {
791
- this.generationCleanupPromise = void 0;
792
- }
793
- });
794
- }
795
- async waitForGenerationCleanup() {
796
- await this.generationCleanupPromise;
797
- }
798
666
  };
799
667
 
800
668
  // src/internal/StoredValue.ts
@@ -946,50 +814,523 @@ function normalizePositiveSeconds(value) {
946
814
  if (!value || value <= 0) {
947
815
  return void 0;
948
816
  }
949
- return value;
950
- }
951
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
952
- if (value == null) {
953
- return true;
817
+ return value;
818
+ }
819
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
820
+ if (value == null) {
821
+ return true;
822
+ }
823
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
824
+ }
825
+
826
+ // src/internal/CacheStackLayerWriter.ts
827
+ var CacheStackLayerWriter = class {
828
+ constructor(options) {
829
+ this.options = options;
830
+ }
831
+ options;
832
+ async writeAcrossLayers(key, kind, value, writeOptions) {
833
+ const now = Date.now();
834
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
835
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
836
+ const immediateOperations = [];
837
+ const deferredOperations = [];
838
+ for (const layer of this.options.layers) {
839
+ const operation = async () => {
840
+ if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
841
+ return;
842
+ }
843
+ if (this.options.shouldSkipLayer(layer)) {
844
+ return;
845
+ }
846
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
847
+ try {
848
+ await layer.set(entry.key, entry.value, entry.ttl);
849
+ } catch (error) {
850
+ await this.options.handleLayerFailure(layer, "write", error);
851
+ }
852
+ };
853
+ if (this.options.shouldWriteBehind(layer)) {
854
+ deferredOperations.push(operation);
855
+ } else {
856
+ immediateOperations.push(operation);
857
+ }
858
+ }
859
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
860
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
861
+ }
862
+ async writeBatch(entries) {
863
+ const now = Date.now();
864
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
865
+ const entryEpochs = new Map(
866
+ entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
867
+ );
868
+ const entriesByLayer = /* @__PURE__ */ new Map();
869
+ const immediateOperations = [];
870
+ const deferredOperations = [];
871
+ for (const entry of entries) {
872
+ for (const layer of this.options.layers) {
873
+ if (this.options.shouldSkipLayer(layer)) {
874
+ continue;
875
+ }
876
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
877
+ const bucket = entriesByLayer.get(layer) ?? [];
878
+ bucket.push(layerEntry);
879
+ entriesByLayer.set(layer, bucket);
880
+ }
881
+ }
882
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
883
+ const operation = async () => {
884
+ if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
885
+ return;
886
+ }
887
+ const activeEntries = layerEntries.filter(
888
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
889
+ );
890
+ if (activeEntries.length === 0) {
891
+ return;
892
+ }
893
+ try {
894
+ if (layer.setMany) {
895
+ await layer.setMany(activeEntries);
896
+ return;
897
+ }
898
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
899
+ } catch (error) {
900
+ await this.options.handleLayerFailure(layer, "write", error);
901
+ }
902
+ };
903
+ if (this.options.shouldWriteBehind(layer)) {
904
+ deferredOperations.push(operation);
905
+ } else {
906
+ immediateOperations.push(operation);
907
+ }
908
+ }
909
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
910
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
911
+ return { clearEpoch, entryEpochs };
912
+ }
913
+ async executeLayerOperations(operations, context) {
914
+ if (this.options.writePolicy !== "best-effort") {
915
+ await Promise.all(operations.map((operation) => operation()));
916
+ return;
917
+ }
918
+ const results = await Promise.allSettled(operations.map((operation) => operation()));
919
+ const failures = results.filter((result) => result.status === "rejected");
920
+ if (failures.length === 0) {
921
+ return;
922
+ }
923
+ this.options.onWriteFailures(
924
+ context,
925
+ failures.map((failure) => failure.reason)
926
+ );
927
+ if (failures.length === operations.length) {
928
+ throw new AggregateError(
929
+ failures.map((failure) => failure.reason),
930
+ `${context.action} failed for every cache layer`
931
+ );
932
+ }
933
+ }
934
+ buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
935
+ const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
936
+ const staleWhileRevalidate = this.options.resolveLayerSeconds(
937
+ layer.name,
938
+ writeOptions?.staleWhileRevalidate,
939
+ this.options.globalStaleWhileRevalidate
940
+ );
941
+ const staleIfError = this.options.resolveLayerSeconds(
942
+ layer.name,
943
+ writeOptions?.staleIfError,
944
+ this.options.globalStaleIfError
945
+ );
946
+ const payload = createStoredValueEnvelope({
947
+ kind,
948
+ value,
949
+ freshTtlSeconds: freshTtl,
950
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
951
+ staleIfErrorSeconds: staleIfError,
952
+ now
953
+ });
954
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
955
+ return {
956
+ key,
957
+ value: payload,
958
+ ttl
959
+ };
960
+ }
961
+ };
962
+
963
+ // src/internal/CacheStackMaintenance.ts
964
+ var CacheStackMaintenance = class {
965
+ keyEpochs = /* @__PURE__ */ new Map();
966
+ writeBehindQueue = [];
967
+ writeBehindTimer;
968
+ writeBehindFlushPromise;
969
+ generationCleanupPromise;
970
+ clearEpoch = 0;
971
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
972
+ if (writeStrategy !== "write-behind") {
973
+ return;
974
+ }
975
+ const flushIntervalMs = options?.flushIntervalMs;
976
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
977
+ return;
978
+ }
979
+ this.disposeWriteBehindTimer();
980
+ this.writeBehindTimer = setInterval(() => {
981
+ void flush();
982
+ }, flushIntervalMs);
983
+ this.writeBehindTimer.unref?.();
984
+ }
985
+ disposeWriteBehindTimer() {
986
+ if (!this.writeBehindTimer) {
987
+ return;
988
+ }
989
+ clearInterval(this.writeBehindTimer);
990
+ this.writeBehindTimer = void 0;
991
+ }
992
+ beginClearEpoch() {
993
+ this.clearEpoch += 1;
994
+ this.keyEpochs.clear();
995
+ this.writeBehindQueue.length = 0;
996
+ }
997
+ currentClearEpoch() {
998
+ return this.clearEpoch;
999
+ }
1000
+ currentKeyEpoch(key) {
1001
+ return this.keyEpochs.get(key) ?? 0;
1002
+ }
1003
+ bumpKeyEpochs(keys) {
1004
+ for (const key of keys) {
1005
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1006
+ }
1007
+ }
1008
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1009
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
1010
+ return true;
1011
+ }
1012
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
1013
+ return true;
1014
+ }
1015
+ return false;
1016
+ }
1017
+ async enqueueWriteBehind(operation, options, flushBatch) {
1018
+ this.writeBehindQueue.push(operation);
1019
+ const batchSize = options?.batchSize ?? 100;
1020
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
1021
+ if (this.writeBehindQueue.length >= batchSize) {
1022
+ await this.flushWriteBehindQueue(options, flushBatch);
1023
+ return;
1024
+ }
1025
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1026
+ await this.flushWriteBehindQueue(options, flushBatch);
1027
+ }
1028
+ }
1029
+ async flushWriteBehindQueue(options, flushBatch) {
1030
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1031
+ await this.writeBehindFlushPromise;
1032
+ return;
1033
+ }
1034
+ const batchSize = options?.batchSize ?? 100;
1035
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1036
+ this.writeBehindFlushPromise = flushBatch(batch);
1037
+ try {
1038
+ await this.writeBehindFlushPromise;
1039
+ } finally {
1040
+ this.writeBehindFlushPromise = void 0;
1041
+ }
1042
+ if (this.writeBehindQueue.length > 0) {
1043
+ await this.flushWriteBehindQueue(options, flushBatch);
1044
+ }
1045
+ }
1046
+ scheduleGenerationCleanup(generation, task, onError) {
1047
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
1048
+ onError(generation, error);
1049
+ });
1050
+ this.generationCleanupPromise = scheduledTask.finally(() => {
1051
+ if (this.generationCleanupPromise === scheduledTask) {
1052
+ this.generationCleanupPromise = void 0;
1053
+ }
1054
+ });
1055
+ }
1056
+ async waitForGenerationCleanup() {
1057
+ await this.generationCleanupPromise;
1058
+ }
1059
+ };
1060
+
1061
+ // src/internal/CacheStackRuntimePolicy.ts
1062
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1063
+ return degradedUntil !== void 0 && degradedUntil > now;
1064
+ }
1065
+ function shouldStartBackgroundRefresh({
1066
+ isDisconnecting,
1067
+ hasRefreshInFlight
1068
+ }) {
1069
+ return !isDisconnecting && !hasRefreshInFlight;
1070
+ }
1071
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1072
+ if (!gracefulDegradation) {
1073
+ return { degrade: false };
1074
+ }
1075
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1076
+ return {
1077
+ degrade: true,
1078
+ degradedUntil: now + retryAfterMs
1079
+ };
1080
+ }
1081
+ function planFreshReadPolicies({
1082
+ stored,
1083
+ hasFetcher,
1084
+ slidingTtl,
1085
+ refreshAheadSeconds
1086
+ }) {
1087
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1088
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1089
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1090
+ return {
1091
+ refreshedStored,
1092
+ refreshedStoredTtl,
1093
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1094
+ };
1095
+ }
1096
+
1097
+ // src/internal/CacheStackSnapshotManager.ts
1098
+ var import_node_fs = require("fs");
1099
+ var import_node_path = __toESM(require("path"), 1);
1100
+
1101
+ // src/internal/CacheSnapshotFile.ts
1102
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1103
+ const relative = path2.relative(realBaseDir, candidatePath);
1104
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1105
+ }
1106
+ async function findExistingAncestor(directory, fs3, path2) {
1107
+ let current = directory;
1108
+ while (true) {
1109
+ try {
1110
+ await fs3.lstat(current);
1111
+ return current;
1112
+ } catch (error) {
1113
+ if (error.code !== "ENOENT") {
1114
+ throw error;
1115
+ }
1116
+ }
1117
+ const parent = path2.dirname(current);
1118
+ if (parent === current) {
1119
+ return current;
1120
+ }
1121
+ current = parent;
1122
+ }
1123
+ }
1124
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
1125
+ if (filePath.length === 0) {
1126
+ throw new Error("filePath must not be empty.");
1127
+ }
1128
+ if (filePath.includes("\0")) {
1129
+ throw new Error("filePath must not contain null bytes.");
1130
+ }
1131
+ const { promises: fs3 } = await import("fs");
1132
+ const path2 = await import("path");
1133
+ const resolved = path2.resolve(filePath);
1134
+ const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1135
+ if (baseDir === false) {
1136
+ return resolved;
1137
+ }
1138
+ await fs3.mkdir(baseDir, { recursive: true });
1139
+ const realBaseDir = await fs3.realpath(baseDir);
1140
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1141
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1142
+ }
1143
+ if (mode === "read") {
1144
+ const realTarget = await fs3.realpath(resolved);
1145
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1146
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1147
+ }
1148
+ return realTarget;
1149
+ }
1150
+ const parentDir = path2.dirname(resolved);
1151
+ const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
1152
+ const realExistingAncestor = await fs3.realpath(existingAncestor);
1153
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1154
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1155
+ }
1156
+ await fs3.mkdir(parentDir, { recursive: true });
1157
+ const realParentDir = await fs3.realpath(parentDir);
1158
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1159
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1160
+ }
1161
+ const targetPath = path2.join(realParentDir, path2.basename(resolved));
1162
+ try {
1163
+ const existing = await fs3.lstat(targetPath);
1164
+ if (existing.isSymbolicLink()) {
1165
+ throw new Error("filePath must not point to a symbolic link.");
1166
+ }
1167
+ } catch (error) {
1168
+ if (error.code !== "ENOENT") {
1169
+ throw error;
1170
+ }
1171
+ }
1172
+ return targetPath;
1173
+ }
1174
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
1175
+ if (byteLimit === false) {
1176
+ return handle.readFile({ encoding: "utf8" });
1177
+ }
1178
+ const chunks = [];
1179
+ let totalBytes = 0;
1180
+ let position = 0;
1181
+ while (true) {
1182
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
1183
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
1184
+ if (bytesRead === 0) {
1185
+ break;
1186
+ }
1187
+ totalBytes += bytesRead;
1188
+ if (totalBytes > byteLimit) {
1189
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
1190
+ }
1191
+ chunks.push(buffer.subarray(0, bytesRead));
1192
+ position += bytesRead;
1193
+ }
1194
+ return Buffer.concat(chunks).toString("utf8");
1195
+ }
1196
+
1197
+ // src/internal/CacheStackSnapshotManager.ts
1198
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1199
+ var CacheStackSnapshotManager = class {
1200
+ constructor(options) {
1201
+ this.options = options;
1202
+ }
1203
+ options;
1204
+ async exportState(maxEntries) {
1205
+ const entries = [];
1206
+ await this.visitExportEntries(maxEntries, async (entry) => {
1207
+ entries.push(entry);
1208
+ });
1209
+ return entries;
1210
+ }
1211
+ async importState(entries) {
1212
+ const normalizedEntries = entries.map((entry) => ({
1213
+ key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
1214
+ value: entry.value,
1215
+ ttl: entry.ttl
1216
+ }));
1217
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1218
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1219
+ await Promise.all(
1220
+ batch.map(async (entry) => {
1221
+ await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1222
+ await this.options.tagIndex.touch(entry.key);
1223
+ })
1224
+ );
1225
+ }
1226
+ }
1227
+ async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1228
+ const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1229
+ const tempPath = import_node_path.default.join(
1230
+ import_node_path.default.dirname(targetPath),
1231
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
1232
+ );
1233
+ let handle;
1234
+ try {
1235
+ handle = await import_node_fs.promises.open(tempPath, "wx");
1236
+ const openedHandle = handle;
1237
+ await openedHandle.writeFile("[", "utf8");
1238
+ let wroteAny = false;
1239
+ await this.visitExportEntries(maxEntries, async (entry) => {
1240
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1241
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1242
+ wroteAny = true;
1243
+ });
1244
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1245
+ await openedHandle.close();
1246
+ handle = void 0;
1247
+ await import_node_fs.promises.rename(tempPath, targetPath);
1248
+ } catch (error) {
1249
+ await handle?.close().catch(() => void 0);
1250
+ await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
1251
+ throw error;
1252
+ }
1253
+ }
1254
+ async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1255
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1256
+ const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
1257
+ let raw;
1258
+ try {
1259
+ if (maxBytes !== false) {
1260
+ const stat = await handle.stat();
1261
+ if (stat.size > maxBytes) {
1262
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1263
+ }
1264
+ }
1265
+ raw = await readUtf8HandleWithLimit(handle, maxBytes);
1266
+ } finally {
1267
+ await handle.close();
1268
+ }
1269
+ let parsed;
1270
+ try {
1271
+ parsed = JSON.parse(raw);
1272
+ } catch (cause) {
1273
+ throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1274
+ }
1275
+ if (!this.isCacheSnapshotEntries(parsed)) {
1276
+ throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1277
+ }
1278
+ await this.importState(
1279
+ parsed.map((entry) => ({
1280
+ key: entry.key,
1281
+ value: this.sanitizeSnapshotValue(entry.value),
1282
+ ttl: entry.ttl
1283
+ }))
1284
+ );
1285
+ }
1286
+ async visitExportEntries(maxEntries, visitor) {
1287
+ const exported = /* @__PURE__ */ new Set();
1288
+ for (const layer of this.options.layers) {
1289
+ if (!layer.keys && !layer.forEachKey) {
1290
+ continue;
1291
+ }
1292
+ const visitKey = async (key) => {
1293
+ const exportedKey = this.options.stripQualifiedKey(key);
1294
+ if (exported.has(exportedKey)) {
1295
+ return;
1296
+ }
1297
+ const stored = await this.options.readLayerEntry(layer, key);
1298
+ if (stored === null) {
1299
+ return;
1300
+ }
1301
+ exported.add(exportedKey);
1302
+ if (maxEntries !== false && exported.size > maxEntries) {
1303
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1304
+ }
1305
+ await visitor({
1306
+ key: exportedKey,
1307
+ value: stored,
1308
+ ttl: remainingStoredTtlSeconds(stored)
1309
+ });
1310
+ };
1311
+ if (layer.forEachKey) {
1312
+ await layer.forEachKey(visitKey);
1313
+ continue;
1314
+ }
1315
+ const keys = await layer.keys?.();
1316
+ for (const key of keys ?? []) {
1317
+ await visitKey(key);
1318
+ }
1319
+ }
1320
+ }
1321
+ isCacheSnapshotEntries(value) {
1322
+ return Array.isArray(value) && value.every((entry) => {
1323
+ if (!entry || typeof entry !== "object") {
1324
+ return false;
1325
+ }
1326
+ const candidate = entry;
1327
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1328
+ });
954
1329
  }
955
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
956
- }
957
-
958
- // src/internal/CacheStackRuntimePolicy.ts
959
- function shouldSkipLayer(degradedUntil, now = Date.now()) {
960
- return degradedUntil !== void 0 && degradedUntil > now;
961
- }
962
- function shouldStartBackgroundRefresh({
963
- isDisconnecting,
964
- hasRefreshInFlight
965
- }) {
966
- return !isDisconnecting && !hasRefreshInFlight;
967
- }
968
- function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
969
- if (!gracefulDegradation) {
970
- return { degrade: false };
1330
+ sanitizeSnapshotValue(value) {
1331
+ return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
971
1332
  }
972
- const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
973
- return {
974
- degrade: true,
975
- degradedUntil: now + retryAfterMs
976
- };
977
- }
978
- function planFreshReadPolicies({
979
- stored,
980
- hasFetcher,
981
- slidingTtl,
982
- refreshAheadSeconds
983
- }) {
984
- const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
985
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
986
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
987
- return {
988
- refreshedStored,
989
- refreshedStoredTtl,
990
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
991
- };
992
- }
1333
+ };
993
1334
 
994
1335
  // src/internal/CacheStackValidation.ts
995
1336
  var MAX_CACHE_KEY_LENGTH = 1024;
@@ -1218,7 +1559,11 @@ var FetchRateLimiter = class {
1218
1559
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
1219
1560
  nextFetcherBucketId = 0;
1220
1561
  drainTimer;
1562
+ isDisposed = false;
1221
1563
  async schedule(options, context, task) {
1564
+ if (this.isDisposed) {
1565
+ throw new Error("FetchRateLimiter has been disposed.");
1566
+ }
1222
1567
  if (!options) {
1223
1568
  return task();
1224
1569
  }
@@ -1241,6 +1586,27 @@ var FetchRateLimiter = class {
1241
1586
  this.drain();
1242
1587
  });
1243
1588
  }
1589
+ dispose() {
1590
+ this.isDisposed = true;
1591
+ if (this.drainTimer) {
1592
+ clearTimeout(this.drainTimer);
1593
+ this.drainTimer = void 0;
1594
+ }
1595
+ for (const bucket of this.buckets.values()) {
1596
+ if (bucket.cleanupTimer) {
1597
+ clearTimeout(bucket.cleanupTimer);
1598
+ bucket.cleanupTimer = void 0;
1599
+ }
1600
+ }
1601
+ for (const queue of this.queuesByBucket.values()) {
1602
+ for (const item of queue) {
1603
+ item.reject(new Error("FetchRateLimiter has been disposed."));
1604
+ }
1605
+ }
1606
+ this.queuesByBucket.clear();
1607
+ this.pendingBuckets.clear();
1608
+ this.buckets.clear();
1609
+ }
1244
1610
  normalize(options) {
1245
1611
  const maxConcurrent = options.maxConcurrent;
1246
1612
  const intervalMs = options.intervalMs;
@@ -1276,6 +1642,9 @@ var FetchRateLimiter = class {
1276
1642
  return "global";
1277
1643
  }
1278
1644
  drain() {
1645
+ if (this.isDisposed) {
1646
+ return;
1647
+ }
1279
1648
  if (this.drainTimer) {
1280
1649
  clearTimeout(this.drainTimer);
1281
1650
  this.drainTimer = void 0;
@@ -1372,6 +1741,9 @@ var FetchRateLimiter = class {
1372
1741
  }
1373
1742
  }
1374
1743
  bucketState(bucketKey) {
1744
+ if (this.isDisposed) {
1745
+ throw new Error("FetchRateLimiter has been disposed.");
1746
+ }
1375
1747
  const existing = this.buckets.get(bucketKey);
1376
1748
  if (existing) {
1377
1749
  return existing;
@@ -1830,19 +2202,19 @@ var TagIndex = class {
1830
2202
  if (!this.knownKeys.delete(key)) {
1831
2203
  return;
1832
2204
  }
1833
- const path = [];
2205
+ const path2 = [];
1834
2206
  let node = this.root;
1835
2207
  for (const character of key) {
1836
2208
  const child = node.children.get(character);
1837
2209
  if (!child) {
1838
2210
  return;
1839
2211
  }
1840
- path.push([node, character]);
2212
+ path2.push([node, character]);
1841
2213
  node = child;
1842
2214
  }
1843
2215
  node.terminal = false;
1844
- for (let index = path.length - 1; index >= 0; index -= 1) {
1845
- const entry = path[index];
2216
+ for (let index = path2.length - 1; index >= 0; index -= 1) {
2217
+ const entry = path2[index];
1846
2218
  if (!entry) {
1847
2219
  continue;
1848
2220
  }
@@ -1856,39 +2228,31 @@ var TagIndex = class {
1856
2228
  }
1857
2229
  };
1858
2230
 
1859
- // src/serialization/JsonSerializer.ts
1860
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1861
- var MAX_SANITIZE_NODES = 1e4;
1862
- var JsonSerializer = class {
1863
- serialize(value) {
1864
- return JSON.stringify(value);
1865
- }
1866
- deserialize(payload) {
1867
- const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1868
- return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
1869
- }
1870
- };
1871
- var MAX_SANITIZE_DEPTH = 200;
1872
- function sanitizeJsonValue(value, depth, state) {
2231
+ // src/internal/StructuredDataSanitizer.ts
2232
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2233
+ function sanitizeStructuredData(value, options) {
2234
+ return sanitizeValue(value, 0, { count: 0 }, options);
2235
+ }
2236
+ function sanitizeValue(value, depth, state, options) {
1873
2237
  state.count += 1;
1874
- if (state.count > MAX_SANITIZE_NODES) {
1875
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
2238
+ if (state.count > options.maxNodes) {
2239
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1876
2240
  }
1877
- if (depth > MAX_SANITIZE_DEPTH) {
1878
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
2241
+ if (depth > options.maxDepth) {
2242
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1879
2243
  }
1880
2244
  if (Array.isArray(value)) {
1881
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
2245
+ return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
1882
2246
  }
1883
2247
  if (!isPlainObject(value)) {
1884
2248
  return value;
1885
2249
  }
1886
- const sanitized = {};
2250
+ const sanitized = options.createObject?.() ?? {};
1887
2251
  for (const [key, entry] of Object.entries(value)) {
1888
- if (DANGEROUS_JSON_KEYS.has(key)) {
2252
+ if (DANGEROUS_KEYS.has(key)) {
1889
2253
  continue;
1890
2254
  }
1891
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
2255
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1892
2256
  }
1893
2257
  return sanitized;
1894
2258
  }
@@ -1896,6 +2260,21 @@ function isPlainObject(value) {
1896
2260
  return Object.prototype.toString.call(value) === "[object Object]";
1897
2261
  }
1898
2262
 
2263
+ // src/serialization/JsonSerializer.ts
2264
+ var JsonSerializer = class {
2265
+ serialize(value) {
2266
+ return JSON.stringify(value);
2267
+ }
2268
+ deserialize(payload) {
2269
+ const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2270
+ return sanitizeStructuredData(JSON.parse(normalized), {
2271
+ label: "JSON payload",
2272
+ maxDepth: 200,
2273
+ maxNodes: 1e4
2274
+ });
2275
+ }
2276
+ };
2277
+
1899
2278
  // src/stampede/StampedeGuard.ts
1900
2279
  var import_async_mutex2 = require("async-mutex");
1901
2280
  var StampedeGuard = class {
@@ -1940,7 +2319,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1940
2319
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1941
2320
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1942
2321
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1943
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1944
2322
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1945
2323
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1946
2324
  var DebugLogger = class {
@@ -1997,6 +2375,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
1997
2375
  await this.handleLayerFailure(layer, operation, error);
1998
2376
  }
1999
2377
  });
2378
+ this.invalidation = new CacheStackInvalidationSupport({
2379
+ tagIndex: this.tagIndex,
2380
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2381
+ handleLayerFailure: async (layer, operation, error) => {
2382
+ await this.handleLayerFailure(layer, operation, error);
2383
+ }
2384
+ });
2385
+ this.layerWriter = new CacheStackLayerWriter({
2386
+ layers: this.layers,
2387
+ maintenance: this.maintenance,
2388
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2389
+ shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
2390
+ handleLayerFailure: async (layer, operation, error) => {
2391
+ await this.handleLayerFailure(layer, operation, error);
2392
+ },
2393
+ enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2394
+ resolveFreshTtl: this.resolveFreshTtl.bind(this),
2395
+ resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2396
+ globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2397
+ globalStaleIfError: this.options.staleIfError,
2398
+ writePolicy: this.options.writePolicy,
2399
+ onWriteFailures: (context, failures) => {
2400
+ this.metricsCollector.increment("writeFailures", failures.length);
2401
+ this.logger.debug?.("write-failure", {
2402
+ ...context,
2403
+ failures: failures.map((failure) => this.formatError(failure))
2404
+ });
2405
+ }
2406
+ });
2000
2407
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
2001
2408
  this.logger.warn?.(
2002
2409
  "Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
@@ -2012,6 +2419,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2012
2419
  "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
2013
2420
  );
2014
2421
  }
2422
+ this.snapshots = new CacheStackSnapshotManager({
2423
+ layers: this.layers,
2424
+ tagIndex: this.tagIndex,
2425
+ snapshotSerializer: this.snapshotSerializer,
2426
+ readLayerEntry: this.readLayerEntry.bind(this),
2427
+ qualifyKey: this.qualifyKey.bind(this),
2428
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
2429
+ validateCacheKey,
2430
+ formatError: this.formatError.bind(this)
2431
+ });
2015
2432
  this.initializeWriteBehind(options.writeBehind);
2016
2433
  this.startup = this.initialize();
2017
2434
  }
@@ -2027,11 +2444,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2027
2444
  keyDiscovery;
2028
2445
  fetchRateLimiter = new FetchRateLimiter();
2029
2446
  snapshotSerializer = new JsonSerializer();
2447
+ invalidation;
2448
+ layerWriter;
2449
+ snapshots;
2030
2450
  backgroundRefreshes = /* @__PURE__ */ new Map();
2031
2451
  layerDegradedUntil = /* @__PURE__ */ new Map();
2032
2452
  maintenance = new CacheStackMaintenance();
2033
2453
  ttlResolver;
2034
2454
  circuitBreakerManager;
2455
+ nextOperationId = 0;
2035
2456
  currentGeneration;
2036
2457
  isDisconnecting = false;
2037
2458
  disconnectPromise;
@@ -2042,10 +2463,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
2042
2463
  * and no `fetcher` is provided.
2043
2464
  */
2044
2465
  async get(key, fetcher, options) {
2045
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2046
- this.validateWriteOptions(options);
2047
- await this.awaitStartup("get");
2048
- return this.getPrepared(normalizedKey, fetcher, options);
2466
+ return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
2467
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2468
+ this.validateWriteOptions(options);
2469
+ await this.awaitStartup("get");
2470
+ return this.getPrepared(normalizedKey, fetcher, options);
2471
+ });
2049
2472
  }
2050
2473
  async getPrepared(normalizedKey, fetcher, options) {
2051
2474
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
@@ -2167,23 +2590,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
2167
2590
  * Stores a value in all cache layers. Overwrites any existing value.
2168
2591
  */
2169
2592
  async set(key, value, options) {
2170
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2171
- this.validateWriteOptions(options);
2172
- await this.awaitStartup("set");
2173
- await this.storeEntry(normalizedKey, "value", value, options);
2593
+ await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2594
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2595
+ this.validateWriteOptions(options);
2596
+ await this.awaitStartup("set");
2597
+ await this.storeEntry(normalizedKey, "value", value, options);
2598
+ });
2174
2599
  }
2175
2600
  /**
2176
2601
  * Deletes the key from all layers and publishes an invalidation message.
2177
2602
  */
2178
2603
  async delete(key) {
2179
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2180
- await this.awaitStartup("delete");
2181
- await this.deleteKeys([normalizedKey]);
2182
- await this.publishInvalidation({
2183
- scope: "key",
2184
- keys: [normalizedKey],
2185
- sourceId: this.instanceId,
2186
- operation: "delete"
2604
+ await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2605
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2606
+ await this.awaitStartup("delete");
2607
+ await this.deleteKeys([normalizedKey]);
2608
+ await this.publishInvalidation({
2609
+ scope: "key",
2610
+ keys: [normalizedKey],
2611
+ sourceId: this.instanceId,
2612
+ operation: "delete"
2613
+ });
2187
2614
  });
2188
2615
  }
2189
2616
  async clear() {
@@ -2216,95 +2643,99 @@ var CacheStack = class extends import_node_events.EventEmitter {
2216
2643
  });
2217
2644
  }
2218
2645
  async mget(entries) {
2219
- this.assertActive("mget");
2220
- if (entries.length === 0) {
2221
- return [];
2222
- }
2223
- const normalizedEntries = entries.map((entry) => ({
2224
- ...entry,
2225
- key: this.qualifyKey(validateCacheKey(entry.key))
2226
- }));
2227
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2228
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2229
- if (!canFastPath) {
2646
+ return this.observeOperation("layercache.mget", void 0, async () => {
2647
+ this.assertActive("mget");
2648
+ if (entries.length === 0) {
2649
+ return [];
2650
+ }
2651
+ const normalizedEntries = entries.map((entry) => ({
2652
+ ...entry,
2653
+ key: this.qualifyKey(validateCacheKey(entry.key))
2654
+ }));
2655
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2656
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2657
+ if (!canFastPath) {
2658
+ await this.awaitStartup("mget");
2659
+ const pendingReads = /* @__PURE__ */ new Map();
2660
+ return Promise.all(
2661
+ normalizedEntries.map((entry) => {
2662
+ const optionsSignature = serializeOptions(entry.options);
2663
+ const existing = pendingReads.get(entry.key);
2664
+ if (!existing) {
2665
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2666
+ pendingReads.set(entry.key, {
2667
+ promise,
2668
+ fetch: entry.fetch,
2669
+ optionsSignature
2670
+ });
2671
+ return promise;
2672
+ }
2673
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2674
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2675
+ }
2676
+ return existing.promise;
2677
+ })
2678
+ );
2679
+ }
2230
2680
  await this.awaitStartup("mget");
2231
- const pendingReads = /* @__PURE__ */ new Map();
2232
- return Promise.all(
2233
- normalizedEntries.map((entry) => {
2234
- const optionsSignature = serializeOptions(entry.options);
2235
- const existing = pendingReads.get(entry.key);
2236
- if (!existing) {
2237
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2238
- pendingReads.set(entry.key, {
2239
- promise,
2240
- fetch: entry.fetch,
2241
- optionsSignature
2242
- });
2243
- return promise;
2244
- }
2245
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2246
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2247
- }
2248
- return existing.promise;
2249
- })
2250
- );
2251
- }
2252
- await this.awaitStartup("mget");
2253
- const pending = /* @__PURE__ */ new Set();
2254
- const indexesByKey = /* @__PURE__ */ new Map();
2255
- const resultsByKey = /* @__PURE__ */ new Map();
2256
- for (let index = 0; index < normalizedEntries.length; index += 1) {
2257
- const entry = normalizedEntries[index];
2258
- if (!entry) continue;
2259
- const key = entry.key;
2260
- const indexes = indexesByKey.get(key) ?? [];
2261
- indexes.push(index);
2262
- indexesByKey.set(key, indexes);
2263
- pending.add(key);
2264
- }
2265
- for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2266
- const layer = this.layers[layerIndex];
2267
- if (!layer) continue;
2268
- const keys = [...pending];
2269
- if (keys.length === 0) {
2270
- break;
2681
+ const pending = /* @__PURE__ */ new Set();
2682
+ const indexesByKey = /* @__PURE__ */ new Map();
2683
+ const resultsByKey = /* @__PURE__ */ new Map();
2684
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
2685
+ const entry = normalizedEntries[index];
2686
+ if (!entry) continue;
2687
+ const key = entry.key;
2688
+ const indexes = indexesByKey.get(key) ?? [];
2689
+ indexes.push(index);
2690
+ indexesByKey.set(key, indexes);
2691
+ pending.add(key);
2271
2692
  }
2272
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2273
- for (let offset = 0; offset < values.length; offset += 1) {
2274
- const key = keys[offset];
2275
- const stored = values[offset];
2276
- if (!key || stored === null) {
2277
- continue;
2693
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2694
+ const layer = this.layers[layerIndex];
2695
+ if (!layer) continue;
2696
+ const keys = [...pending];
2697
+ if (keys.length === 0) {
2698
+ break;
2278
2699
  }
2279
- const resolved = resolveStoredValue(stored);
2280
- if (resolved.state === "expired") {
2281
- await layer.delete(key);
2282
- continue;
2700
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2701
+ for (let offset = 0; offset < values.length; offset += 1) {
2702
+ const key = keys[offset];
2703
+ const stored = values[offset];
2704
+ if (!key || stored === null) {
2705
+ continue;
2706
+ }
2707
+ const resolved = resolveStoredValue(stored);
2708
+ if (resolved.state === "expired") {
2709
+ await layer.delete(key);
2710
+ continue;
2711
+ }
2712
+ await this.tagIndex.touch(key);
2713
+ await this.backfill(key, stored, layerIndex - 1);
2714
+ resultsByKey.set(key, resolved.value);
2715
+ pending.delete(key);
2716
+ this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2283
2717
  }
2284
- await this.tagIndex.touch(key);
2285
- await this.backfill(key, stored, layerIndex - 1);
2286
- resultsByKey.set(key, resolved.value);
2287
- pending.delete(key);
2288
- this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2289
2718
  }
2290
- }
2291
- if (pending.size > 0) {
2292
- for (const key of pending) {
2293
- await this.tagIndex.remove(key);
2294
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2719
+ if (pending.size > 0) {
2720
+ for (const key of pending) {
2721
+ await this.tagIndex.remove(key);
2722
+ this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2723
+ }
2295
2724
  }
2296
- }
2297
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2725
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2726
+ });
2298
2727
  }
2299
2728
  async mset(entries) {
2300
- this.assertActive("mset");
2301
- const normalizedEntries = entries.map((entry) => ({
2302
- ...entry,
2303
- key: this.qualifyKey(validateCacheKey(entry.key))
2304
- }));
2305
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2306
- await this.awaitStartup("mset");
2307
- await this.writeBatch(normalizedEntries);
2729
+ await this.observeOperation("layercache.mset", void 0, async () => {
2730
+ this.assertActive("mset");
2731
+ const normalizedEntries = entries.map((entry) => ({
2732
+ ...entry,
2733
+ key: this.qualifyKey(validateCacheKey(entry.key))
2734
+ }));
2735
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2736
+ await this.awaitStartup("mset");
2737
+ await this.writeBatch(normalizedEntries);
2738
+ });
2308
2739
  }
2309
2740
  async warm(entries, options = {}) {
2310
2741
  this.assertActive("warm");
@@ -2357,40 +2788,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
2357
2788
  return new CacheNamespace(this, prefix);
2358
2789
  }
2359
2790
  async invalidateByTag(tag) {
2360
- validateTag(tag);
2361
- await this.awaitStartup("invalidateByTag");
2362
- const keys = await this.collectKeysForTag(tag);
2363
- await this.deleteKeys(keys);
2364
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2791
+ await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2792
+ validateTag(tag);
2793
+ await this.awaitStartup("invalidateByTag");
2794
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
2795
+ await this.deleteKeys(keys);
2796
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2797
+ });
2365
2798
  }
2366
2799
  async invalidateByTags(tags, mode = "any") {
2367
- if (tags.length === 0) {
2368
- return;
2369
- }
2370
- validateTags(tags);
2371
- await this.awaitStartup("invalidateByTags");
2372
- const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
2373
- const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2374
- this.assertWithinInvalidationKeyLimit(keys.length);
2375
- await this.deleteKeys(keys);
2376
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2800
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2801
+ if (tags.length === 0) {
2802
+ return;
2803
+ }
2804
+ validateTags(tags);
2805
+ await this.awaitStartup("invalidateByTags");
2806
+ const keysByTag = await Promise.all(
2807
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2808
+ );
2809
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2810
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2811
+ await this.deleteKeys(keys);
2812
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2813
+ });
2377
2814
  }
2378
2815
  async invalidateByPattern(pattern) {
2379
- validatePattern(pattern);
2380
- await this.awaitStartup("invalidateByPattern");
2381
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2382
- this.qualifyPattern(pattern),
2383
- this.invalidationMaxKeys()
2384
- );
2385
- await this.deleteKeys(keys);
2386
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2816
+ await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2817
+ validatePattern(pattern);
2818
+ await this.awaitStartup("invalidateByPattern");
2819
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2820
+ this.qualifyPattern(pattern),
2821
+ this.invalidationMaxKeys()
2822
+ );
2823
+ await this.deleteKeys(keys);
2824
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2825
+ });
2387
2826
  }
2388
2827
  async invalidateByPrefix(prefix) {
2389
- await this.awaitStartup("invalidateByPrefix");
2390
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2391
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2392
- await this.deleteKeys(keys);
2393
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2828
+ await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2829
+ await this.awaitStartup("invalidateByPrefix");
2830
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2831
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2832
+ await this.deleteKeys(keys);
2833
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2834
+ });
2394
2835
  }
2395
2836
  getMetrics() {
2396
2837
  return this.metricsCollector.snapshot;
@@ -2492,104 +2933,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
2492
2933
  errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
2493
2934
  isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
2494
2935
  }
2495
- }
2496
- if (foundInLayers.length === 0) {
2497
- return null;
2498
- }
2499
- const tags = await this.getTagsForKey(normalizedKey);
2500
- return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
2501
- }
2502
- async exportState() {
2503
- await this.awaitStartup("exportState");
2504
- const entries = [];
2505
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2506
- entries.push(entry);
2507
- });
2508
- return entries;
2509
- }
2510
- async importState(entries) {
2511
- await this.awaitStartup("importState");
2512
- const normalizedEntries = entries.map((entry) => ({
2513
- key: this.qualifyKey(validateCacheKey(entry.key)),
2514
- value: entry.value,
2515
- ttl: entry.ttl
2516
- }));
2517
- for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2518
- const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2519
- await Promise.all(
2520
- batch.map(async (entry) => {
2521
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2522
- await this.tagIndex.touch(entry.key);
2523
- })
2524
- );
2525
- }
2526
- }
2527
- async persistToFile(filePath) {
2528
- this.assertActive("persistToFile");
2529
- const { promises: fs2 } = await import("fs");
2530
- const path = await import("path");
2531
- const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2532
- const tempPath = path.join(
2533
- path.dirname(targetPath),
2534
- `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2535
- );
2536
- let handle;
2537
- try {
2538
- handle = await fs2.open(tempPath, "wx");
2539
- const openedHandle = handle;
2540
- await openedHandle.writeFile("[", "utf8");
2541
- let wroteAny = false;
2542
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2543
- await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2544
- await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2545
- wroteAny = true;
2546
- });
2547
- await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2548
- await openedHandle.close();
2549
- handle = void 0;
2550
- await fs2.rename(tempPath, targetPath);
2551
- } catch (error) {
2552
- await handle?.close().catch(() => void 0);
2553
- await fs2.unlink(tempPath).catch(() => void 0);
2554
- throw error;
2555
- }
2556
- }
2557
- async restoreFromFile(filePath) {
2558
- this.assertActive("restoreFromFile");
2559
- const { promises: fs2, constants } = await import("fs");
2560
- const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2561
- const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2562
- const snapshotMaxBytes = this.snapshotMaxBytes();
2563
- let raw;
2564
- try {
2565
- if (snapshotMaxBytes !== false) {
2566
- const stat = await handle.stat();
2567
- if (stat.size > snapshotMaxBytes) {
2568
- throw new Error(
2569
- `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2570
- );
2571
- }
2572
- }
2573
- raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2574
- } finally {
2575
- await handle.close();
2576
- }
2577
- let parsed;
2578
- try {
2579
- parsed = JSON.parse(raw);
2580
- } catch (cause) {
2581
- throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
2582
- }
2583
- if (!this.isCacheSnapshotEntries(parsed)) {
2584
- throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
2585
- }
2586
- await this.importState(
2587
- parsed.map((entry) => ({
2588
- key: entry.key,
2589
- value: this.sanitizeSnapshotValue(entry.value),
2590
- ttl: entry.ttl
2591
- }))
2592
- );
2936
+ }
2937
+ if (foundInLayers.length === 0) {
2938
+ return null;
2939
+ }
2940
+ const tags = await this.getTagsForKey(normalizedKey);
2941
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
2942
+ }
2943
+ async exportState() {
2944
+ await this.awaitStartup("exportState");
2945
+ return this.snapshots.exportState(this.snapshotMaxEntries());
2946
+ }
2947
+ async importState(entries) {
2948
+ await this.awaitStartup("importState");
2949
+ await this.snapshots.importState(entries);
2950
+ }
2951
+ async persistToFile(filePath) {
2952
+ this.assertActive("persistToFile");
2953
+ await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2954
+ }
2955
+ async restoreFromFile(filePath) {
2956
+ this.assertActive("restoreFromFile");
2957
+ await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2593
2958
  }
2594
2959
  async disconnect() {
2595
2960
  if (!this.disconnectPromise) {
@@ -2601,6 +2966,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2601
2966
  await this.maintenance.waitForGenerationCleanup();
2602
2967
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2603
2968
  this.maintenance.disposeWriteBehindTimer();
2969
+ this.fetchRateLimiter.dispose();
2604
2970
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2605
2971
  })();
2606
2972
  }
@@ -2714,7 +3080,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2714
3080
  async storeEntry(key, kind, value, options) {
2715
3081
  const clearEpoch = this.maintenance.currentClearEpoch();
2716
3082
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
2717
- await this.writeAcrossLayers(key, kind, value, options);
3083
+ await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2718
3084
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2719
3085
  return;
2720
3086
  }
@@ -2731,52 +3097,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2731
3097
  }
2732
3098
  }
2733
3099
  async writeBatch(entries) {
2734
- const now = Date.now();
2735
- const clearEpoch = this.maintenance.currentClearEpoch();
2736
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2737
- const entriesByLayer = /* @__PURE__ */ new Map();
2738
- const immediateOperations = [];
2739
- const deferredOperations = [];
2740
- for (const entry of entries) {
2741
- for (const layer of this.layers) {
2742
- if (this.shouldSkipLayer(layer)) {
2743
- continue;
2744
- }
2745
- const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
2746
- const bucket = entriesByLayer.get(layer) ?? [];
2747
- bucket.push(layerEntry);
2748
- entriesByLayer.set(layer, bucket);
2749
- }
2750
- }
2751
- for (const [layer, layerEntries] of entriesByLayer.entries()) {
2752
- const operation = async () => {
2753
- if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2754
- return;
2755
- }
2756
- const activeEntries = layerEntries.filter(
2757
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2758
- );
2759
- if (activeEntries.length === 0) {
2760
- return;
2761
- }
2762
- try {
2763
- if (layer.setMany) {
2764
- await layer.setMany(activeEntries);
2765
- return;
2766
- }
2767
- await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2768
- } catch (error) {
2769
- await this.handleLayerFailure(layer, "write", error);
2770
- }
2771
- };
2772
- if (this.shouldWriteBehind(layer)) {
2773
- deferredOperations.push(operation);
2774
- } else {
2775
- immediateOperations.push(operation);
2776
- }
2777
- }
2778
- await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2779
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
3100
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
2780
3101
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2781
3102
  return;
2782
3103
  }
@@ -2883,58 +3204,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2883
3204
  this.emit("backfill", { key, layer: layer.name });
2884
3205
  }
2885
3206
  }
2886
- async writeAcrossLayers(key, kind, value, options) {
2887
- const now = Date.now();
2888
- const clearEpoch = this.maintenance.currentClearEpoch();
2889
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
2890
- const immediateOperations = [];
2891
- const deferredOperations = [];
2892
- for (const layer of this.layers) {
2893
- const operation = async () => {
2894
- if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2895
- return;
2896
- }
2897
- if (this.shouldSkipLayer(layer)) {
2898
- return;
2899
- }
2900
- const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
2901
- try {
2902
- await layer.set(entry.key, entry.value, entry.ttl);
2903
- } catch (error) {
2904
- await this.handleLayerFailure(layer, "write", error);
2905
- }
2906
- };
2907
- if (this.shouldWriteBehind(layer)) {
2908
- deferredOperations.push(operation);
2909
- } else {
2910
- immediateOperations.push(operation);
2911
- }
2912
- }
2913
- await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
2914
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2915
- }
2916
- async executeLayerOperations(operations, context) {
2917
- if (this.options.writePolicy !== "best-effort") {
2918
- await Promise.all(operations.map((operation) => operation()));
2919
- return;
2920
- }
2921
- const results = await Promise.allSettled(operations.map((operation) => operation()));
2922
- const failures = results.filter((result) => result.status === "rejected");
2923
- if (failures.length === 0) {
2924
- return;
2925
- }
2926
- this.metricsCollector.increment("writeFailures", failures.length);
2927
- this.logger.debug?.("write-failure", {
2928
- ...context,
2929
- failures: failures.map((failure) => this.formatError(failure.reason))
2930
- });
2931
- if (failures.length === operations.length) {
2932
- throw new AggregateError(
2933
- failures.map((failure) => failure.reason),
2934
- `${context.action} failed for every cache layer`
2935
- );
2936
- }
2937
- }
2938
3207
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2939
3208
  return this.ttlResolver.resolveFreshTtl(
2940
3209
  key,
@@ -3000,7 +3269,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3000
3269
  return;
3001
3270
  }
3002
3271
  this.maintenance.bumpKeyEpochs(keys);
3003
- await this.deleteKeysFromLayers(this.layers, keys);
3272
+ await this.invalidation.deleteKeysFromLayers(this.layers, keys);
3004
3273
  for (const key of keys) {
3005
3274
  await this.tagIndex.remove(key);
3006
3275
  this.ttlResolver.deleteProfile(key);
@@ -3032,7 +3301,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3032
3301
  }
3033
3302
  const keys = message.keys ?? [];
3034
3303
  this.maintenance.bumpKeyEpochs(keys);
3035
- await this.deleteKeysFromLayers(localLayers, keys);
3304
+ await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3036
3305
  if (message.operation !== "write") {
3037
3306
  for (const key of keys) {
3038
3307
  await this.tagIndex.remove(key);
@@ -3089,6 +3358,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
3089
3358
  shouldBroadcastL1Invalidation() {
3090
3359
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3091
3360
  }
3361
+ async observeOperation(name, attributes, execute) {
3362
+ const id = this.nextOperationId;
3363
+ this.nextOperationId += 1;
3364
+ this.emit("operation-start", { id, name, attributes });
3365
+ try {
3366
+ const result = await execute();
3367
+ this.emit("operation-end", {
3368
+ id,
3369
+ name,
3370
+ attributes,
3371
+ success: true,
3372
+ result: result === null ? "null" : void 0
3373
+ });
3374
+ return result;
3375
+ } catch (error) {
3376
+ this.emit("operation-end", {
3377
+ id,
3378
+ name,
3379
+ attributes,
3380
+ success: false,
3381
+ error
3382
+ });
3383
+ throw error;
3384
+ }
3385
+ }
3092
3386
  scheduleGenerationCleanup(generation) {
3093
3387
  this.maintenance.scheduleGenerationCleanup(
3094
3388
  generation,
@@ -3144,37 +3438,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3144
3438
  });
3145
3439
  this.emitError("write-behind", { failed: failures.length, total: batch.length });
3146
3440
  }
3147
- buildLayerSetEntry(layer, key, kind, value, options, now) {
3148
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
3149
- const staleWhileRevalidate = this.resolveLayerSeconds(
3150
- layer.name,
3151
- options?.staleWhileRevalidate,
3152
- this.options.staleWhileRevalidate
3153
- );
3154
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
3155
- const payload = createStoredValueEnvelope({
3156
- kind,
3157
- value,
3158
- freshTtlSeconds: freshTtl,
3159
- staleWhileRevalidateSeconds: staleWhileRevalidate,
3160
- staleIfErrorSeconds: staleIfError,
3161
- now
3162
- });
3163
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
3164
- return {
3165
- key,
3166
- value: payload,
3167
- ttl
3168
- };
3169
- }
3170
- intersectKeys(groups) {
3171
- if (groups.length === 0) {
3172
- return [];
3173
- }
3174
- const [firstGroup, ...rest] = groups;
3175
- const restSets = rest.map((group) => new Set(group));
3176
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3177
- }
3178
3441
  qualifyKey(key) {
3179
3442
  return qualifyGenerationKey(key, this.currentGeneration);
3180
3443
  }
@@ -3184,32 +3447,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3184
3447
  stripQualifiedKey(key) {
3185
3448
  return stripGenerationPrefix(key, this.currentGeneration);
3186
3449
  }
3187
- async deleteKeysFromLayers(layers, keys) {
3188
- await Promise.all(
3189
- layers.map(async (layer) => {
3190
- if (this.shouldSkipLayer(layer)) {
3191
- return;
3192
- }
3193
- if (layer.deleteMany) {
3194
- try {
3195
- await layer.deleteMany(keys);
3196
- } catch (error) {
3197
- await this.handleLayerFailure(layer, "delete", error);
3198
- }
3199
- return;
3200
- }
3201
- await Promise.all(
3202
- keys.map(async (key) => {
3203
- try {
3204
- await layer.delete(key);
3205
- } catch (error) {
3206
- await this.handleLayerFailure(layer, "delete", error);
3207
- }
3208
- })
3209
- );
3210
- })
3211
- );
3212
- }
3213
3450
  validateConfiguration() {
3214
3451
  if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
3215
3452
  throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
@@ -3340,18 +3577,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3340
3577
  this.emit("error", { operation, ...context });
3341
3578
  }
3342
3579
  }
3343
- isCacheSnapshotEntries(value) {
3344
- return Array.isArray(value) && value.every((entry) => {
3345
- if (!entry || typeof entry !== "object") {
3346
- return false;
3347
- }
3348
- const candidate = entry;
3349
- return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
3350
- });
3351
- }
3352
- sanitizeSnapshotValue(value) {
3353
- return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
3354
- }
3355
3580
  snapshotMaxBytes() {
3356
3581
  return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3357
3582
  }
@@ -3361,62 +3586,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3361
3586
  invalidationMaxKeys() {
3362
3587
  return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3363
3588
  }
3364
- async collectKeysForTag(tag) {
3365
- const keys = /* @__PURE__ */ new Set();
3366
- if (this.tagIndex.forEachKeyForTag) {
3367
- await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3368
- keys.add(key);
3369
- this.assertWithinInvalidationKeyLimit(keys.size);
3370
- });
3371
- return [...keys];
3372
- }
3373
- for (const key of await this.tagIndex.keysForTag(tag)) {
3374
- keys.add(key);
3375
- this.assertWithinInvalidationKeyLimit(keys.size);
3376
- }
3377
- return [...keys];
3378
- }
3379
- assertWithinInvalidationKeyLimit(size) {
3380
- const maxKeys = this.invalidationMaxKeys();
3381
- if (maxKeys !== false && size > maxKeys) {
3382
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
3383
- }
3384
- }
3385
- async visitExportEntries(maxEntries, visitor) {
3386
- const exported = /* @__PURE__ */ new Set();
3387
- for (const layer of this.layers) {
3388
- if (!layer.keys && !layer.forEachKey) {
3389
- continue;
3390
- }
3391
- const visitKey = async (key) => {
3392
- const exportedKey = this.stripQualifiedKey(key);
3393
- if (exported.has(exportedKey)) {
3394
- return;
3395
- }
3396
- const stored = await this.readLayerEntry(layer, key);
3397
- if (stored === null) {
3398
- return;
3399
- }
3400
- exported.add(exportedKey);
3401
- if (maxEntries !== false && exported.size > maxEntries) {
3402
- throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3403
- }
3404
- await visitor({
3405
- key: exportedKey,
3406
- value: stored,
3407
- ttl: remainingStoredTtlSeconds(stored)
3408
- });
3409
- };
3410
- if (layer.forEachKey) {
3411
- await layer.forEachKey(visitKey);
3412
- continue;
3413
- }
3414
- const keys = await layer.keys?.();
3415
- for (const key of keys ?? []) {
3416
- await visitKey(key);
3417
- }
3418
- }
3419
- }
3420
3589
  };
3421
3590
 
3422
3591
  // src/invalidation/RedisInvalidationBus.ts
@@ -3458,7 +3627,12 @@ var RedisInvalidationBus = class {
3458
3627
  async dispatchToHandlers(payload) {
3459
3628
  let message;
3460
3629
  try {
3461
- const parsed = sanitizeJsonValue2(JSON.parse(payload));
3630
+ const parsed = sanitizeStructuredData(JSON.parse(payload), {
3631
+ label: "Invalidation payload",
3632
+ maxDepth: 64,
3633
+ maxNodes: 1e4,
3634
+ createObject: () => /* @__PURE__ */ Object.create(null)
3635
+ });
3462
3636
  if (!this.isInvalidationMessage(parsed)) {
3463
3637
  throw new Error("Invalid invalidation payload shape.");
3464
3638
  }
@@ -3495,31 +3669,6 @@ var RedisInvalidationBus = class {
3495
3669
  console.error(`[layercache] ${message}`, error);
3496
3670
  }
3497
3671
  };
3498
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3499
- var MAX_SANITIZE_DEPTH2 = 64;
3500
- var MAX_SANITIZE_NODES2 = 1e4;
3501
- function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
3502
- state.count += 1;
3503
- if (state.count > MAX_SANITIZE_NODES2) {
3504
- throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
3505
- }
3506
- if (depth > MAX_SANITIZE_DEPTH2) {
3507
- throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
3508
- }
3509
- if (Array.isArray(value)) {
3510
- return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
3511
- }
3512
- if (value && typeof value === "object") {
3513
- const result = /* @__PURE__ */ Object.create(null);
3514
- for (const key of Object.keys(value)) {
3515
- if (!DANGEROUS_KEYS.has(key)) {
3516
- result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
3517
- }
3518
- }
3519
- return result;
3520
- }
3521
- return value;
3522
- }
3523
3672
 
3524
3673
  // src/invalidation/RedisTagIndex.ts
3525
3674
  var RedisTagIndex = class {
@@ -3889,64 +4038,37 @@ function normalizeUrl2(url) {
3889
4038
 
3890
4039
  // src/integrations/opentelemetry.ts
3891
4040
  function createOpenTelemetryPlugin(cache, tracer) {
3892
- const originals = {
3893
- get: cache.get.bind(cache),
3894
- set: cache.set.bind(cache),
3895
- delete: cache.delete.bind(cache),
3896
- mget: cache.mget.bind(cache),
3897
- mset: cache.mset.bind(cache),
3898
- invalidateByTag: cache.invalidateByTag.bind(cache),
3899
- invalidateByTags: cache.invalidateByTags.bind(cache),
3900
- invalidateByPattern: cache.invalidateByPattern.bind(cache),
3901
- invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
4041
+ const spans = /* @__PURE__ */ new Map();
4042
+ const onStart = (event) => {
4043
+ spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
3902
4044
  };
3903
- cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
3904
- "layercache.key": String(args[0] ?? "")
3905
- }));
3906
- cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
3907
- "layercache.key": String(args[0] ?? "")
3908
- }));
3909
- cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
3910
- "layercache.key": String(args[0] ?? "")
3911
- }));
3912
- cache.mget = instrument("layercache.mget", tracer, originals.mget);
3913
- cache.mset = instrument("layercache.mset", tracer, originals.mset);
3914
- cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
3915
- cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
3916
- cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
3917
- cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
3918
- return {
3919
- uninstall() {
3920
- cache.get = originals.get;
3921
- cache.set = originals.set;
3922
- cache.delete = originals.delete;
3923
- cache.mget = originals.mget;
3924
- cache.mset = originals.mset;
3925
- cache.invalidateByTag = originals.invalidateByTag;
3926
- cache.invalidateByTags = originals.invalidateByTags;
3927
- cache.invalidateByPattern = originals.invalidateByPattern;
3928
- cache.invalidateByPrefix = originals.invalidateByPrefix;
4045
+ const onEnd = (event) => {
4046
+ const span = spans.get(event.id);
4047
+ if (!span) {
4048
+ return;
4049
+ }
4050
+ spans.delete(event.id);
4051
+ span.setAttribute?.("layercache.success", event.success);
4052
+ if (event.result) {
4053
+ span.setAttribute?.("layercache.result", event.result);
3929
4054
  }
4055
+ if (event.error !== void 0) {
4056
+ span.recordException?.(event.error);
4057
+ }
4058
+ span.end();
3930
4059
  };
3931
- }
3932
- function instrument(name, tracer, method, attributes) {
3933
- return (async (...args) => {
3934
- const span = tracer.startSpan(name, { attributes: attributes?.(args) });
3935
- try {
3936
- const result = await method(...args);
3937
- span.setAttribute?.("layercache.success", true);
3938
- if (result === null) {
3939
- span.setAttribute?.("layercache.result", "null");
4060
+ cache.on("operation-start", onStart);
4061
+ cache.on("operation-end", onEnd);
4062
+ return {
4063
+ uninstall() {
4064
+ cache.off("operation-start", onStart);
4065
+ cache.off("operation-end", onEnd);
4066
+ for (const span of spans.values()) {
4067
+ span.end();
3940
4068
  }
3941
- return result;
3942
- } catch (error) {
3943
- span.setAttribute?.("layercache.success", false);
3944
- span.recordException?.(error);
3945
- throw error;
3946
- } finally {
3947
- span.end();
4069
+ spans.clear();
3948
4070
  }
3949
- });
4071
+ };
3950
4072
  }
3951
4073
 
3952
4074
  // src/integrations/trpc.ts
@@ -4498,8 +4620,8 @@ var RedisLayer = class {
4498
4620
 
4499
4621
  // src/layers/DiskLayer.ts
4500
4622
  var import_node_crypto = require("crypto");
4501
- var import_node_fs = require("fs");
4502
- var import_node_path = require("path");
4623
+ var import_node_fs2 = require("fs");
4624
+ var import_node_path2 = require("path");
4503
4625
  var FILE_SCAN_CONCURRENCY = 32;
4504
4626
  var DiskLayer = class {
4505
4627
  name;
@@ -4542,7 +4664,7 @@ var DiskLayer = class {
4542
4664
  }
4543
4665
  async set(key, value, ttl = this.defaultTtl) {
4544
4666
  await this.enqueueWrite(async () => {
4545
- await import_node_fs.promises.mkdir(this.directory, { recursive: true });
4667
+ await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
4546
4668
  const entry = {
4547
4669
  key,
4548
4670
  value,
@@ -4552,8 +4674,8 @@ var DiskLayer = class {
4552
4674
  const targetPath = this.keyToPath(key);
4553
4675
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
4554
4676
  try {
4555
- await import_node_fs.promises.writeFile(tempPath, payload);
4556
- await import_node_fs.promises.rename(tempPath, targetPath);
4677
+ await import_node_fs2.promises.writeFile(tempPath, payload);
4678
+ await import_node_fs2.promises.rename(tempPath, targetPath);
4557
4679
  } catch (error) {
4558
4680
  await this.safeDelete(tempPath);
4559
4681
  throw error;
@@ -4607,12 +4729,12 @@ var DiskLayer = class {
4607
4729
  await this.enqueueWrite(async () => {
4608
4730
  let entries;
4609
4731
  try {
4610
- entries = await import_node_fs.promises.readdir(this.directory);
4732
+ entries = await import_node_fs2.promises.readdir(this.directory);
4611
4733
  } catch {
4612
4734
  return;
4613
4735
  }
4614
4736
  await this.deletePathsWithConcurrency(
4615
- entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
4737
+ entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
4616
4738
  );
4617
4739
  });
4618
4740
  }
@@ -4641,7 +4763,7 @@ var DiskLayer = class {
4641
4763
  }
4642
4764
  async ping() {
4643
4765
  try {
4644
- await import_node_fs.promises.mkdir(this.directory, { recursive: true });
4766
+ await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
4645
4767
  return true;
4646
4768
  } catch {
4647
4769
  return false;
@@ -4651,7 +4773,7 @@ var DiskLayer = class {
4651
4773
  }
4652
4774
  keyToPath(key) {
4653
4775
  const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
4654
- return (0, import_node_path.join)(this.directory, `${hash}.lc`);
4776
+ return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
4655
4777
  }
4656
4778
  resolveDirectory(directory) {
4657
4779
  if (typeof directory !== "string" || directory.trim().length === 0) {
@@ -4660,7 +4782,7 @@ var DiskLayer = class {
4660
4782
  if (directory.includes("\0")) {
4661
4783
  throw new Error("DiskLayer.directory must not contain null bytes.");
4662
4784
  }
4663
- return (0, import_node_path.resolve)(directory);
4785
+ return (0, import_node_path2.resolve)(directory);
4664
4786
  }
4665
4787
  normalizeMaxFiles(maxFiles) {
4666
4788
  if (maxFiles === void 0) {
@@ -4684,7 +4806,7 @@ var DiskLayer = class {
4684
4806
  async readEntryFile(filePath) {
4685
4807
  let handle;
4686
4808
  try {
4687
- handle = await import_node_fs.promises.open(filePath, "r");
4809
+ handle = await import_node_fs2.promises.open(filePath, "r");
4688
4810
  return await this.readHandleWithLimit(handle);
4689
4811
  } catch {
4690
4812
  await this.safeDelete(filePath);
@@ -4724,7 +4846,7 @@ var DiskLayer = class {
4724
4846
  async scanEntries(visitor) {
4725
4847
  let entries;
4726
4848
  try {
4727
- entries = await import_node_fs.promises.readdir(this.directory);
4849
+ entries = await import_node_fs2.promises.readdir(this.directory);
4728
4850
  } catch {
4729
4851
  return;
4730
4852
  }
@@ -4740,7 +4862,7 @@ var DiskLayer = class {
4740
4862
  if (name === void 0) {
4741
4863
  return;
4742
4864
  }
4743
- const filePath = (0, import_node_path.join)(this.directory, name);
4865
+ const filePath = (0, import_node_path2.join)(this.directory, name);
4744
4866
  const raw = await this.readEntryFile(filePath);
4745
4867
  if (raw === null) {
4746
4868
  continue;
@@ -4787,7 +4909,7 @@ var DiskLayer = class {
4787
4909
  }
4788
4910
  async safeDelete(filePath) {
4789
4911
  try {
4790
- await import_node_fs.promises.unlink(filePath);
4912
+ await import_node_fs2.promises.unlink(filePath);
4791
4913
  } catch {
4792
4914
  }
4793
4915
  }
@@ -4805,7 +4927,7 @@ var DiskLayer = class {
4805
4927
  }
4806
4928
  let entries;
4807
4929
  try {
4808
- entries = await import_node_fs.promises.readdir(this.directory);
4930
+ entries = await import_node_fs2.promises.readdir(this.directory);
4809
4931
  } catch {
4810
4932
  return;
4811
4933
  }
@@ -4815,9 +4937,9 @@ var DiskLayer = class {
4815
4937
  }
4816
4938
  const withStats = await Promise.all(
4817
4939
  lcFiles.map(async (name) => {
4818
- const filePath = (0, import_node_path.join)(this.directory, name);
4940
+ const filePath = (0, import_node_path2.join)(this.directory, name);
4819
4941
  try {
4820
- const stat = await import_node_fs.promises.stat(filePath);
4942
+ const stat = await import_node_fs2.promises.stat(filePath);
4821
4943
  return { filePath, mtimeMs: stat.mtimeMs };
4822
4944
  } catch {
4823
4945
  return { filePath, mtimeMs: 0 };
@@ -4913,44 +5035,19 @@ var MemcachedLayer = class {
4913
5035
 
4914
5036
  // src/serialization/MsgpackSerializer.ts
4915
5037
  var import_msgpack = require("@msgpack/msgpack");
4916
- var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
4917
- var MAX_SANITIZE_DEPTH3 = 64;
4918
- var MAX_SANITIZE_NODES3 = 1e4;
4919
5038
  var MsgpackSerializer = class {
4920
5039
  serialize(value) {
4921
5040
  return Buffer.from((0, import_msgpack.encode)(value));
4922
5041
  }
4923
5042
  deserialize(payload) {
4924
5043
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4925
- return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
5044
+ return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
5045
+ label: "MessagePack payload",
5046
+ maxDepth: 64,
5047
+ maxNodes: 1e4
5048
+ });
4926
5049
  }
4927
5050
  };
4928
- function sanitizeMsgpackValue(value, depth, state) {
4929
- state.count += 1;
4930
- if (state.count > MAX_SANITIZE_NODES3) {
4931
- throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
4932
- }
4933
- if (depth > MAX_SANITIZE_DEPTH3) {
4934
- throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
4935
- }
4936
- if (Array.isArray(value)) {
4937
- return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
4938
- }
4939
- if (!isPlainObject2(value)) {
4940
- return value;
4941
- }
4942
- const sanitized = {};
4943
- for (const [key, entry] of Object.entries(value)) {
4944
- if (DANGEROUS_KEYS2.has(key)) {
4945
- continue;
4946
- }
4947
- sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
4948
- }
4949
- return sanitized;
4950
- }
4951
- function isPlainObject2(value) {
4952
- return Object.prototype.toString.call(value) === "[object Object]";
4953
- }
4954
5051
 
4955
5052
  // src/singleflight/RedisSingleFlightCoordinator.ts
4956
5053
  var import_node_crypto2 = require("crypto");