layercache 1.2.7 → 1.2.9

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
@@ -528,7 +528,7 @@ function normalizeForSerialization(value) {
528
528
  }
529
529
  function serializeKeyPart(value) {
530
530
  if (typeof value === "string") {
531
- return `s:${value}`;
531
+ return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
532
532
  }
533
533
  if (typeof value === "number") {
534
534
  return `n:${value}`;
@@ -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,576 @@ 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
+ const degraded = results.filter((result) => result.status === "fulfilled");
921
+ if (failures.length === 0) {
922
+ return;
923
+ }
924
+ this.options.onWriteFailures(
925
+ context,
926
+ failures.map((failure) => failure.reason)
927
+ );
928
+ if (failures.length === operations.length) {
929
+ throw new AggregateError(
930
+ failures.map((failure) => failure.reason),
931
+ `${context.action} failed for every cache layer`
932
+ );
933
+ }
934
+ }
935
+ buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
936
+ const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
937
+ const staleWhileRevalidate = this.options.resolveLayerSeconds(
938
+ layer.name,
939
+ writeOptions?.staleWhileRevalidate,
940
+ this.options.globalStaleWhileRevalidate
941
+ );
942
+ const staleIfError = this.options.resolveLayerSeconds(
943
+ layer.name,
944
+ writeOptions?.staleIfError,
945
+ this.options.globalStaleIfError
946
+ );
947
+ const payload = createStoredValueEnvelope({
948
+ kind,
949
+ value,
950
+ freshTtlSeconds: freshTtl,
951
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
952
+ staleIfErrorSeconds: staleIfError,
953
+ now
954
+ });
955
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
956
+ return {
957
+ key,
958
+ value: payload,
959
+ ttl
960
+ };
961
+ }
962
+ };
963
+
964
+ // src/internal/CacheStackMaintenance.ts
965
+ var CacheStackMaintenance = class {
966
+ keyEpochs = /* @__PURE__ */ new Map();
967
+ writeBehindQueue = [];
968
+ writeBehindTimer;
969
+ writeBehindFlushPromise;
970
+ generationCleanupPromise;
971
+ clearEpoch = 0;
972
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
973
+ if (writeStrategy !== "write-behind") {
974
+ return;
975
+ }
976
+ const flushIntervalMs = options?.flushIntervalMs;
977
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
978
+ return;
979
+ }
980
+ this.disposeWriteBehindTimer();
981
+ this.writeBehindTimer = setInterval(() => {
982
+ void flush();
983
+ }, flushIntervalMs);
984
+ this.writeBehindTimer.unref?.();
985
+ }
986
+ disposeWriteBehindTimer() {
987
+ if (!this.writeBehindTimer) {
988
+ return;
989
+ }
990
+ clearInterval(this.writeBehindTimer);
991
+ this.writeBehindTimer = void 0;
992
+ }
993
+ beginClearEpoch() {
994
+ this.clearEpoch += 1;
995
+ this.keyEpochs.clear();
996
+ this.writeBehindQueue.length = 0;
997
+ }
998
+ currentClearEpoch() {
999
+ return this.clearEpoch;
1000
+ }
1001
+ currentKeyEpoch(key) {
1002
+ return this.keyEpochs.get(key) ?? 0;
1003
+ }
1004
+ bumpKeyEpochs(keys) {
1005
+ for (const key of keys) {
1006
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1007
+ }
1008
+ }
1009
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1010
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
1011
+ return true;
1012
+ }
1013
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
1014
+ return true;
1015
+ }
1016
+ return false;
1017
+ }
1018
+ async enqueueWriteBehind(operation, options, flushBatch) {
1019
+ this.writeBehindQueue.push(operation);
1020
+ const batchSize = options?.batchSize ?? 100;
1021
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
1022
+ if (this.writeBehindQueue.length >= batchSize) {
1023
+ await this.flushWriteBehindQueue(options, flushBatch);
1024
+ return;
1025
+ }
1026
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1027
+ await this.flushWriteBehindQueue(options, flushBatch);
1028
+ }
1029
+ }
1030
+ async flushWriteBehindQueue(options, flushBatch) {
1031
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1032
+ await this.writeBehindFlushPromise;
1033
+ return;
1034
+ }
1035
+ const batchSize = options?.batchSize ?? 100;
1036
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1037
+ this.writeBehindFlushPromise = flushBatch(batch);
1038
+ try {
1039
+ await this.writeBehindFlushPromise;
1040
+ } finally {
1041
+ this.writeBehindFlushPromise = void 0;
1042
+ }
1043
+ if (this.writeBehindQueue.length > 0) {
1044
+ await this.flushWriteBehindQueue(options, flushBatch);
1045
+ }
1046
+ }
1047
+ scheduleGenerationCleanup(generation, task, onError) {
1048
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
1049
+ onError(generation, error);
1050
+ });
1051
+ this.generationCleanupPromise = scheduledTask.finally(() => {
1052
+ if (this.generationCleanupPromise === scheduledTask) {
1053
+ this.generationCleanupPromise = void 0;
1054
+ }
1055
+ });
1056
+ }
1057
+ async waitForGenerationCleanup() {
1058
+ await this.generationCleanupPromise;
1059
+ }
1060
+ };
1061
+
1062
+ // src/internal/CacheStackRuntimePolicy.ts
1063
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1064
+ return degradedUntil !== void 0 && degradedUntil > now;
1065
+ }
1066
+ function shouldStartBackgroundRefresh({
1067
+ isDisconnecting,
1068
+ hasRefreshInFlight
1069
+ }) {
1070
+ return !isDisconnecting && !hasRefreshInFlight;
1071
+ }
1072
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1073
+ if (!gracefulDegradation) {
1074
+ return { degrade: false };
1075
+ }
1076
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1077
+ return {
1078
+ degrade: true,
1079
+ degradedUntil: now + retryAfterMs
1080
+ };
1081
+ }
1082
+ function planFreshReadPolicies({
1083
+ stored,
1084
+ hasFetcher,
1085
+ slidingTtl,
1086
+ refreshAheadSeconds
1087
+ }) {
1088
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1089
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1090
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1091
+ return {
1092
+ refreshedStored,
1093
+ refreshedStoredTtl,
1094
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1095
+ };
1096
+ }
1097
+
1098
+ // src/internal/CacheStackSnapshotManager.ts
1099
+ var import_node_crypto = require("crypto");
1100
+ var import_node_fs = require("fs");
1101
+ var import_node_path = __toESM(require("path"), 1);
1102
+
1103
+ // src/internal/CacheSnapshotFile.ts
1104
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1105
+ const relative = path2.relative(realBaseDir, candidatePath);
1106
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1107
+ }
1108
+ async function findExistingAncestor(directory, fs3, path2) {
1109
+ let current = directory;
1110
+ while (true) {
1111
+ try {
1112
+ await fs3.lstat(current);
1113
+ return current;
1114
+ } catch (error) {
1115
+ if (error.code !== "ENOENT") {
1116
+ throw error;
1117
+ }
1118
+ }
1119
+ const parent = path2.dirname(current);
1120
+ if (parent === current) {
1121
+ return current;
1122
+ }
1123
+ current = parent;
1124
+ }
1125
+ }
1126
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
1127
+ if (filePath.length === 0) {
1128
+ throw new Error("filePath must not be empty.");
1129
+ }
1130
+ if (filePath.includes("\0")) {
1131
+ throw new Error("filePath must not contain null bytes.");
1132
+ }
1133
+ const { promises: fs3 } = await import("fs");
1134
+ const path2 = await import("path");
1135
+ const resolved = path2.resolve(filePath);
1136
+ const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1137
+ if (baseDir === false) {
1138
+ return resolved;
1139
+ }
1140
+ await fs3.mkdir(baseDir, { recursive: true });
1141
+ const realBaseDir = await fs3.realpath(baseDir);
1142
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1143
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1144
+ }
1145
+ if (mode === "read") {
1146
+ const realTarget = await fs3.realpath(resolved);
1147
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1148
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1149
+ }
1150
+ return realTarget;
1151
+ }
1152
+ const parentDir = path2.dirname(resolved);
1153
+ const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
1154
+ const realExistingAncestor = await fs3.realpath(existingAncestor);
1155
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1156
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1157
+ }
1158
+ await fs3.mkdir(parentDir, { recursive: true });
1159
+ const realParentDir = await fs3.realpath(parentDir);
1160
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1161
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1162
+ }
1163
+ const targetPath = path2.join(realParentDir, path2.basename(resolved));
1164
+ try {
1165
+ const existing = await fs3.lstat(targetPath);
1166
+ if (existing.isSymbolicLink()) {
1167
+ throw new Error("filePath must not point to a symbolic link.");
1168
+ }
1169
+ } catch (error) {
1170
+ if (error.code !== "ENOENT") {
1171
+ throw error;
1172
+ }
1173
+ }
1174
+ return targetPath;
1175
+ }
1176
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
1177
+ if (byteLimit === false) {
1178
+ return handle.readFile({ encoding: "utf8" });
1179
+ }
1180
+ const chunks = [];
1181
+ let totalBytes = 0;
1182
+ let position = 0;
1183
+ while (true) {
1184
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
1185
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
1186
+ if (bytesRead === 0) {
1187
+ break;
1188
+ }
1189
+ totalBytes += bytesRead;
1190
+ if (totalBytes > byteLimit) {
1191
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
1192
+ }
1193
+ chunks.push(buffer.subarray(0, bytesRead));
1194
+ position += bytesRead;
1195
+ }
1196
+ return Buffer.concat(chunks).toString("utf8");
1197
+ }
1198
+
1199
+ // src/internal/StructuredDataSanitizer.ts
1200
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1201
+ function sanitizeStructuredData(value, options) {
1202
+ return sanitizeValue(value, 0, { count: 0 }, options);
1203
+ }
1204
+ function sanitizeValue(value, depth, state, options) {
1205
+ state.count += 1;
1206
+ if (state.count > options.maxNodes) {
1207
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1208
+ }
1209
+ if (depth > options.maxDepth) {
1210
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1211
+ }
1212
+ if (Array.isArray(value)) {
1213
+ const sanitized2 = [];
1214
+ for (const entry of value) {
1215
+ sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
1216
+ }
1217
+ return sanitized2;
1218
+ }
1219
+ if (!isPlainObject(value)) {
1220
+ return value;
1221
+ }
1222
+ const sanitized = options.createObject?.() ?? {};
1223
+ for (const [key, entry] of Object.entries(value)) {
1224
+ if (DANGEROUS_KEYS.has(key)) {
1225
+ continue;
1226
+ }
1227
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1228
+ }
1229
+ return sanitized;
1230
+ }
1231
+ function isPlainObject(value) {
1232
+ return Object.prototype.toString.call(value) === "[object Object]";
1233
+ }
1234
+
1235
+ // src/internal/CacheStackSnapshotManager.ts
1236
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1237
+ var CacheStackSnapshotManager = class {
1238
+ constructor(options) {
1239
+ this.options = options;
1240
+ }
1241
+ options;
1242
+ async exportState(maxEntries) {
1243
+ const entries = [];
1244
+ await this.visitExportEntries(maxEntries, async (entry) => {
1245
+ entries.push(entry);
1246
+ });
1247
+ return entries;
1248
+ }
1249
+ async importState(entries) {
1250
+ const normalizedEntries = entries.map((entry) => ({
1251
+ key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
1252
+ value: entry.value,
1253
+ ttl: entry.ttl
1254
+ }));
1255
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1256
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1257
+ await Promise.all(
1258
+ batch.map(async (entry) => {
1259
+ await Promise.all(
1260
+ this.options.layers.map(async (layer) => {
1261
+ if (this.options.shouldSkipLayer(layer)) return;
1262
+ try {
1263
+ await layer.set(entry.key, entry.value, entry.ttl);
1264
+ } catch (error) {
1265
+ await this.options.handleLayerFailure(layer, "write", error);
1266
+ }
1267
+ })
1268
+ );
1269
+ await this.options.tagIndex.touch(entry.key);
1270
+ })
1271
+ );
1272
+ }
1273
+ }
1274
+ async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1275
+ const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1276
+ const tempPath = import_node_path.default.join(
1277
+ import_node_path.default.dirname(targetPath),
1278
+ `.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
1279
+ );
1280
+ let handle;
1281
+ try {
1282
+ handle = await import_node_fs.promises.open(tempPath, "wx");
1283
+ const openedHandle = handle;
1284
+ await openedHandle.writeFile("[", "utf8");
1285
+ let wroteAny = false;
1286
+ await this.visitExportEntries(maxEntries, async (entry) => {
1287
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1288
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1289
+ wroteAny = true;
1290
+ });
1291
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1292
+ await openedHandle.close();
1293
+ handle = void 0;
1294
+ await import_node_fs.promises.rename(tempPath, targetPath);
1295
+ } catch (error) {
1296
+ await handle?.close().catch(() => void 0);
1297
+ await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
1298
+ throw error;
1299
+ }
1300
+ }
1301
+ async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1302
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1303
+ const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
1304
+ let raw;
1305
+ try {
1306
+ if (maxBytes !== false) {
1307
+ const stat = await handle.stat();
1308
+ if (stat.size > maxBytes) {
1309
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1310
+ }
1311
+ }
1312
+ raw = await readUtf8HandleWithLimit(handle, maxBytes);
1313
+ } finally {
1314
+ await handle.close();
1315
+ }
1316
+ let parsed;
1317
+ try {
1318
+ parsed = JSON.parse(raw);
1319
+ } catch (cause) {
1320
+ throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1321
+ }
1322
+ if (!this.isCacheSnapshotEntries(parsed)) {
1323
+ throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1324
+ }
1325
+ await this.importState(
1326
+ parsed.map((entry) => ({
1327
+ key: entry.key,
1328
+ value: this.sanitizeSnapshotValue(entry.value),
1329
+ ttl: entry.ttl
1330
+ }))
1331
+ );
1332
+ }
1333
+ async visitExportEntries(maxEntries, visitor) {
1334
+ const exported = /* @__PURE__ */ new Set();
1335
+ for (const layer of this.options.layers) {
1336
+ if (!layer.keys && !layer.forEachKey) {
1337
+ continue;
1338
+ }
1339
+ const visitKey = async (key) => {
1340
+ const exportedKey = this.options.stripQualifiedKey(key);
1341
+ if (exported.has(exportedKey)) {
1342
+ return;
1343
+ }
1344
+ const stored = await this.options.readLayerEntry(layer, key);
1345
+ if (stored === null) {
1346
+ return;
1347
+ }
1348
+ exported.add(exportedKey);
1349
+ if (maxEntries !== false && exported.size > maxEntries) {
1350
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1351
+ }
1352
+ await visitor({
1353
+ key: exportedKey,
1354
+ value: stored,
1355
+ ttl: remainingStoredTtlSeconds(stored)
1356
+ });
1357
+ };
1358
+ if (layer.forEachKey) {
1359
+ await layer.forEachKey(visitKey);
1360
+ continue;
1361
+ }
1362
+ const keys = await layer.keys?.();
1363
+ for (const key of keys ?? []) {
1364
+ await visitKey(key);
1365
+ }
1366
+ }
1367
+ }
1368
+ isCacheSnapshotEntries(value) {
1369
+ return Array.isArray(value) && value.every((entry) => {
1370
+ if (!entry || typeof entry !== "object") {
1371
+ return false;
1372
+ }
1373
+ const candidate = entry;
1374
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1375
+ });
954
1376
  }
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 };
1377
+ sanitizeSnapshotValue(value) {
1378
+ const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1379
+ return sanitizeStructuredData(roundTripped, {
1380
+ label: "Snapshot value",
1381
+ maxDepth: 64,
1382
+ maxNodes: 1e4,
1383
+ createObject: () => /* @__PURE__ */ Object.create(null)
1384
+ });
971
1385
  }
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
- }
1386
+ };
993
1387
 
994
1388
  // src/internal/CacheStackValidation.ts
995
1389
  var MAX_CACHE_KEY_LENGTH = 1024;
@@ -1218,7 +1612,11 @@ var FetchRateLimiter = class {
1218
1612
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
1219
1613
  nextFetcherBucketId = 0;
1220
1614
  drainTimer;
1615
+ isDisposed = false;
1221
1616
  async schedule(options, context, task) {
1617
+ if (this.isDisposed) {
1618
+ throw new Error("FetchRateLimiter has been disposed.");
1619
+ }
1222
1620
  if (!options) {
1223
1621
  return task();
1224
1622
  }
@@ -1241,6 +1639,27 @@ var FetchRateLimiter = class {
1241
1639
  this.drain();
1242
1640
  });
1243
1641
  }
1642
+ dispose() {
1643
+ this.isDisposed = true;
1644
+ if (this.drainTimer) {
1645
+ clearTimeout(this.drainTimer);
1646
+ this.drainTimer = void 0;
1647
+ }
1648
+ for (const bucket of this.buckets.values()) {
1649
+ if (bucket.cleanupTimer) {
1650
+ clearTimeout(bucket.cleanupTimer);
1651
+ bucket.cleanupTimer = void 0;
1652
+ }
1653
+ }
1654
+ for (const queue of this.queuesByBucket.values()) {
1655
+ for (const item of queue) {
1656
+ item.reject(new Error("FetchRateLimiter has been disposed."));
1657
+ }
1658
+ }
1659
+ this.queuesByBucket.clear();
1660
+ this.pendingBuckets.clear();
1661
+ this.buckets.clear();
1662
+ }
1244
1663
  normalize(options) {
1245
1664
  const maxConcurrent = options.maxConcurrent;
1246
1665
  const intervalMs = options.intervalMs;
@@ -1276,6 +1695,9 @@ var FetchRateLimiter = class {
1276
1695
  return "global";
1277
1696
  }
1278
1697
  drain() {
1698
+ if (this.isDisposed) {
1699
+ return;
1700
+ }
1279
1701
  if (this.drainTimer) {
1280
1702
  clearTimeout(this.drainTimer);
1281
1703
  this.drainTimer = void 0;
@@ -1339,7 +1761,13 @@ var FetchRateLimiter = class {
1339
1761
  this.pendingBuckets.add(next.bucketKey);
1340
1762
  }
1341
1763
  this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
1342
- this.drain();
1764
+ if (!this.drainTimer) {
1765
+ this.drainTimer = setTimeout(() => {
1766
+ this.drainTimer = void 0;
1767
+ this.drain();
1768
+ }, 0);
1769
+ this.drainTimer.unref?.();
1770
+ }
1343
1771
  });
1344
1772
  }
1345
1773
  }
@@ -1372,12 +1800,18 @@ var FetchRateLimiter = class {
1372
1800
  }
1373
1801
  }
1374
1802
  bucketState(bucketKey) {
1803
+ if (this.isDisposed) {
1804
+ throw new Error("FetchRateLimiter has been disposed.");
1805
+ }
1375
1806
  const existing = this.buckets.get(bucketKey);
1376
1807
  if (existing) {
1377
1808
  return existing;
1378
1809
  }
1379
1810
  if (this.buckets.size >= MAX_BUCKETS) {
1380
1811
  this.evictIdleBuckets();
1812
+ if (this.buckets.size >= MAX_BUCKETS) {
1813
+ throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
1814
+ }
1381
1815
  }
1382
1816
  const bucket = { active: 0, startedAt: [] };
1383
1817
  this.buckets.set(bucketKey, bucket);
@@ -1830,19 +2264,19 @@ var TagIndex = class {
1830
2264
  if (!this.knownKeys.delete(key)) {
1831
2265
  return;
1832
2266
  }
1833
- const path = [];
2267
+ const path2 = [];
1834
2268
  let node = this.root;
1835
2269
  for (const character of key) {
1836
2270
  const child = node.children.get(character);
1837
2271
  if (!child) {
1838
2272
  return;
1839
2273
  }
1840
- path.push([node, character]);
2274
+ path2.push([node, character]);
1841
2275
  node = child;
1842
2276
  }
1843
2277
  node.terminal = false;
1844
- for (let index = path.length - 1; index >= 0; index -= 1) {
1845
- const entry = path[index];
2278
+ for (let index = path2.length - 1; index >= 0; index -= 1) {
2279
+ const entry = path2[index];
1846
2280
  if (!entry) {
1847
2281
  continue;
1848
2282
  }
@@ -1857,44 +2291,19 @@ var TagIndex = class {
1857
2291
  };
1858
2292
 
1859
2293
  // src/serialization/JsonSerializer.ts
1860
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1861
- var MAX_SANITIZE_NODES = 1e4;
1862
2294
  var JsonSerializer = class {
1863
2295
  serialize(value) {
1864
2296
  return JSON.stringify(value);
1865
2297
  }
1866
2298
  deserialize(payload) {
1867
2299
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1868
- return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
2300
+ return sanitizeStructuredData(JSON.parse(normalized), {
2301
+ label: "JSON payload",
2302
+ maxDepth: 200,
2303
+ maxNodes: 1e4
2304
+ });
1869
2305
  }
1870
2306
  };
1871
- var MAX_SANITIZE_DEPTH = 200;
1872
- function sanitizeJsonValue(value, depth, state) {
1873
- state.count += 1;
1874
- if (state.count > MAX_SANITIZE_NODES) {
1875
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
1876
- }
1877
- if (depth > MAX_SANITIZE_DEPTH) {
1878
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
1879
- }
1880
- if (Array.isArray(value)) {
1881
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
1882
- }
1883
- if (!isPlainObject(value)) {
1884
- return value;
1885
- }
1886
- const sanitized = {};
1887
- for (const [key, entry] of Object.entries(value)) {
1888
- if (DANGEROUS_JSON_KEYS.has(key)) {
1889
- continue;
1890
- }
1891
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
1892
- }
1893
- return sanitized;
1894
- }
1895
- function isPlainObject(value) {
1896
- return Object.prototype.toString.call(value) === "[object Object]";
1897
- }
1898
2307
 
1899
2308
  // src/stampede/StampedeGuard.ts
1900
2309
  var import_async_mutex2 = require("async-mutex");
@@ -1940,7 +2349,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1940
2349
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1941
2350
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1942
2351
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1943
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1944
2352
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1945
2353
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1946
2354
  var DebugLogger = class {
@@ -1997,6 +2405,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
1997
2405
  await this.handleLayerFailure(layer, operation, error);
1998
2406
  }
1999
2407
  });
2408
+ this.invalidation = new CacheStackInvalidationSupport({
2409
+ tagIndex: this.tagIndex,
2410
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2411
+ handleLayerFailure: async (layer, operation, error) => {
2412
+ await this.handleLayerFailure(layer, operation, error);
2413
+ }
2414
+ });
2415
+ this.layerWriter = new CacheStackLayerWriter({
2416
+ layers: this.layers,
2417
+ maintenance: this.maintenance,
2418
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2419
+ shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
2420
+ handleLayerFailure: async (layer, operation, error) => {
2421
+ await this.handleLayerFailure(layer, operation, error);
2422
+ },
2423
+ enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2424
+ resolveFreshTtl: this.resolveFreshTtl.bind(this),
2425
+ resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2426
+ globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2427
+ globalStaleIfError: this.options.staleIfError,
2428
+ writePolicy: this.options.writePolicy,
2429
+ onWriteFailures: (context, failures) => {
2430
+ this.metricsCollector.increment("writeFailures", failures.length);
2431
+ this.logger.debug?.("write-failure", {
2432
+ ...context,
2433
+ failures: failures.map((failure) => this.formatError(failure))
2434
+ });
2435
+ }
2436
+ });
2000
2437
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
2001
2438
  this.logger.warn?.(
2002
2439
  "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 +2449,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2012
2449
  "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
2013
2450
  );
2014
2451
  }
2452
+ this.snapshots = new CacheStackSnapshotManager({
2453
+ layers: this.layers,
2454
+ tagIndex: this.tagIndex,
2455
+ snapshotSerializer: this.snapshotSerializer,
2456
+ readLayerEntry: this.readLayerEntry.bind(this),
2457
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2458
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2459
+ qualifyKey: this.qualifyKey.bind(this),
2460
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
2461
+ validateCacheKey,
2462
+ formatError: this.formatError.bind(this)
2463
+ });
2015
2464
  this.initializeWriteBehind(options.writeBehind);
2016
2465
  this.startup = this.initialize();
2017
2466
  }
@@ -2027,11 +2476,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2027
2476
  keyDiscovery;
2028
2477
  fetchRateLimiter = new FetchRateLimiter();
2029
2478
  snapshotSerializer = new JsonSerializer();
2479
+ invalidation;
2480
+ layerWriter;
2481
+ snapshots;
2030
2482
  backgroundRefreshes = /* @__PURE__ */ new Map();
2483
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
2031
2484
  layerDegradedUntil = /* @__PURE__ */ new Map();
2032
2485
  maintenance = new CacheStackMaintenance();
2033
2486
  ttlResolver;
2034
2487
  circuitBreakerManager;
2488
+ nextOperationId = 0;
2035
2489
  currentGeneration;
2036
2490
  isDisconnecting = false;
2037
2491
  disconnectPromise;
@@ -2042,10 +2496,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
2042
2496
  * and no `fetcher` is provided.
2043
2497
  */
2044
2498
  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);
2499
+ return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
2500
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2501
+ this.validateWriteOptions(options);
2502
+ await this.awaitStartup("get");
2503
+ return this.getPrepared(normalizedKey, fetcher, options);
2504
+ });
2049
2505
  }
2050
2506
  async getPrepared(normalizedKey, fetcher, options) {
2051
2507
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
@@ -2167,23 +2623,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
2167
2623
  * Stores a value in all cache layers. Overwrites any existing value.
2168
2624
  */
2169
2625
  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);
2626
+ await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2627
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2628
+ this.validateWriteOptions(options);
2629
+ await this.awaitStartup("set");
2630
+ await this.storeEntry(normalizedKey, "value", value, options);
2631
+ });
2174
2632
  }
2175
2633
  /**
2176
2634
  * Deletes the key from all layers and publishes an invalidation message.
2177
2635
  */
2178
2636
  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"
2637
+ await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2638
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2639
+ await this.awaitStartup("delete");
2640
+ await this.deleteKeys([normalizedKey]);
2641
+ await this.publishInvalidation({
2642
+ scope: "key",
2643
+ keys: [normalizedKey],
2644
+ sourceId: this.instanceId,
2645
+ operation: "delete"
2646
+ });
2187
2647
  });
2188
2648
  }
2189
2649
  async clear() {
@@ -2216,95 +2676,102 @@ var CacheStack = class extends import_node_events.EventEmitter {
2216
2676
  });
2217
2677
  }
2218
2678
  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) {
2679
+ return this.observeOperation("layercache.mget", void 0, async () => {
2680
+ this.assertActive("mget");
2681
+ if (entries.length === 0) {
2682
+ return [];
2683
+ }
2684
+ const normalizedEntries = entries.map((entry) => ({
2685
+ ...entry,
2686
+ key: this.qualifyKey(validateCacheKey(entry.key))
2687
+ }));
2688
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2689
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2690
+ if (!canFastPath) {
2691
+ await this.awaitStartup("mget");
2692
+ const pendingReads = /* @__PURE__ */ new Map();
2693
+ return Promise.all(
2694
+ normalizedEntries.map((entry) => {
2695
+ const optionsSignature = serializeOptions(entry.options);
2696
+ const existing = pendingReads.get(entry.key);
2697
+ if (!existing) {
2698
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2699
+ pendingReads.set(entry.key, {
2700
+ promise,
2701
+ fetch: entry.fetch,
2702
+ optionsSignature
2703
+ });
2704
+ return promise;
2705
+ }
2706
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2707
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2708
+ }
2709
+ return existing.promise;
2710
+ })
2711
+ );
2712
+ }
2230
2713
  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;
2714
+ const pending = /* @__PURE__ */ new Set();
2715
+ const indexesByKey = /* @__PURE__ */ new Map();
2716
+ const resultsByKey = /* @__PURE__ */ new Map();
2717
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
2718
+ const entry = normalizedEntries[index];
2719
+ if (!entry) continue;
2720
+ const key = entry.key;
2721
+ const indexes = indexesByKey.get(key) ?? [];
2722
+ indexes.push(index);
2723
+ indexesByKey.set(key, indexes);
2724
+ pending.add(key);
2271
2725
  }
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;
2726
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2727
+ const layer = this.layers[layerIndex];
2728
+ if (!layer || this.shouldSkipLayer(layer)) continue;
2729
+ const keys = [...pending];
2730
+ if (keys.length === 0) {
2731
+ break;
2278
2732
  }
2279
- const resolved = resolveStoredValue(stored);
2280
- if (resolved.state === "expired") {
2281
- await layer.delete(key);
2282
- continue;
2733
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2734
+ for (let offset = 0; offset < values.length; offset += 1) {
2735
+ const key = keys[offset];
2736
+ const stored = values[offset];
2737
+ if (!key || stored === null) {
2738
+ continue;
2739
+ }
2740
+ const resolved = resolveStoredValue(stored);
2741
+ if (resolved.state === "expired") {
2742
+ await layer.delete(key);
2743
+ continue;
2744
+ }
2745
+ if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
2746
+ this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2747
+ }
2748
+ await this.tagIndex.touch(key);
2749
+ await this.backfill(key, stored, layerIndex - 1);
2750
+ resultsByKey.set(key, resolved.value);
2751
+ pending.delete(key);
2752
+ this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2283
2753
  }
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
2754
  }
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);
2755
+ if (pending.size > 0) {
2756
+ for (const key of pending) {
2757
+ await this.tagIndex.remove(key);
2758
+ this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2759
+ }
2295
2760
  }
2296
- }
2297
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2761
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2762
+ });
2298
2763
  }
2299
2764
  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);
2765
+ await this.observeOperation("layercache.mset", void 0, async () => {
2766
+ this.assertActive("mset");
2767
+ const normalizedEntries = entries.map((entry) => ({
2768
+ ...entry,
2769
+ key: this.qualifyKey(validateCacheKey(entry.key))
2770
+ }));
2771
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2772
+ await this.awaitStartup("mset");
2773
+ await this.writeBatch(normalizedEntries);
2774
+ });
2308
2775
  }
2309
2776
  async warm(entries, options = {}) {
2310
2777
  this.assertActive("warm");
@@ -2357,40 +2824,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
2357
2824
  return new CacheNamespace(this, prefix);
2358
2825
  }
2359
2826
  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" });
2827
+ await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2828
+ validateTag(tag);
2829
+ await this.awaitStartup("invalidateByTag");
2830
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
2831
+ await this.deleteKeys(keys);
2832
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2833
+ });
2365
2834
  }
2366
2835
  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" });
2836
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2837
+ if (tags.length === 0) {
2838
+ return;
2839
+ }
2840
+ validateTags(tags);
2841
+ await this.awaitStartup("invalidateByTags");
2842
+ const keysByTag = await Promise.all(
2843
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2844
+ );
2845
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2846
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2847
+ await this.deleteKeys(keys);
2848
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2849
+ });
2377
2850
  }
2378
2851
  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" });
2852
+ await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2853
+ validatePattern(pattern);
2854
+ await this.awaitStartup("invalidateByPattern");
2855
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2856
+ this.qualifyPattern(pattern),
2857
+ this.invalidationMaxKeys()
2858
+ );
2859
+ await this.deleteKeys(keys);
2860
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2861
+ });
2387
2862
  }
2388
2863
  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" });
2864
+ await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2865
+ await this.awaitStartup("invalidateByPrefix");
2866
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2867
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2868
+ await this.deleteKeys(keys);
2869
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2870
+ });
2394
2871
  }
2395
2872
  getMetrics() {
2396
2873
  return this.metricsCollector.snapshot;
@@ -2501,95 +2978,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
2501
2978
  }
2502
2979
  async exportState() {
2503
2980
  await this.awaitStartup("exportState");
2504
- const entries = [];
2505
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2506
- entries.push(entry);
2507
- });
2508
- return entries;
2981
+ return this.snapshots.exportState(this.snapshotMaxEntries());
2509
2982
  }
2510
2983
  async importState(entries) {
2511
2984
  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
- }
2985
+ await this.snapshots.importState(entries);
2526
2986
  }
2527
2987
  async persistToFile(filePath) {
2528
2988
  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
- }
2989
+ await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2556
2990
  }
2557
2991
  async restoreFromFile(filePath) {
2558
2992
  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
- );
2993
+ await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2593
2994
  }
2594
2995
  async disconnect() {
2595
2996
  if (!this.disconnectPromise) {
@@ -2599,8 +3000,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
2599
3000
  await this.unsubscribeInvalidation?.();
2600
3001
  await this.flushWriteBehindQueue();
2601
3002
  await this.maintenance.waitForGenerationCleanup();
2602
- await Promise.allSettled([...this.backgroundRefreshes.values()]);
3003
+ for (const key of this.backgroundRefreshAbort.keys()) {
3004
+ this.backgroundRefreshAbort.set(key, true);
3005
+ }
3006
+ await Promise.allSettled(
3007
+ [...this.backgroundRefreshes.values()].map((promise) => {
3008
+ let timer;
3009
+ return Promise.race([
3010
+ promise,
3011
+ new Promise((resolve2) => {
3012
+ timer = setTimeout(resolve2, 5e3);
3013
+ timer.unref?.();
3014
+ })
3015
+ ]).finally(() => {
3016
+ if (timer) clearTimeout(timer);
3017
+ });
3018
+ })
3019
+ );
3020
+ this.backgroundRefreshes.clear();
3021
+ this.backgroundRefreshAbort.clear();
2603
3022
  this.maintenance.disposeWriteBehindTimer();
3023
+ this.fetchRateLimiter.dispose();
2604
3024
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2605
3025
  })();
2606
3026
  }
@@ -2714,7 +3134,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2714
3134
  async storeEntry(key, kind, value, options) {
2715
3135
  const clearEpoch = this.maintenance.currentClearEpoch();
2716
3136
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
2717
- await this.writeAcrossLayers(key, kind, value, options);
3137
+ await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2718
3138
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2719
3139
  return;
2720
3140
  }
@@ -2731,52 +3151,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2731
3151
  }
2732
3152
  }
2733
3153
  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)));
3154
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
2780
3155
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2781
3156
  return;
2782
3157
  }
@@ -2883,58 +3258,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2883
3258
  this.emit("backfill", { key, layer: layer.name });
2884
3259
  }
2885
3260
  }
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
3261
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2939
3262
  return this.ttlResolver.resolveFreshTtl(
2940
3263
  key,
@@ -2962,15 +3285,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
2962
3285
  }
2963
3286
  const clearEpoch = this.maintenance.currentClearEpoch();
2964
3287
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3288
+ this.backgroundRefreshAbort.set(key, false);
2965
3289
  const refresh = (async () => {
2966
3290
  this.metricsCollector.increment("refreshes");
2967
3291
  try {
3292
+ if (this.backgroundRefreshAbort.get(key)) return;
2968
3293
  await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2969
3294
  } catch (error) {
3295
+ if (this.backgroundRefreshAbort.get(key)) return;
2970
3296
  this.metricsCollector.increment("refreshErrors");
2971
3297
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
2972
3298
  } finally {
2973
3299
  this.backgroundRefreshes.delete(key);
3300
+ this.backgroundRefreshAbort.delete(key);
2974
3301
  }
2975
3302
  })();
2976
3303
  this.backgroundRefreshes.set(key, refresh);
@@ -3000,7 +3327,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3000
3327
  return;
3001
3328
  }
3002
3329
  this.maintenance.bumpKeyEpochs(keys);
3003
- await this.deleteKeysFromLayers(this.layers, keys);
3330
+ await this.invalidation.deleteKeysFromLayers(this.layers, keys);
3004
3331
  for (const key of keys) {
3005
3332
  await this.tagIndex.remove(key);
3006
3333
  this.ttlResolver.deleteProfile(key);
@@ -3032,7 +3359,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3032
3359
  }
3033
3360
  const keys = message.keys ?? [];
3034
3361
  this.maintenance.bumpKeyEpochs(keys);
3035
- await this.deleteKeysFromLayers(localLayers, keys);
3362
+ await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3036
3363
  if (message.operation !== "write") {
3037
3364
  for (const key of keys) {
3038
3365
  await this.tagIndex.remove(key);
@@ -3073,7 +3400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3073
3400
  timer.unref?.();
3074
3401
  })
3075
3402
  ]);
3076
- if (result && typeof result === "object" && "kind" in result) {
3403
+ if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
3077
3404
  if (result.kind === "error") {
3078
3405
  throw result.error;
3079
3406
  }
@@ -3089,6 +3416,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
3089
3416
  shouldBroadcastL1Invalidation() {
3090
3417
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3091
3418
  }
3419
+ async observeOperation(name, attributes, execute) {
3420
+ const id = this.nextOperationId;
3421
+ this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
3422
+ this.emit("operation-start", { id, name, attributes });
3423
+ try {
3424
+ const result = await execute();
3425
+ this.emit("operation-end", {
3426
+ id,
3427
+ name,
3428
+ attributes,
3429
+ success: true,
3430
+ result: result === null ? "null" : void 0
3431
+ });
3432
+ return result;
3433
+ } catch (error) {
3434
+ this.emit("operation-end", {
3435
+ id,
3436
+ name,
3437
+ attributes,
3438
+ success: false,
3439
+ error
3440
+ });
3441
+ throw error;
3442
+ }
3443
+ }
3092
3444
  scheduleGenerationCleanup(generation) {
3093
3445
  this.maintenance.scheduleGenerationCleanup(
3094
3446
  generation,
@@ -3144,37 +3496,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3144
3496
  });
3145
3497
  this.emitError("write-behind", { failed: failures.length, total: batch.length });
3146
3498
  }
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
3499
  qualifyKey(key) {
3179
3500
  return qualifyGenerationKey(key, this.currentGeneration);
3180
3501
  }
@@ -3184,32 +3505,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3184
3505
  stripQualifiedKey(key) {
3185
3506
  return stripGenerationPrefix(key, this.currentGeneration);
3186
3507
  }
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
3508
  validateConfiguration() {
3214
3509
  if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
3215
3510
  throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
@@ -3340,18 +3635,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3340
3635
  this.emit("error", { operation, ...context });
3341
3636
  }
3342
3637
  }
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
3638
  snapshotMaxBytes() {
3356
3639
  return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3357
3640
  }
@@ -3361,62 +3644,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3361
3644
  invalidationMaxKeys() {
3362
3645
  return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3363
3646
  }
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
3647
  };
3421
3648
 
3422
3649
  // src/invalidation/RedisInvalidationBus.ts
@@ -3427,6 +3654,7 @@ var RedisInvalidationBus = class {
3427
3654
  logger;
3428
3655
  handlers = /* @__PURE__ */ new Set();
3429
3656
  sharedListener;
3657
+ subscribePromise;
3430
3658
  constructor(options) {
3431
3659
  this.publisher = options.publisher;
3432
3660
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
@@ -3434,15 +3662,27 @@ var RedisInvalidationBus = class {
3434
3662
  this.logger = options.logger;
3435
3663
  }
3436
3664
  async subscribe(handler) {
3437
- if (this.handlers.size === 0) {
3438
- const listener = (_channel, payload) => {
3439
- void this.dispatchToHandlers(payload);
3440
- };
3441
- this.sharedListener = listener;
3442
- this.subscriber.on("message", listener);
3443
- await this.subscriber.subscribe(this.channel);
3665
+ const previousPromise = this.subscribePromise;
3666
+ let resolveThis;
3667
+ this.subscribePromise = new Promise((resolve2) => {
3668
+ resolveThis = resolve2;
3669
+ });
3670
+ if (previousPromise) {
3671
+ await previousPromise;
3672
+ }
3673
+ try {
3674
+ if (this.handlers.size === 0) {
3675
+ const listener = (_channel, payload) => {
3676
+ void this.dispatchToHandlers(payload);
3677
+ };
3678
+ this.sharedListener = listener;
3679
+ this.subscriber.on("message", listener);
3680
+ await this.subscriber.subscribe(this.channel);
3681
+ }
3682
+ this.handlers.add(handler);
3683
+ } finally {
3684
+ resolveThis();
3444
3685
  }
3445
- this.handlers.add(handler);
3446
3686
  return async () => {
3447
3687
  this.handlers.delete(handler);
3448
3688
  if (this.handlers.size === 0 && this.sharedListener) {
@@ -3458,7 +3698,12 @@ var RedisInvalidationBus = class {
3458
3698
  async dispatchToHandlers(payload) {
3459
3699
  let message;
3460
3700
  try {
3461
- const parsed = sanitizeJsonValue2(JSON.parse(payload));
3701
+ const parsed = sanitizeStructuredData(JSON.parse(payload), {
3702
+ label: "Invalidation payload",
3703
+ maxDepth: 64,
3704
+ maxNodes: 1e4,
3705
+ createObject: () => /* @__PURE__ */ Object.create(null)
3706
+ });
3462
3707
  if (!this.isInvalidationMessage(parsed)) {
3463
3708
  throw new Error("Invalid invalidation payload shape.");
3464
3709
  }
@@ -3495,31 +3740,6 @@ var RedisInvalidationBus = class {
3495
3740
  console.error(`[layercache] ${message}`, error);
3496
3741
  }
3497
3742
  };
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
3743
 
3524
3744
  // src/invalidation/RedisTagIndex.ts
3525
3745
  var RedisTagIndex = class {
@@ -3888,65 +4108,52 @@ function normalizeUrl2(url) {
3888
4108
  }
3889
4109
 
3890
4110
  // src/integrations/opentelemetry.ts
4111
+ var MAX_SPANS = 1e4;
3891
4112
  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)
3902
- };
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;
4113
+ const spans = /* @__PURE__ */ new Map();
4114
+ const onStart = (event) => {
4115
+ try {
4116
+ if (spans.size >= MAX_SPANS) {
4117
+ const oldest = spans.keys().next().value;
4118
+ if (oldest !== void 0) {
4119
+ spans.get(oldest)?.end();
4120
+ spans.delete(oldest);
4121
+ }
4122
+ }
4123
+ spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
4124
+ } catch {
3929
4125
  }
3930
4126
  };
3931
- }
3932
- function instrument(name, tracer, method, attributes) {
3933
- return (async (...args) => {
3934
- const span = tracer.startSpan(name, { attributes: attributes?.(args) });
4127
+ const onEnd = (event) => {
4128
+ const span = spans.get(event.id);
4129
+ if (!span) {
4130
+ return;
4131
+ }
4132
+ spans.delete(event.id);
3935
4133
  try {
3936
- const result = await method(...args);
3937
- span.setAttribute?.("layercache.success", true);
3938
- if (result === null) {
3939
- span.setAttribute?.("layercache.result", "null");
4134
+ span.setAttribute?.("layercache.success", event.success);
4135
+ if (event.result) {
4136
+ span.setAttribute?.("layercache.result", event.result);
3940
4137
  }
3941
- return result;
3942
- } catch (error) {
3943
- span.setAttribute?.("layercache.success", false);
3944
- span.recordException?.(error);
3945
- throw error;
3946
- } finally {
3947
- span.end();
4138
+ if (event.error !== void 0) {
4139
+ span.recordException?.(event.error);
4140
+ }
4141
+ } catch {
3948
4142
  }
3949
- });
4143
+ span.end();
4144
+ };
4145
+ cache.on("operation-start", onStart);
4146
+ cache.on("operation-end", onEnd);
4147
+ return {
4148
+ uninstall() {
4149
+ cache.off("operation-start", onStart);
4150
+ cache.off("operation-end", onEnd);
4151
+ for (const span of spans.values()) {
4152
+ span.end();
4153
+ }
4154
+ spans.clear();
4155
+ }
4156
+ };
3950
4157
  }
3951
4158
 
3952
4159
  // src/integrations/trpc.ts
@@ -4497,9 +4704,9 @@ var RedisLayer = class {
4497
4704
  };
4498
4705
 
4499
4706
  // src/layers/DiskLayer.ts
4500
- var import_node_crypto = require("crypto");
4501
- var import_node_fs = require("fs");
4502
- var import_node_path = require("path");
4707
+ var import_node_crypto2 = require("crypto");
4708
+ var import_node_fs2 = require("fs");
4709
+ var import_node_path2 = require("path");
4503
4710
  var FILE_SCAN_CONCURRENCY = 32;
4504
4711
  var DiskLayer = class {
4505
4712
  name;
@@ -4542,7 +4749,7 @@ var DiskLayer = class {
4542
4749
  }
4543
4750
  async set(key, value, ttl = this.defaultTtl) {
4544
4751
  await this.enqueueWrite(async () => {
4545
- await import_node_fs.promises.mkdir(this.directory, { recursive: true });
4752
+ await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
4546
4753
  const entry = {
4547
4754
  key,
4548
4755
  value,
@@ -4550,10 +4757,10 @@ var DiskLayer = class {
4550
4757
  };
4551
4758
  const payload = this.serializer.serialize(entry);
4552
4759
  const targetPath = this.keyToPath(key);
4553
- const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
4760
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
4554
4761
  try {
4555
- await import_node_fs.promises.writeFile(tempPath, payload);
4556
- await import_node_fs.promises.rename(tempPath, targetPath);
4762
+ await import_node_fs2.promises.writeFile(tempPath, payload);
4763
+ await import_node_fs2.promises.rename(tempPath, targetPath);
4557
4764
  } catch (error) {
4558
4765
  await this.safeDelete(tempPath);
4559
4766
  throw error;
@@ -4607,12 +4814,12 @@ var DiskLayer = class {
4607
4814
  await this.enqueueWrite(async () => {
4608
4815
  let entries;
4609
4816
  try {
4610
- entries = await import_node_fs.promises.readdir(this.directory);
4817
+ entries = await import_node_fs2.promises.readdir(this.directory);
4611
4818
  } catch {
4612
4819
  return;
4613
4820
  }
4614
4821
  await this.deletePathsWithConcurrency(
4615
- entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
4822
+ entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
4616
4823
  );
4617
4824
  });
4618
4825
  }
@@ -4641,7 +4848,7 @@ var DiskLayer = class {
4641
4848
  }
4642
4849
  async ping() {
4643
4850
  try {
4644
- await import_node_fs.promises.mkdir(this.directory, { recursive: true });
4851
+ await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
4645
4852
  return true;
4646
4853
  } catch {
4647
4854
  return false;
@@ -4650,8 +4857,8 @@ var DiskLayer = class {
4650
4857
  async dispose() {
4651
4858
  }
4652
4859
  keyToPath(key) {
4653
- const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
4654
- return (0, import_node_path.join)(this.directory, `${hash}.lc`);
4860
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
4861
+ return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
4655
4862
  }
4656
4863
  resolveDirectory(directory) {
4657
4864
  if (typeof directory !== "string" || directory.trim().length === 0) {
@@ -4660,7 +4867,7 @@ var DiskLayer = class {
4660
4867
  if (directory.includes("\0")) {
4661
4868
  throw new Error("DiskLayer.directory must not contain null bytes.");
4662
4869
  }
4663
- return (0, import_node_path.resolve)(directory);
4870
+ return (0, import_node_path2.resolve)(directory);
4664
4871
  }
4665
4872
  normalizeMaxFiles(maxFiles) {
4666
4873
  if (maxFiles === void 0) {
@@ -4684,7 +4891,7 @@ var DiskLayer = class {
4684
4891
  async readEntryFile(filePath) {
4685
4892
  let handle;
4686
4893
  try {
4687
- handle = await import_node_fs.promises.open(filePath, "r");
4894
+ handle = await import_node_fs2.promises.open(filePath, "r");
4688
4895
  return await this.readHandleWithLimit(handle);
4689
4896
  } catch {
4690
4897
  await this.safeDelete(filePath);
@@ -4724,7 +4931,7 @@ var DiskLayer = class {
4724
4931
  async scanEntries(visitor) {
4725
4932
  let entries;
4726
4933
  try {
4727
- entries = await import_node_fs.promises.readdir(this.directory);
4934
+ entries = await import_node_fs2.promises.readdir(this.directory);
4728
4935
  } catch {
4729
4936
  return;
4730
4937
  }
@@ -4740,7 +4947,7 @@ var DiskLayer = class {
4740
4947
  if (name === void 0) {
4741
4948
  return;
4742
4949
  }
4743
- const filePath = (0, import_node_path.join)(this.directory, name);
4950
+ const filePath = (0, import_node_path2.join)(this.directory, name);
4744
4951
  const raw = await this.readEntryFile(filePath);
4745
4952
  if (raw === null) {
4746
4953
  continue;
@@ -4787,7 +4994,7 @@ var DiskLayer = class {
4787
4994
  }
4788
4995
  async safeDelete(filePath) {
4789
4996
  try {
4790
- await import_node_fs.promises.unlink(filePath);
4997
+ await import_node_fs2.promises.unlink(filePath);
4791
4998
  } catch {
4792
4999
  }
4793
5000
  }
@@ -4805,7 +5012,7 @@ var DiskLayer = class {
4805
5012
  }
4806
5013
  let entries;
4807
5014
  try {
4808
- entries = await import_node_fs.promises.readdir(this.directory);
5015
+ entries = await import_node_fs2.promises.readdir(this.directory);
4809
5016
  } catch {
4810
5017
  return;
4811
5018
  }
@@ -4815,9 +5022,9 @@ var DiskLayer = class {
4815
5022
  }
4816
5023
  const withStats = await Promise.all(
4817
5024
  lcFiles.map(async (name) => {
4818
- const filePath = (0, import_node_path.join)(this.directory, name);
5025
+ const filePath = (0, import_node_path2.join)(this.directory, name);
4819
5026
  try {
4820
- const stat = await import_node_fs.promises.stat(filePath);
5027
+ const stat = await import_node_fs2.promises.stat(filePath);
4821
5028
  return { filePath, mtimeMs: stat.mtimeMs };
4822
5029
  } catch {
4823
5030
  return { filePath, mtimeMs: 0 };
@@ -4913,47 +5120,22 @@ var MemcachedLayer = class {
4913
5120
 
4914
5121
  // src/serialization/MsgpackSerializer.ts
4915
5122
  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
5123
  var MsgpackSerializer = class {
4920
5124
  serialize(value) {
4921
5125
  return Buffer.from((0, import_msgpack.encode)(value));
4922
5126
  }
4923
5127
  deserialize(payload) {
4924
5128
  const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4925
- return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
5129
+ return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
5130
+ label: "MessagePack payload",
5131
+ maxDepth: 64,
5132
+ maxNodes: 1e4
5133
+ });
4926
5134
  }
4927
5135
  };
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
5136
 
4955
5137
  // src/singleflight/RedisSingleFlightCoordinator.ts
4956
- var import_node_crypto2 = require("crypto");
5138
+ var import_node_crypto3 = require("crypto");
4957
5139
  var RELEASE_SCRIPT = `
4958
5140
  if redis.call("get", KEYS[1]) == ARGV[1] then
4959
5141
  return redis.call("del", KEYS[1])
@@ -4975,7 +5157,7 @@ var RedisSingleFlightCoordinator = class {
4975
5157
  }
4976
5158
  async execute(key, options, worker, waiter) {
4977
5159
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
4978
- const token = (0, import_node_crypto2.randomUUID)();
5160
+ const token = (0, import_node_crypto3.randomUUID)();
4979
5161
  const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
4980
5162
  if (acquired === "OK") {
4981
5163
  const renewTimer = this.startLeaseRenewal(lockKey, token, options);