layercache 1.2.7 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -716,102 +716,6 @@ function createInstanceId() {
716
716
  return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
717
717
  }
718
718
 
719
- // ../../src/internal/CacheSnapshotFile.ts
720
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
721
- const relative = path.relative(realBaseDir, candidatePath);
722
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
723
- }
724
- async function findExistingAncestor(directory, fs, path) {
725
- let current = directory;
726
- while (true) {
727
- try {
728
- await fs.lstat(current);
729
- return current;
730
- } catch (error) {
731
- if (error.code !== "ENOENT") {
732
- throw error;
733
- }
734
- }
735
- const parent = path.dirname(current);
736
- if (parent === current) {
737
- return current;
738
- }
739
- current = parent;
740
- }
741
- }
742
- async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
743
- if (filePath.length === 0) {
744
- throw new Error("filePath must not be empty.");
745
- }
746
- if (filePath.includes("\0")) {
747
- throw new Error("filePath must not contain null bytes.");
748
- }
749
- const { promises: fs } = await import("fs");
750
- const path = await import("path");
751
- const resolved = path.resolve(filePath);
752
- const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
753
- if (baseDir === false) {
754
- return resolved;
755
- }
756
- await fs.mkdir(baseDir, { recursive: true });
757
- const realBaseDir = await fs.realpath(baseDir);
758
- if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
759
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
760
- }
761
- if (mode === "read") {
762
- const realTarget = await fs.realpath(resolved);
763
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
764
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
765
- }
766
- return realTarget;
767
- }
768
- const parentDir = path.dirname(resolved);
769
- const existingAncestor = await findExistingAncestor(parentDir, fs, path);
770
- const realExistingAncestor = await fs.realpath(existingAncestor);
771
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
772
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
773
- }
774
- await fs.mkdir(parentDir, { recursive: true });
775
- const realParentDir = await fs.realpath(parentDir);
776
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
777
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
778
- }
779
- const targetPath = path.join(realParentDir, path.basename(resolved));
780
- try {
781
- const existing = await fs.lstat(targetPath);
782
- if (existing.isSymbolicLink()) {
783
- throw new Error("filePath must not point to a symbolic link.");
784
- }
785
- } catch (error) {
786
- if (error.code !== "ENOENT") {
787
- throw error;
788
- }
789
- }
790
- return targetPath;
791
- }
792
- async function readUtf8HandleWithLimit(handle, byteLimit) {
793
- if (byteLimit === false) {
794
- return handle.readFile({ encoding: "utf8" });
795
- }
796
- const chunks = [];
797
- let totalBytes = 0;
798
- let position = 0;
799
- while (true) {
800
- const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
801
- const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
802
- if (bytesRead === 0) {
803
- break;
804
- }
805
- totalBytes += bytesRead;
806
- if (totalBytes > byteLimit) {
807
- throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
808
- }
809
- chunks.push(buffer.subarray(0, bytesRead));
810
- position += bytesRead;
811
- }
812
- return Buffer.concat(chunks).toString("utf8");
813
- }
814
-
815
719
  // ../../src/internal/CacheStackGeneration.ts
816
720
  var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
817
721
  function generationPrefix(generation) {
@@ -859,102 +763,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
859
763
  return batches;
860
764
  }
861
765
 
862
- // ../../src/internal/CacheStackMaintenance.ts
863
- var CacheStackMaintenance = class {
864
- keyEpochs = /* @__PURE__ */ new Map();
865
- writeBehindQueue = [];
866
- writeBehindTimer;
867
- writeBehindFlushPromise;
868
- generationCleanupPromise;
869
- clearEpoch = 0;
870
- initializeWriteBehindTimer(writeStrategy, options, flush) {
871
- if (writeStrategy !== "write-behind") {
872
- return;
873
- }
874
- const flushIntervalMs = options?.flushIntervalMs;
875
- if (!flushIntervalMs || flushIntervalMs <= 0) {
876
- return;
877
- }
878
- this.disposeWriteBehindTimer();
879
- this.writeBehindTimer = setInterval(() => {
880
- void flush();
881
- }, flushIntervalMs);
882
- this.writeBehindTimer.unref?.();
766
+ // ../../src/internal/CacheStackInvalidationSupport.ts
767
+ var CacheStackInvalidationSupport = class {
768
+ constructor(options) {
769
+ this.options = options;
883
770
  }
884
- disposeWriteBehindTimer() {
885
- if (!this.writeBehindTimer) {
886
- return;
771
+ options;
772
+ async collectKeysForTag(tag, maxKeys) {
773
+ const keys = /* @__PURE__ */ new Set();
774
+ if (this.options.tagIndex.forEachKeyForTag) {
775
+ await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
776
+ keys.add(key);
777
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
778
+ });
779
+ return [...keys];
887
780
  }
888
- clearInterval(this.writeBehindTimer);
889
- this.writeBehindTimer = void 0;
890
- }
891
- beginClearEpoch() {
892
- this.clearEpoch += 1;
893
- this.keyEpochs.clear();
894
- this.writeBehindQueue.length = 0;
895
- }
896
- currentClearEpoch() {
897
- return this.clearEpoch;
898
- }
899
- currentKeyEpoch(key) {
900
- return this.keyEpochs.get(key) ?? 0;
901
- }
902
- bumpKeyEpochs(keys) {
903
- for (const key of keys) {
904
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
781
+ for (const key of await this.options.tagIndex.keysForTag(tag)) {
782
+ keys.add(key);
783
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
905
784
  }
785
+ return [...keys];
906
786
  }
907
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
908
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
909
- return true;
910
- }
911
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
912
- return true;
787
+ intersectKeys(groups) {
788
+ if (groups.length === 0) {
789
+ return [];
913
790
  }
914
- return false;
791
+ const [firstGroup, ...rest] = groups;
792
+ const restSets = rest.map((group) => new Set(group));
793
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
915
794
  }
916
- async enqueueWriteBehind(operation, options, flushBatch) {
917
- this.writeBehindQueue.push(operation);
918
- const batchSize = options?.batchSize ?? 100;
919
- const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
920
- if (this.writeBehindQueue.length >= batchSize) {
921
- await this.flushWriteBehindQueue(options, flushBatch);
922
- return;
923
- }
924
- if (this.writeBehindQueue.length >= maxQueueSize) {
925
- await this.flushWriteBehindQueue(options, flushBatch);
926
- }
795
+ async deleteKeysFromLayers(layers, keys) {
796
+ await Promise.all(
797
+ layers.map(async (layer) => {
798
+ if (this.options.shouldSkipLayer(layer)) {
799
+ return;
800
+ }
801
+ if (layer.deleteMany) {
802
+ try {
803
+ await layer.deleteMany(keys);
804
+ } catch (error) {
805
+ await this.options.handleLayerFailure(layer, "delete", error);
806
+ }
807
+ return;
808
+ }
809
+ await Promise.all(
810
+ keys.map(async (key) => {
811
+ try {
812
+ await layer.delete(key);
813
+ } catch (error) {
814
+ await this.options.handleLayerFailure(layer, "delete", error);
815
+ }
816
+ })
817
+ );
818
+ })
819
+ );
927
820
  }
928
- async flushWriteBehindQueue(options, flushBatch) {
929
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
930
- await this.writeBehindFlushPromise;
931
- return;
932
- }
933
- const batchSize = options?.batchSize ?? 100;
934
- const batch = this.writeBehindQueue.splice(0, batchSize);
935
- this.writeBehindFlushPromise = flushBatch(batch);
936
- try {
937
- await this.writeBehindFlushPromise;
938
- } finally {
939
- this.writeBehindFlushPromise = void 0;
940
- }
941
- if (this.writeBehindQueue.length > 0) {
942
- await this.flushWriteBehindQueue(options, flushBatch);
821
+ assertWithinInvalidationKeyLimit(size, maxKeys) {
822
+ if (maxKeys !== false && size > maxKeys) {
823
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
943
824
  }
944
825
  }
945
- scheduleGenerationCleanup(generation, task, onError) {
946
- const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
947
- onError(generation, error);
948
- });
949
- this.generationCleanupPromise = scheduledTask.finally(() => {
950
- if (this.generationCleanupPromise === scheduledTask) {
951
- this.generationCleanupPromise = void 0;
952
- }
953
- });
954
- }
955
- async waitForGenerationCleanup() {
956
- await this.generationCleanupPromise;
957
- }
958
826
  };
959
827
 
960
828
  // ../../src/internal/StoredValue.ts
@@ -1084,72 +952,545 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
1084
952
  if (!isStoredValueEnvelope(stored)) {
1085
953
  return stored;
1086
954
  }
1087
- return createStoredValueEnvelope({
1088
- kind: stored.kind,
1089
- value: stored.value,
1090
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1091
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1092
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1093
- now
1094
- });
1095
- }
1096
- function maxExpiry(stored) {
1097
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1098
- (value) => value !== null
1099
- );
1100
- if (values.length === 0) {
1101
- return null;
955
+ return createStoredValueEnvelope({
956
+ kind: stored.kind,
957
+ value: stored.value,
958
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
959
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
960
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
961
+ now
962
+ });
963
+ }
964
+ function maxExpiry(stored) {
965
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
966
+ (value) => value !== null
967
+ );
968
+ if (values.length === 0) {
969
+ return null;
970
+ }
971
+ return Math.max(...values);
972
+ }
973
+ function normalizePositiveSeconds(value) {
974
+ if (!value || value <= 0) {
975
+ return void 0;
976
+ }
977
+ return value;
978
+ }
979
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
980
+ if (value == null) {
981
+ return true;
982
+ }
983
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
984
+ }
985
+
986
+ // ../../src/internal/CacheStackLayerWriter.ts
987
+ var CacheStackLayerWriter = class {
988
+ constructor(options) {
989
+ this.options = options;
990
+ }
991
+ options;
992
+ async writeAcrossLayers(key, kind, value, writeOptions) {
993
+ const now = Date.now();
994
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
995
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
996
+ const immediateOperations = [];
997
+ const deferredOperations = [];
998
+ for (const layer of this.options.layers) {
999
+ const operation = async () => {
1000
+ if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
1001
+ return;
1002
+ }
1003
+ if (this.options.shouldSkipLayer(layer)) {
1004
+ return;
1005
+ }
1006
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
1007
+ try {
1008
+ await layer.set(entry.key, entry.value, entry.ttl);
1009
+ } catch (error) {
1010
+ await this.options.handleLayerFailure(layer, "write", error);
1011
+ }
1012
+ };
1013
+ if (this.options.shouldWriteBehind(layer)) {
1014
+ deferredOperations.push(operation);
1015
+ } else {
1016
+ immediateOperations.push(operation);
1017
+ }
1018
+ }
1019
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1020
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1021
+ }
1022
+ async writeBatch(entries) {
1023
+ const now = Date.now();
1024
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1025
+ const entryEpochs = new Map(
1026
+ entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
1027
+ );
1028
+ const entriesByLayer = /* @__PURE__ */ new Map();
1029
+ const immediateOperations = [];
1030
+ const deferredOperations = [];
1031
+ for (const entry of entries) {
1032
+ for (const layer of this.options.layers) {
1033
+ if (this.options.shouldSkipLayer(layer)) {
1034
+ continue;
1035
+ }
1036
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1037
+ const bucket = entriesByLayer.get(layer) ?? [];
1038
+ bucket.push(layerEntry);
1039
+ entriesByLayer.set(layer, bucket);
1040
+ }
1041
+ }
1042
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1043
+ const operation = async () => {
1044
+ if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
1045
+ return;
1046
+ }
1047
+ const activeEntries = layerEntries.filter(
1048
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
1049
+ );
1050
+ if (activeEntries.length === 0) {
1051
+ return;
1052
+ }
1053
+ try {
1054
+ if (layer.setMany) {
1055
+ await layer.setMany(activeEntries);
1056
+ return;
1057
+ }
1058
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1059
+ } catch (error) {
1060
+ await this.options.handleLayerFailure(layer, "write", error);
1061
+ }
1062
+ };
1063
+ if (this.options.shouldWriteBehind(layer)) {
1064
+ deferredOperations.push(operation);
1065
+ } else {
1066
+ immediateOperations.push(operation);
1067
+ }
1068
+ }
1069
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1070
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1071
+ return { clearEpoch, entryEpochs };
1072
+ }
1073
+ async executeLayerOperations(operations, context) {
1074
+ if (this.options.writePolicy !== "best-effort") {
1075
+ await Promise.all(operations.map((operation) => operation()));
1076
+ return;
1077
+ }
1078
+ const results = await Promise.allSettled(operations.map((operation) => operation()));
1079
+ const failures = results.filter((result) => result.status === "rejected");
1080
+ if (failures.length === 0) {
1081
+ return;
1082
+ }
1083
+ this.options.onWriteFailures(
1084
+ context,
1085
+ failures.map((failure) => failure.reason)
1086
+ );
1087
+ if (failures.length === operations.length) {
1088
+ throw new AggregateError(
1089
+ failures.map((failure) => failure.reason),
1090
+ `${context.action} failed for every cache layer`
1091
+ );
1092
+ }
1093
+ }
1094
+ buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
1095
+ const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
1096
+ const staleWhileRevalidate = this.options.resolveLayerSeconds(
1097
+ layer.name,
1098
+ writeOptions?.staleWhileRevalidate,
1099
+ this.options.globalStaleWhileRevalidate
1100
+ );
1101
+ const staleIfError = this.options.resolveLayerSeconds(
1102
+ layer.name,
1103
+ writeOptions?.staleIfError,
1104
+ this.options.globalStaleIfError
1105
+ );
1106
+ const payload = createStoredValueEnvelope({
1107
+ kind,
1108
+ value,
1109
+ freshTtlSeconds: freshTtl,
1110
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
1111
+ staleIfErrorSeconds: staleIfError,
1112
+ now
1113
+ });
1114
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1115
+ return {
1116
+ key,
1117
+ value: payload,
1118
+ ttl
1119
+ };
1120
+ }
1121
+ };
1122
+
1123
+ // ../../src/internal/CacheStackMaintenance.ts
1124
+ var CacheStackMaintenance = class {
1125
+ keyEpochs = /* @__PURE__ */ new Map();
1126
+ writeBehindQueue = [];
1127
+ writeBehindTimer;
1128
+ writeBehindFlushPromise;
1129
+ generationCleanupPromise;
1130
+ clearEpoch = 0;
1131
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
1132
+ if (writeStrategy !== "write-behind") {
1133
+ return;
1134
+ }
1135
+ const flushIntervalMs = options?.flushIntervalMs;
1136
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1137
+ return;
1138
+ }
1139
+ this.disposeWriteBehindTimer();
1140
+ this.writeBehindTimer = setInterval(() => {
1141
+ void flush();
1142
+ }, flushIntervalMs);
1143
+ this.writeBehindTimer.unref?.();
1144
+ }
1145
+ disposeWriteBehindTimer() {
1146
+ if (!this.writeBehindTimer) {
1147
+ return;
1148
+ }
1149
+ clearInterval(this.writeBehindTimer);
1150
+ this.writeBehindTimer = void 0;
1151
+ }
1152
+ beginClearEpoch() {
1153
+ this.clearEpoch += 1;
1154
+ this.keyEpochs.clear();
1155
+ this.writeBehindQueue.length = 0;
1156
+ }
1157
+ currentClearEpoch() {
1158
+ return this.clearEpoch;
1159
+ }
1160
+ currentKeyEpoch(key) {
1161
+ return this.keyEpochs.get(key) ?? 0;
1162
+ }
1163
+ bumpKeyEpochs(keys) {
1164
+ for (const key of keys) {
1165
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1166
+ }
1167
+ }
1168
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1169
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
1170
+ return true;
1171
+ }
1172
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
1173
+ return true;
1174
+ }
1175
+ return false;
1176
+ }
1177
+ async enqueueWriteBehind(operation, options, flushBatch) {
1178
+ this.writeBehindQueue.push(operation);
1179
+ const batchSize = options?.batchSize ?? 100;
1180
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
1181
+ if (this.writeBehindQueue.length >= batchSize) {
1182
+ await this.flushWriteBehindQueue(options, flushBatch);
1183
+ return;
1184
+ }
1185
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1186
+ await this.flushWriteBehindQueue(options, flushBatch);
1187
+ }
1188
+ }
1189
+ async flushWriteBehindQueue(options, flushBatch) {
1190
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1191
+ await this.writeBehindFlushPromise;
1192
+ return;
1193
+ }
1194
+ const batchSize = options?.batchSize ?? 100;
1195
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1196
+ this.writeBehindFlushPromise = flushBatch(batch);
1197
+ try {
1198
+ await this.writeBehindFlushPromise;
1199
+ } finally {
1200
+ this.writeBehindFlushPromise = void 0;
1201
+ }
1202
+ if (this.writeBehindQueue.length > 0) {
1203
+ await this.flushWriteBehindQueue(options, flushBatch);
1204
+ }
1205
+ }
1206
+ scheduleGenerationCleanup(generation, task, onError) {
1207
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
1208
+ onError(generation, error);
1209
+ });
1210
+ this.generationCleanupPromise = scheduledTask.finally(() => {
1211
+ if (this.generationCleanupPromise === scheduledTask) {
1212
+ this.generationCleanupPromise = void 0;
1213
+ }
1214
+ });
1215
+ }
1216
+ async waitForGenerationCleanup() {
1217
+ await this.generationCleanupPromise;
1218
+ }
1219
+ };
1220
+
1221
+ // ../../src/internal/CacheStackRuntimePolicy.ts
1222
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1223
+ return degradedUntil !== void 0 && degradedUntil > now;
1224
+ }
1225
+ function shouldStartBackgroundRefresh({
1226
+ isDisconnecting,
1227
+ hasRefreshInFlight
1228
+ }) {
1229
+ return !isDisconnecting && !hasRefreshInFlight;
1230
+ }
1231
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1232
+ if (!gracefulDegradation) {
1233
+ return { degrade: false };
1234
+ }
1235
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1236
+ return {
1237
+ degrade: true,
1238
+ degradedUntil: now + retryAfterMs
1239
+ };
1240
+ }
1241
+ function planFreshReadPolicies({
1242
+ stored,
1243
+ hasFetcher,
1244
+ slidingTtl,
1245
+ refreshAheadSeconds
1246
+ }) {
1247
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1248
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1249
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1250
+ return {
1251
+ refreshedStored,
1252
+ refreshedStoredTtl,
1253
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1254
+ };
1255
+ }
1256
+
1257
+ // ../../src/internal/CacheStackSnapshotManager.ts
1258
+ import { constants, promises as fs } from "fs";
1259
+ import path from "path";
1260
+
1261
+ // ../../src/internal/CacheSnapshotFile.ts
1262
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1263
+ const relative = path2.relative(realBaseDir, candidatePath);
1264
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1265
+ }
1266
+ async function findExistingAncestor(directory, fs2, path2) {
1267
+ let current = directory;
1268
+ while (true) {
1269
+ try {
1270
+ await fs2.lstat(current);
1271
+ return current;
1272
+ } catch (error) {
1273
+ if (error.code !== "ENOENT") {
1274
+ throw error;
1275
+ }
1276
+ }
1277
+ const parent = path2.dirname(current);
1278
+ if (parent === current) {
1279
+ return current;
1280
+ }
1281
+ current = parent;
1282
+ }
1283
+ }
1284
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
1285
+ if (filePath.length === 0) {
1286
+ throw new Error("filePath must not be empty.");
1287
+ }
1288
+ if (filePath.includes("\0")) {
1289
+ throw new Error("filePath must not contain null bytes.");
1290
+ }
1291
+ const { promises: fs2 } = await import("fs");
1292
+ const path2 = await import("path");
1293
+ const resolved = path2.resolve(filePath);
1294
+ const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1295
+ if (baseDir === false) {
1296
+ return resolved;
1297
+ }
1298
+ await fs2.mkdir(baseDir, { recursive: true });
1299
+ const realBaseDir = await fs2.realpath(baseDir);
1300
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1301
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1302
+ }
1303
+ if (mode === "read") {
1304
+ const realTarget = await fs2.realpath(resolved);
1305
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1306
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1307
+ }
1308
+ return realTarget;
1309
+ }
1310
+ const parentDir = path2.dirname(resolved);
1311
+ const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
1312
+ const realExistingAncestor = await fs2.realpath(existingAncestor);
1313
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1314
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1315
+ }
1316
+ await fs2.mkdir(parentDir, { recursive: true });
1317
+ const realParentDir = await fs2.realpath(parentDir);
1318
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1319
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1320
+ }
1321
+ const targetPath = path2.join(realParentDir, path2.basename(resolved));
1322
+ try {
1323
+ const existing = await fs2.lstat(targetPath);
1324
+ if (existing.isSymbolicLink()) {
1325
+ throw new Error("filePath must not point to a symbolic link.");
1326
+ }
1327
+ } catch (error) {
1328
+ if (error.code !== "ENOENT") {
1329
+ throw error;
1330
+ }
1331
+ }
1332
+ return targetPath;
1333
+ }
1334
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
1335
+ if (byteLimit === false) {
1336
+ return handle.readFile({ encoding: "utf8" });
1337
+ }
1338
+ const chunks = [];
1339
+ let totalBytes = 0;
1340
+ let position = 0;
1341
+ while (true) {
1342
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
1343
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
1344
+ if (bytesRead === 0) {
1345
+ break;
1346
+ }
1347
+ totalBytes += bytesRead;
1348
+ if (totalBytes > byteLimit) {
1349
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
1350
+ }
1351
+ chunks.push(buffer.subarray(0, bytesRead));
1352
+ position += bytesRead;
1353
+ }
1354
+ return Buffer.concat(chunks).toString("utf8");
1355
+ }
1356
+
1357
+ // ../../src/internal/CacheStackSnapshotManager.ts
1358
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1359
+ var CacheStackSnapshotManager = class {
1360
+ constructor(options) {
1361
+ this.options = options;
1362
+ }
1363
+ options;
1364
+ async exportState(maxEntries) {
1365
+ const entries = [];
1366
+ await this.visitExportEntries(maxEntries, async (entry) => {
1367
+ entries.push(entry);
1368
+ });
1369
+ return entries;
1370
+ }
1371
+ async importState(entries) {
1372
+ const normalizedEntries = entries.map((entry) => ({
1373
+ key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
1374
+ value: entry.value,
1375
+ ttl: entry.ttl
1376
+ }));
1377
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1378
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1379
+ await Promise.all(
1380
+ batch.map(async (entry) => {
1381
+ await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1382
+ await this.options.tagIndex.touch(entry.key);
1383
+ })
1384
+ );
1385
+ }
1386
+ }
1387
+ async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1388
+ const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1389
+ const tempPath = path.join(
1390
+ path.dirname(targetPath),
1391
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
1392
+ );
1393
+ let handle;
1394
+ try {
1395
+ handle = await fs.open(tempPath, "wx");
1396
+ const openedHandle = handle;
1397
+ await openedHandle.writeFile("[", "utf8");
1398
+ let wroteAny = false;
1399
+ await this.visitExportEntries(maxEntries, async (entry) => {
1400
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1401
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1402
+ wroteAny = true;
1403
+ });
1404
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1405
+ await openedHandle.close();
1406
+ handle = void 0;
1407
+ await fs.rename(tempPath, targetPath);
1408
+ } catch (error) {
1409
+ await handle?.close().catch(() => void 0);
1410
+ await fs.unlink(tempPath).catch(() => void 0);
1411
+ throw error;
1412
+ }
1413
+ }
1414
+ async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1415
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1416
+ const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1417
+ let raw;
1418
+ try {
1419
+ if (maxBytes !== false) {
1420
+ const stat = await handle.stat();
1421
+ if (stat.size > maxBytes) {
1422
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1423
+ }
1424
+ }
1425
+ raw = await readUtf8HandleWithLimit(handle, maxBytes);
1426
+ } finally {
1427
+ await handle.close();
1428
+ }
1429
+ let parsed;
1430
+ try {
1431
+ parsed = JSON.parse(raw);
1432
+ } catch (cause) {
1433
+ throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1434
+ }
1435
+ if (!this.isCacheSnapshotEntries(parsed)) {
1436
+ throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1437
+ }
1438
+ await this.importState(
1439
+ parsed.map((entry) => ({
1440
+ key: entry.key,
1441
+ value: this.sanitizeSnapshotValue(entry.value),
1442
+ ttl: entry.ttl
1443
+ }))
1444
+ );
1102
1445
  }
1103
- return Math.max(...values);
1104
- }
1105
- function normalizePositiveSeconds(value) {
1106
- if (!value || value <= 0) {
1107
- return void 0;
1446
+ async visitExportEntries(maxEntries, visitor) {
1447
+ const exported = /* @__PURE__ */ new Set();
1448
+ for (const layer of this.options.layers) {
1449
+ if (!layer.keys && !layer.forEachKey) {
1450
+ continue;
1451
+ }
1452
+ const visitKey = async (key) => {
1453
+ const exportedKey = this.options.stripQualifiedKey(key);
1454
+ if (exported.has(exportedKey)) {
1455
+ return;
1456
+ }
1457
+ const stored = await this.options.readLayerEntry(layer, key);
1458
+ if (stored === null) {
1459
+ return;
1460
+ }
1461
+ exported.add(exportedKey);
1462
+ if (maxEntries !== false && exported.size > maxEntries) {
1463
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1464
+ }
1465
+ await visitor({
1466
+ key: exportedKey,
1467
+ value: stored,
1468
+ ttl: remainingStoredTtlSeconds(stored)
1469
+ });
1470
+ };
1471
+ if (layer.forEachKey) {
1472
+ await layer.forEachKey(visitKey);
1473
+ continue;
1474
+ }
1475
+ const keys = await layer.keys?.();
1476
+ for (const key of keys ?? []) {
1477
+ await visitKey(key);
1478
+ }
1479
+ }
1108
1480
  }
1109
- return value;
1110
- }
1111
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1112
- if (value == null) {
1113
- return true;
1481
+ isCacheSnapshotEntries(value) {
1482
+ return Array.isArray(value) && value.every((entry) => {
1483
+ if (!entry || typeof entry !== "object") {
1484
+ return false;
1485
+ }
1486
+ const candidate = entry;
1487
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1488
+ });
1114
1489
  }
1115
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1116
- }
1117
-
1118
- // ../../src/internal/CacheStackRuntimePolicy.ts
1119
- function shouldSkipLayer(degradedUntil, now = Date.now()) {
1120
- return degradedUntil !== void 0 && degradedUntil > now;
1121
- }
1122
- function shouldStartBackgroundRefresh({
1123
- isDisconnecting,
1124
- hasRefreshInFlight
1125
- }) {
1126
- return !isDisconnecting && !hasRefreshInFlight;
1127
- }
1128
- function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1129
- if (!gracefulDegradation) {
1130
- return { degrade: false };
1490
+ sanitizeSnapshotValue(value) {
1491
+ return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1131
1492
  }
1132
- const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1133
- return {
1134
- degrade: true,
1135
- degradedUntil: now + retryAfterMs
1136
- };
1137
- }
1138
- function planFreshReadPolicies({
1139
- stored,
1140
- hasFetcher,
1141
- slidingTtl,
1142
- refreshAheadSeconds
1143
- }) {
1144
- const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1145
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1146
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1147
- return {
1148
- refreshedStored,
1149
- refreshedStoredTtl,
1150
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1151
- };
1152
- }
1493
+ };
1153
1494
 
1154
1495
  // ../../src/internal/CacheStackValidation.ts
1155
1496
  var MAX_CACHE_KEY_LENGTH = 1024;
@@ -1378,7 +1719,11 @@ var FetchRateLimiter = class {
1378
1719
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
1379
1720
  nextFetcherBucketId = 0;
1380
1721
  drainTimer;
1722
+ isDisposed = false;
1381
1723
  async schedule(options, context, task) {
1724
+ if (this.isDisposed) {
1725
+ throw new Error("FetchRateLimiter has been disposed.");
1726
+ }
1382
1727
  if (!options) {
1383
1728
  return task();
1384
1729
  }
@@ -1401,6 +1746,27 @@ var FetchRateLimiter = class {
1401
1746
  this.drain();
1402
1747
  });
1403
1748
  }
1749
+ dispose() {
1750
+ this.isDisposed = true;
1751
+ if (this.drainTimer) {
1752
+ clearTimeout(this.drainTimer);
1753
+ this.drainTimer = void 0;
1754
+ }
1755
+ for (const bucket of this.buckets.values()) {
1756
+ if (bucket.cleanupTimer) {
1757
+ clearTimeout(bucket.cleanupTimer);
1758
+ bucket.cleanupTimer = void 0;
1759
+ }
1760
+ }
1761
+ for (const queue of this.queuesByBucket.values()) {
1762
+ for (const item of queue) {
1763
+ item.reject(new Error("FetchRateLimiter has been disposed."));
1764
+ }
1765
+ }
1766
+ this.queuesByBucket.clear();
1767
+ this.pendingBuckets.clear();
1768
+ this.buckets.clear();
1769
+ }
1404
1770
  normalize(options) {
1405
1771
  const maxConcurrent = options.maxConcurrent;
1406
1772
  const intervalMs = options.intervalMs;
@@ -1436,6 +1802,9 @@ var FetchRateLimiter = class {
1436
1802
  return "global";
1437
1803
  }
1438
1804
  drain() {
1805
+ if (this.isDisposed) {
1806
+ return;
1807
+ }
1439
1808
  if (this.drainTimer) {
1440
1809
  clearTimeout(this.drainTimer);
1441
1810
  this.drainTimer = void 0;
@@ -1532,6 +1901,9 @@ var FetchRateLimiter = class {
1532
1901
  }
1533
1902
  }
1534
1903
  bucketState(bucketKey) {
1904
+ if (this.isDisposed) {
1905
+ throw new Error("FetchRateLimiter has been disposed.");
1906
+ }
1535
1907
  const existing = this.buckets.get(bucketKey);
1536
1908
  if (existing) {
1537
1909
  return existing;
@@ -1990,19 +2362,19 @@ var TagIndex = class {
1990
2362
  if (!this.knownKeys.delete(key)) {
1991
2363
  return;
1992
2364
  }
1993
- const path = [];
2365
+ const path2 = [];
1994
2366
  let node = this.root;
1995
2367
  for (const character of key) {
1996
2368
  const child = node.children.get(character);
1997
2369
  if (!child) {
1998
2370
  return;
1999
2371
  }
2000
- path.push([node, character]);
2372
+ path2.push([node, character]);
2001
2373
  node = child;
2002
2374
  }
2003
2375
  node.terminal = false;
2004
- for (let index = path.length - 1; index >= 0; index -= 1) {
2005
- const entry = path[index];
2376
+ for (let index = path2.length - 1; index >= 0; index -= 1) {
2377
+ const entry = path2[index];
2006
2378
  if (!entry) {
2007
2379
  continue;
2008
2380
  }
@@ -2016,39 +2388,31 @@ var TagIndex = class {
2016
2388
  }
2017
2389
  };
2018
2390
 
2019
- // ../../src/serialization/JsonSerializer.ts
2020
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2021
- var MAX_SANITIZE_NODES = 1e4;
2022
- var JsonSerializer = class {
2023
- serialize(value) {
2024
- return JSON.stringify(value);
2025
- }
2026
- deserialize(payload) {
2027
- const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2028
- return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
2029
- }
2030
- };
2031
- var MAX_SANITIZE_DEPTH = 200;
2032
- function sanitizeJsonValue(value, depth, state) {
2391
+ // ../../src/internal/StructuredDataSanitizer.ts
2392
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2393
+ function sanitizeStructuredData(value, options) {
2394
+ return sanitizeValue(value, 0, { count: 0 }, options);
2395
+ }
2396
+ function sanitizeValue(value, depth, state, options) {
2033
2397
  state.count += 1;
2034
- if (state.count > MAX_SANITIZE_NODES) {
2035
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
2398
+ if (state.count > options.maxNodes) {
2399
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
2036
2400
  }
2037
- if (depth > MAX_SANITIZE_DEPTH) {
2038
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
2401
+ if (depth > options.maxDepth) {
2402
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
2039
2403
  }
2040
2404
  if (Array.isArray(value)) {
2041
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
2405
+ return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
2042
2406
  }
2043
2407
  if (!isPlainObject(value)) {
2044
2408
  return value;
2045
2409
  }
2046
- const sanitized = {};
2410
+ const sanitized = options.createObject?.() ?? {};
2047
2411
  for (const [key, entry] of Object.entries(value)) {
2048
- if (DANGEROUS_JSON_KEYS.has(key)) {
2412
+ if (DANGEROUS_KEYS.has(key)) {
2049
2413
  continue;
2050
2414
  }
2051
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
2415
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
2052
2416
  }
2053
2417
  return sanitized;
2054
2418
  }
@@ -2056,6 +2420,21 @@ function isPlainObject(value) {
2056
2420
  return Object.prototype.toString.call(value) === "[object Object]";
2057
2421
  }
2058
2422
 
2423
+ // ../../src/serialization/JsonSerializer.ts
2424
+ var JsonSerializer = class {
2425
+ serialize(value) {
2426
+ return JSON.stringify(value);
2427
+ }
2428
+ deserialize(payload) {
2429
+ const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2430
+ return sanitizeStructuredData(JSON.parse(normalized), {
2431
+ label: "JSON payload",
2432
+ maxDepth: 200,
2433
+ maxNodes: 1e4
2434
+ });
2435
+ }
2436
+ };
2437
+
2059
2438
  // ../../src/stampede/StampedeGuard.ts
2060
2439
  var StampedeGuard = class {
2061
2440
  mutexes = /* @__PURE__ */ new Map();
@@ -2099,7 +2478,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2099
2478
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2100
2479
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2101
2480
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2102
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
2103
2481
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
2104
2482
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
2105
2483
  var DebugLogger = class {
@@ -2156,6 +2534,35 @@ var CacheStack = class extends EventEmitter {
2156
2534
  await this.handleLayerFailure(layer, operation, error);
2157
2535
  }
2158
2536
  });
2537
+ this.invalidation = new CacheStackInvalidationSupport({
2538
+ tagIndex: this.tagIndex,
2539
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2540
+ handleLayerFailure: async (layer, operation, error) => {
2541
+ await this.handleLayerFailure(layer, operation, error);
2542
+ }
2543
+ });
2544
+ this.layerWriter = new CacheStackLayerWriter({
2545
+ layers: this.layers,
2546
+ maintenance: this.maintenance,
2547
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2548
+ shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
2549
+ handleLayerFailure: async (layer, operation, error) => {
2550
+ await this.handleLayerFailure(layer, operation, error);
2551
+ },
2552
+ enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2553
+ resolveFreshTtl: this.resolveFreshTtl.bind(this),
2554
+ resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2555
+ globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2556
+ globalStaleIfError: this.options.staleIfError,
2557
+ writePolicy: this.options.writePolicy,
2558
+ onWriteFailures: (context, failures) => {
2559
+ this.metricsCollector.increment("writeFailures", failures.length);
2560
+ this.logger.debug?.("write-failure", {
2561
+ ...context,
2562
+ failures: failures.map((failure) => this.formatError(failure))
2563
+ });
2564
+ }
2565
+ });
2159
2566
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
2160
2567
  this.logger.warn?.(
2161
2568
  "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."
@@ -2171,6 +2578,16 @@ var CacheStack = class extends EventEmitter {
2171
2578
  "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
2172
2579
  );
2173
2580
  }
2581
+ this.snapshots = new CacheStackSnapshotManager({
2582
+ layers: this.layers,
2583
+ tagIndex: this.tagIndex,
2584
+ snapshotSerializer: this.snapshotSerializer,
2585
+ readLayerEntry: this.readLayerEntry.bind(this),
2586
+ qualifyKey: this.qualifyKey.bind(this),
2587
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
2588
+ validateCacheKey,
2589
+ formatError: this.formatError.bind(this)
2590
+ });
2174
2591
  this.initializeWriteBehind(options.writeBehind);
2175
2592
  this.startup = this.initialize();
2176
2593
  }
@@ -2186,11 +2603,15 @@ var CacheStack = class extends EventEmitter {
2186
2603
  keyDiscovery;
2187
2604
  fetchRateLimiter = new FetchRateLimiter();
2188
2605
  snapshotSerializer = new JsonSerializer();
2606
+ invalidation;
2607
+ layerWriter;
2608
+ snapshots;
2189
2609
  backgroundRefreshes = /* @__PURE__ */ new Map();
2190
2610
  layerDegradedUntil = /* @__PURE__ */ new Map();
2191
2611
  maintenance = new CacheStackMaintenance();
2192
2612
  ttlResolver;
2193
2613
  circuitBreakerManager;
2614
+ nextOperationId = 0;
2194
2615
  currentGeneration;
2195
2616
  isDisconnecting = false;
2196
2617
  disconnectPromise;
@@ -2201,10 +2622,12 @@ var CacheStack = class extends EventEmitter {
2201
2622
  * and no `fetcher` is provided.
2202
2623
  */
2203
2624
  async get(key, fetcher, options) {
2204
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2205
- this.validateWriteOptions(options);
2206
- await this.awaitStartup("get");
2207
- return this.getPrepared(normalizedKey, fetcher, options);
2625
+ return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
2626
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2627
+ this.validateWriteOptions(options);
2628
+ await this.awaitStartup("get");
2629
+ return this.getPrepared(normalizedKey, fetcher, options);
2630
+ });
2208
2631
  }
2209
2632
  async getPrepared(normalizedKey, fetcher, options) {
2210
2633
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
@@ -2326,23 +2749,27 @@ var CacheStack = class extends EventEmitter {
2326
2749
  * Stores a value in all cache layers. Overwrites any existing value.
2327
2750
  */
2328
2751
  async set(key, value, options) {
2329
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2330
- this.validateWriteOptions(options);
2331
- await this.awaitStartup("set");
2332
- await this.storeEntry(normalizedKey, "value", value, options);
2752
+ await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2753
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2754
+ this.validateWriteOptions(options);
2755
+ await this.awaitStartup("set");
2756
+ await this.storeEntry(normalizedKey, "value", value, options);
2757
+ });
2333
2758
  }
2334
2759
  /**
2335
2760
  * Deletes the key from all layers and publishes an invalidation message.
2336
2761
  */
2337
2762
  async delete(key) {
2338
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2339
- await this.awaitStartup("delete");
2340
- await this.deleteKeys([normalizedKey]);
2341
- await this.publishInvalidation({
2342
- scope: "key",
2343
- keys: [normalizedKey],
2344
- sourceId: this.instanceId,
2345
- operation: "delete"
2763
+ await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2764
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2765
+ await this.awaitStartup("delete");
2766
+ await this.deleteKeys([normalizedKey]);
2767
+ await this.publishInvalidation({
2768
+ scope: "key",
2769
+ keys: [normalizedKey],
2770
+ sourceId: this.instanceId,
2771
+ operation: "delete"
2772
+ });
2346
2773
  });
2347
2774
  }
2348
2775
  async clear() {
@@ -2375,95 +2802,99 @@ var CacheStack = class extends EventEmitter {
2375
2802
  });
2376
2803
  }
2377
2804
  async mget(entries) {
2378
- this.assertActive("mget");
2379
- if (entries.length === 0) {
2380
- return [];
2381
- }
2382
- const normalizedEntries = entries.map((entry) => ({
2383
- ...entry,
2384
- key: this.qualifyKey(validateCacheKey(entry.key))
2385
- }));
2386
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2387
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2388
- if (!canFastPath) {
2805
+ return this.observeOperation("layercache.mget", void 0, async () => {
2806
+ this.assertActive("mget");
2807
+ if (entries.length === 0) {
2808
+ return [];
2809
+ }
2810
+ const normalizedEntries = entries.map((entry) => ({
2811
+ ...entry,
2812
+ key: this.qualifyKey(validateCacheKey(entry.key))
2813
+ }));
2814
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2815
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2816
+ if (!canFastPath) {
2817
+ await this.awaitStartup("mget");
2818
+ const pendingReads = /* @__PURE__ */ new Map();
2819
+ return Promise.all(
2820
+ normalizedEntries.map((entry) => {
2821
+ const optionsSignature = serializeOptions(entry.options);
2822
+ const existing = pendingReads.get(entry.key);
2823
+ if (!existing) {
2824
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2825
+ pendingReads.set(entry.key, {
2826
+ promise,
2827
+ fetch: entry.fetch,
2828
+ optionsSignature
2829
+ });
2830
+ return promise;
2831
+ }
2832
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2833
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2834
+ }
2835
+ return existing.promise;
2836
+ })
2837
+ );
2838
+ }
2389
2839
  await this.awaitStartup("mget");
2390
- const pendingReads = /* @__PURE__ */ new Map();
2391
- return Promise.all(
2392
- normalizedEntries.map((entry) => {
2393
- const optionsSignature = serializeOptions(entry.options);
2394
- const existing = pendingReads.get(entry.key);
2395
- if (!existing) {
2396
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2397
- pendingReads.set(entry.key, {
2398
- promise,
2399
- fetch: entry.fetch,
2400
- optionsSignature
2401
- });
2402
- return promise;
2840
+ const pending = /* @__PURE__ */ new Set();
2841
+ const indexesByKey = /* @__PURE__ */ new Map();
2842
+ const resultsByKey = /* @__PURE__ */ new Map();
2843
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
2844
+ const entry = normalizedEntries[index];
2845
+ if (!entry) continue;
2846
+ const key = entry.key;
2847
+ const indexes = indexesByKey.get(key) ?? [];
2848
+ indexes.push(index);
2849
+ indexesByKey.set(key, indexes);
2850
+ pending.add(key);
2851
+ }
2852
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2853
+ const layer = this.layers[layerIndex];
2854
+ if (!layer) continue;
2855
+ const keys = [...pending];
2856
+ if (keys.length === 0) {
2857
+ break;
2858
+ }
2859
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2860
+ for (let offset = 0; offset < values.length; offset += 1) {
2861
+ const key = keys[offset];
2862
+ const stored = values[offset];
2863
+ if (!key || stored === null) {
2864
+ continue;
2403
2865
  }
2404
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2405
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2866
+ const resolved = resolveStoredValue(stored);
2867
+ if (resolved.state === "expired") {
2868
+ await layer.delete(key);
2869
+ continue;
2406
2870
  }
2407
- return existing.promise;
2408
- })
2409
- );
2410
- }
2411
- await this.awaitStartup("mget");
2412
- const pending = /* @__PURE__ */ new Set();
2413
- const indexesByKey = /* @__PURE__ */ new Map();
2414
- const resultsByKey = /* @__PURE__ */ new Map();
2415
- for (let index = 0; index < normalizedEntries.length; index += 1) {
2416
- const entry = normalizedEntries[index];
2417
- if (!entry) continue;
2418
- const key = entry.key;
2419
- const indexes = indexesByKey.get(key) ?? [];
2420
- indexes.push(index);
2421
- indexesByKey.set(key, indexes);
2422
- pending.add(key);
2423
- }
2424
- for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2425
- const layer = this.layers[layerIndex];
2426
- if (!layer) continue;
2427
- const keys = [...pending];
2428
- if (keys.length === 0) {
2429
- break;
2430
- }
2431
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2432
- for (let offset = 0; offset < values.length; offset += 1) {
2433
- const key = keys[offset];
2434
- const stored = values[offset];
2435
- if (!key || stored === null) {
2436
- continue;
2437
- }
2438
- const resolved = resolveStoredValue(stored);
2439
- if (resolved.state === "expired") {
2440
- await layer.delete(key);
2441
- continue;
2871
+ await this.tagIndex.touch(key);
2872
+ await this.backfill(key, stored, layerIndex - 1);
2873
+ resultsByKey.set(key, resolved.value);
2874
+ pending.delete(key);
2875
+ this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2442
2876
  }
2443
- await this.tagIndex.touch(key);
2444
- await this.backfill(key, stored, layerIndex - 1);
2445
- resultsByKey.set(key, resolved.value);
2446
- pending.delete(key);
2447
- this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2448
2877
  }
2449
- }
2450
- if (pending.size > 0) {
2451
- for (const key of pending) {
2452
- await this.tagIndex.remove(key);
2453
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2878
+ if (pending.size > 0) {
2879
+ for (const key of pending) {
2880
+ await this.tagIndex.remove(key);
2881
+ this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2882
+ }
2454
2883
  }
2455
- }
2456
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2884
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2885
+ });
2457
2886
  }
2458
2887
  async mset(entries) {
2459
- this.assertActive("mset");
2460
- const normalizedEntries = entries.map((entry) => ({
2461
- ...entry,
2462
- key: this.qualifyKey(validateCacheKey(entry.key))
2463
- }));
2464
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2465
- await this.awaitStartup("mset");
2466
- await this.writeBatch(normalizedEntries);
2888
+ await this.observeOperation("layercache.mset", void 0, async () => {
2889
+ this.assertActive("mset");
2890
+ const normalizedEntries = entries.map((entry) => ({
2891
+ ...entry,
2892
+ key: this.qualifyKey(validateCacheKey(entry.key))
2893
+ }));
2894
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2895
+ await this.awaitStartup("mset");
2896
+ await this.writeBatch(normalizedEntries);
2897
+ });
2467
2898
  }
2468
2899
  async warm(entries, options = {}) {
2469
2900
  this.assertActive("warm");
@@ -2516,40 +2947,50 @@ var CacheStack = class extends EventEmitter {
2516
2947
  return new CacheNamespace(this, prefix);
2517
2948
  }
2518
2949
  async invalidateByTag(tag) {
2519
- validateTag(tag);
2520
- await this.awaitStartup("invalidateByTag");
2521
- const keys = await this.collectKeysForTag(tag);
2522
- await this.deleteKeys(keys);
2523
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2950
+ await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2951
+ validateTag(tag);
2952
+ await this.awaitStartup("invalidateByTag");
2953
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
2954
+ await this.deleteKeys(keys);
2955
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2956
+ });
2524
2957
  }
2525
2958
  async invalidateByTags(tags, mode = "any") {
2526
- if (tags.length === 0) {
2527
- return;
2528
- }
2529
- validateTags(tags);
2530
- await this.awaitStartup("invalidateByTags");
2531
- const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
2532
- const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2533
- this.assertWithinInvalidationKeyLimit(keys.length);
2534
- await this.deleteKeys(keys);
2535
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2959
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2960
+ if (tags.length === 0) {
2961
+ return;
2962
+ }
2963
+ validateTags(tags);
2964
+ await this.awaitStartup("invalidateByTags");
2965
+ const keysByTag = await Promise.all(
2966
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2967
+ );
2968
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2969
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2970
+ await this.deleteKeys(keys);
2971
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2972
+ });
2536
2973
  }
2537
2974
  async invalidateByPattern(pattern) {
2538
- validatePattern(pattern);
2539
- await this.awaitStartup("invalidateByPattern");
2540
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2541
- this.qualifyPattern(pattern),
2542
- this.invalidationMaxKeys()
2543
- );
2544
- await this.deleteKeys(keys);
2545
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2975
+ await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2976
+ validatePattern(pattern);
2977
+ await this.awaitStartup("invalidateByPattern");
2978
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2979
+ this.qualifyPattern(pattern),
2980
+ this.invalidationMaxKeys()
2981
+ );
2982
+ await this.deleteKeys(keys);
2983
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2984
+ });
2546
2985
  }
2547
2986
  async invalidateByPrefix(prefix) {
2548
- await this.awaitStartup("invalidateByPrefix");
2549
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2550
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2551
- await this.deleteKeys(keys);
2552
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2987
+ await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2988
+ await this.awaitStartup("invalidateByPrefix");
2989
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2990
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2991
+ await this.deleteKeys(keys);
2992
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2993
+ });
2553
2994
  }
2554
2995
  getMetrics() {
2555
2996
  return this.metricsCollector.snapshot;
@@ -2660,95 +3101,19 @@ var CacheStack = class extends EventEmitter {
2660
3101
  }
2661
3102
  async exportState() {
2662
3103
  await this.awaitStartup("exportState");
2663
- const entries = [];
2664
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2665
- entries.push(entry);
2666
- });
2667
- return entries;
3104
+ return this.snapshots.exportState(this.snapshotMaxEntries());
2668
3105
  }
2669
3106
  async importState(entries) {
2670
3107
  await this.awaitStartup("importState");
2671
- const normalizedEntries = entries.map((entry) => ({
2672
- key: this.qualifyKey(validateCacheKey(entry.key)),
2673
- value: entry.value,
2674
- ttl: entry.ttl
2675
- }));
2676
- for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2677
- const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2678
- await Promise.all(
2679
- batch.map(async (entry) => {
2680
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2681
- await this.tagIndex.touch(entry.key);
2682
- })
2683
- );
2684
- }
3108
+ await this.snapshots.importState(entries);
2685
3109
  }
2686
3110
  async persistToFile(filePath) {
2687
3111
  this.assertActive("persistToFile");
2688
- const { promises: fs } = await import("fs");
2689
- const path = await import("path");
2690
- const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2691
- const tempPath = path.join(
2692
- path.dirname(targetPath),
2693
- `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2694
- );
2695
- let handle;
2696
- try {
2697
- handle = await fs.open(tempPath, "wx");
2698
- const openedHandle = handle;
2699
- await openedHandle.writeFile("[", "utf8");
2700
- let wroteAny = false;
2701
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2702
- await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2703
- await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2704
- wroteAny = true;
2705
- });
2706
- await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2707
- await openedHandle.close();
2708
- handle = void 0;
2709
- await fs.rename(tempPath, targetPath);
2710
- } catch (error) {
2711
- await handle?.close().catch(() => void 0);
2712
- await fs.unlink(tempPath).catch(() => void 0);
2713
- throw error;
2714
- }
3112
+ await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2715
3113
  }
2716
3114
  async restoreFromFile(filePath) {
2717
3115
  this.assertActive("restoreFromFile");
2718
- const { promises: fs, constants } = await import("fs");
2719
- const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2720
- const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2721
- const snapshotMaxBytes = this.snapshotMaxBytes();
2722
- let raw;
2723
- try {
2724
- if (snapshotMaxBytes !== false) {
2725
- const stat = await handle.stat();
2726
- if (stat.size > snapshotMaxBytes) {
2727
- throw new Error(
2728
- `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2729
- );
2730
- }
2731
- }
2732
- raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2733
- } finally {
2734
- await handle.close();
2735
- }
2736
- let parsed;
2737
- try {
2738
- parsed = JSON.parse(raw);
2739
- } catch (cause) {
2740
- throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
2741
- }
2742
- if (!this.isCacheSnapshotEntries(parsed)) {
2743
- throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
2744
- }
2745
- await this.importState(
2746
- parsed.map((entry) => ({
2747
- key: entry.key,
2748
- value: this.sanitizeSnapshotValue(entry.value),
2749
- ttl: entry.ttl
2750
- }))
2751
- );
3116
+ await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2752
3117
  }
2753
3118
  async disconnect() {
2754
3119
  if (!this.disconnectPromise) {
@@ -2760,6 +3125,7 @@ var CacheStack = class extends EventEmitter {
2760
3125
  await this.maintenance.waitForGenerationCleanup();
2761
3126
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2762
3127
  this.maintenance.disposeWriteBehindTimer();
3128
+ this.fetchRateLimiter.dispose();
2763
3129
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2764
3130
  })();
2765
3131
  }
@@ -2873,7 +3239,7 @@ var CacheStack = class extends EventEmitter {
2873
3239
  async storeEntry(key, kind, value, options) {
2874
3240
  const clearEpoch = this.maintenance.currentClearEpoch();
2875
3241
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
2876
- await this.writeAcrossLayers(key, kind, value, options);
3242
+ await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2877
3243
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2878
3244
  return;
2879
3245
  }
@@ -2890,52 +3256,7 @@ var CacheStack = class extends EventEmitter {
2890
3256
  }
2891
3257
  }
2892
3258
  async writeBatch(entries) {
2893
- const now = Date.now();
2894
- const clearEpoch = this.maintenance.currentClearEpoch();
2895
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2896
- const entriesByLayer = /* @__PURE__ */ new Map();
2897
- const immediateOperations = [];
2898
- const deferredOperations = [];
2899
- for (const entry of entries) {
2900
- for (const layer of this.layers) {
2901
- if (this.shouldSkipLayer(layer)) {
2902
- continue;
2903
- }
2904
- const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
2905
- const bucket = entriesByLayer.get(layer) ?? [];
2906
- bucket.push(layerEntry);
2907
- entriesByLayer.set(layer, bucket);
2908
- }
2909
- }
2910
- for (const [layer, layerEntries] of entriesByLayer.entries()) {
2911
- const operation = async () => {
2912
- if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2913
- return;
2914
- }
2915
- const activeEntries = layerEntries.filter(
2916
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2917
- );
2918
- if (activeEntries.length === 0) {
2919
- return;
2920
- }
2921
- try {
2922
- if (layer.setMany) {
2923
- await layer.setMany(activeEntries);
2924
- return;
2925
- }
2926
- await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2927
- } catch (error) {
2928
- await this.handleLayerFailure(layer, "write", error);
2929
- }
2930
- };
2931
- if (this.shouldWriteBehind(layer)) {
2932
- deferredOperations.push(operation);
2933
- } else {
2934
- immediateOperations.push(operation);
2935
- }
2936
- }
2937
- await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2938
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
3259
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
2939
3260
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2940
3261
  return;
2941
3262
  }
@@ -3042,58 +3363,6 @@ var CacheStack = class extends EventEmitter {
3042
3363
  this.emit("backfill", { key, layer: layer.name });
3043
3364
  }
3044
3365
  }
3045
- async writeAcrossLayers(key, kind, value, options) {
3046
- const now = Date.now();
3047
- const clearEpoch = this.maintenance.currentClearEpoch();
3048
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3049
- const immediateOperations = [];
3050
- const deferredOperations = [];
3051
- for (const layer of this.layers) {
3052
- const operation = async () => {
3053
- if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3054
- return;
3055
- }
3056
- if (this.shouldSkipLayer(layer)) {
3057
- return;
3058
- }
3059
- const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
3060
- try {
3061
- await layer.set(entry.key, entry.value, entry.ttl);
3062
- } catch (error) {
3063
- await this.handleLayerFailure(layer, "write", error);
3064
- }
3065
- };
3066
- if (this.shouldWriteBehind(layer)) {
3067
- deferredOperations.push(operation);
3068
- } else {
3069
- immediateOperations.push(operation);
3070
- }
3071
- }
3072
- await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
3073
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
3074
- }
3075
- async executeLayerOperations(operations, context) {
3076
- if (this.options.writePolicy !== "best-effort") {
3077
- await Promise.all(operations.map((operation) => operation()));
3078
- return;
3079
- }
3080
- const results = await Promise.allSettled(operations.map((operation) => operation()));
3081
- const failures = results.filter((result) => result.status === "rejected");
3082
- if (failures.length === 0) {
3083
- return;
3084
- }
3085
- this.metricsCollector.increment("writeFailures", failures.length);
3086
- this.logger.debug?.("write-failure", {
3087
- ...context,
3088
- failures: failures.map((failure) => this.formatError(failure.reason))
3089
- });
3090
- if (failures.length === operations.length) {
3091
- throw new AggregateError(
3092
- failures.map((failure) => failure.reason),
3093
- `${context.action} failed for every cache layer`
3094
- );
3095
- }
3096
- }
3097
3366
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3098
3367
  return this.ttlResolver.resolveFreshTtl(
3099
3368
  key,
@@ -3159,7 +3428,7 @@ var CacheStack = class extends EventEmitter {
3159
3428
  return;
3160
3429
  }
3161
3430
  this.maintenance.bumpKeyEpochs(keys);
3162
- await this.deleteKeysFromLayers(this.layers, keys);
3431
+ await this.invalidation.deleteKeysFromLayers(this.layers, keys);
3163
3432
  for (const key of keys) {
3164
3433
  await this.tagIndex.remove(key);
3165
3434
  this.ttlResolver.deleteProfile(key);
@@ -3191,7 +3460,7 @@ var CacheStack = class extends EventEmitter {
3191
3460
  }
3192
3461
  const keys = message.keys ?? [];
3193
3462
  this.maintenance.bumpKeyEpochs(keys);
3194
- await this.deleteKeysFromLayers(localLayers, keys);
3463
+ await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3195
3464
  if (message.operation !== "write") {
3196
3465
  for (const key of keys) {
3197
3466
  await this.tagIndex.remove(key);
@@ -3248,6 +3517,31 @@ var CacheStack = class extends EventEmitter {
3248
3517
  shouldBroadcastL1Invalidation() {
3249
3518
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3250
3519
  }
3520
+ async observeOperation(name, attributes, execute) {
3521
+ const id = this.nextOperationId;
3522
+ this.nextOperationId += 1;
3523
+ this.emit("operation-start", { id, name, attributes });
3524
+ try {
3525
+ const result = await execute();
3526
+ this.emit("operation-end", {
3527
+ id,
3528
+ name,
3529
+ attributes,
3530
+ success: true,
3531
+ result: result === null ? "null" : void 0
3532
+ });
3533
+ return result;
3534
+ } catch (error) {
3535
+ this.emit("operation-end", {
3536
+ id,
3537
+ name,
3538
+ attributes,
3539
+ success: false,
3540
+ error
3541
+ });
3542
+ throw error;
3543
+ }
3544
+ }
3251
3545
  scheduleGenerationCleanup(generation) {
3252
3546
  this.maintenance.scheduleGenerationCleanup(
3253
3547
  generation,
@@ -3303,37 +3597,6 @@ var CacheStack = class extends EventEmitter {
3303
3597
  });
3304
3598
  this.emitError("write-behind", { failed: failures.length, total: batch.length });
3305
3599
  }
3306
- buildLayerSetEntry(layer, key, kind, value, options, now) {
3307
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
3308
- const staleWhileRevalidate = this.resolveLayerSeconds(
3309
- layer.name,
3310
- options?.staleWhileRevalidate,
3311
- this.options.staleWhileRevalidate
3312
- );
3313
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
3314
- const payload = createStoredValueEnvelope({
3315
- kind,
3316
- value,
3317
- freshTtlSeconds: freshTtl,
3318
- staleWhileRevalidateSeconds: staleWhileRevalidate,
3319
- staleIfErrorSeconds: staleIfError,
3320
- now
3321
- });
3322
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
3323
- return {
3324
- key,
3325
- value: payload,
3326
- ttl
3327
- };
3328
- }
3329
- intersectKeys(groups) {
3330
- if (groups.length === 0) {
3331
- return [];
3332
- }
3333
- const [firstGroup, ...rest] = groups;
3334
- const restSets = rest.map((group) => new Set(group));
3335
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3336
- }
3337
3600
  qualifyKey(key) {
3338
3601
  return qualifyGenerationKey(key, this.currentGeneration);
3339
3602
  }
@@ -3343,32 +3606,6 @@ var CacheStack = class extends EventEmitter {
3343
3606
  stripQualifiedKey(key) {
3344
3607
  return stripGenerationPrefix(key, this.currentGeneration);
3345
3608
  }
3346
- async deleteKeysFromLayers(layers, keys) {
3347
- await Promise.all(
3348
- layers.map(async (layer) => {
3349
- if (this.shouldSkipLayer(layer)) {
3350
- return;
3351
- }
3352
- if (layer.deleteMany) {
3353
- try {
3354
- await layer.deleteMany(keys);
3355
- } catch (error) {
3356
- await this.handleLayerFailure(layer, "delete", error);
3357
- }
3358
- return;
3359
- }
3360
- await Promise.all(
3361
- keys.map(async (key) => {
3362
- try {
3363
- await layer.delete(key);
3364
- } catch (error) {
3365
- await this.handleLayerFailure(layer, "delete", error);
3366
- }
3367
- })
3368
- );
3369
- })
3370
- );
3371
- }
3372
3609
  validateConfiguration() {
3373
3610
  if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
3374
3611
  throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
@@ -3499,18 +3736,6 @@ var CacheStack = class extends EventEmitter {
3499
3736
  this.emit("error", { operation, ...context });
3500
3737
  }
3501
3738
  }
3502
- isCacheSnapshotEntries(value) {
3503
- return Array.isArray(value) && value.every((entry) => {
3504
- if (!entry || typeof entry !== "object") {
3505
- return false;
3506
- }
3507
- const candidate = entry;
3508
- return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
3509
- });
3510
- }
3511
- sanitizeSnapshotValue(value) {
3512
- return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
3513
- }
3514
3739
  snapshotMaxBytes() {
3515
3740
  return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3516
3741
  }
@@ -3520,62 +3745,6 @@ var CacheStack = class extends EventEmitter {
3520
3745
  invalidationMaxKeys() {
3521
3746
  return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3522
3747
  }
3523
- async collectKeysForTag(tag) {
3524
- const keys = /* @__PURE__ */ new Set();
3525
- if (this.tagIndex.forEachKeyForTag) {
3526
- await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3527
- keys.add(key);
3528
- this.assertWithinInvalidationKeyLimit(keys.size);
3529
- });
3530
- return [...keys];
3531
- }
3532
- for (const key of await this.tagIndex.keysForTag(tag)) {
3533
- keys.add(key);
3534
- this.assertWithinInvalidationKeyLimit(keys.size);
3535
- }
3536
- return [...keys];
3537
- }
3538
- assertWithinInvalidationKeyLimit(size) {
3539
- const maxKeys = this.invalidationMaxKeys();
3540
- if (maxKeys !== false && size > maxKeys) {
3541
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
3542
- }
3543
- }
3544
- async visitExportEntries(maxEntries, visitor) {
3545
- const exported = /* @__PURE__ */ new Set();
3546
- for (const layer of this.layers) {
3547
- if (!layer.keys && !layer.forEachKey) {
3548
- continue;
3549
- }
3550
- const visitKey = async (key) => {
3551
- const exportedKey = this.stripQualifiedKey(key);
3552
- if (exported.has(exportedKey)) {
3553
- return;
3554
- }
3555
- const stored = await this.readLayerEntry(layer, key);
3556
- if (stored === null) {
3557
- return;
3558
- }
3559
- exported.add(exportedKey);
3560
- if (maxEntries !== false && exported.size > maxEntries) {
3561
- throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3562
- }
3563
- await visitor({
3564
- key: exportedKey,
3565
- value: stored,
3566
- ttl: remainingStoredTtlSeconds(stored)
3567
- });
3568
- };
3569
- if (layer.forEachKey) {
3570
- await layer.forEachKey(visitKey);
3571
- continue;
3572
- }
3573
- const keys = await layer.keys?.();
3574
- for (const key of keys ?? []) {
3575
- await visitKey(key);
3576
- }
3577
- }
3578
- }
3579
3748
  };
3580
3749
 
3581
3750
  // src/module.ts