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.
@@ -724,7 +724,7 @@ function normalizeForSerialization(value) {
724
724
  }
725
725
  function serializeKeyPart(value) {
726
726
  if (typeof value === "string") {
727
- return `s:${value}`;
727
+ return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
728
728
  }
729
729
  if (typeof value === "number") {
730
730
  return `n:${value}`;
@@ -752,102 +752,6 @@ function createInstanceId() {
752
752
  return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
753
753
  }
754
754
 
755
- // ../../src/internal/CacheSnapshotFile.ts
756
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
757
- const relative = path.relative(realBaseDir, candidatePath);
758
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
759
- }
760
- async function findExistingAncestor(directory, fs, path) {
761
- let current = directory;
762
- while (true) {
763
- try {
764
- await fs.lstat(current);
765
- return current;
766
- } catch (error) {
767
- if (error.code !== "ENOENT") {
768
- throw error;
769
- }
770
- }
771
- const parent = path.dirname(current);
772
- if (parent === current) {
773
- return current;
774
- }
775
- current = parent;
776
- }
777
- }
778
- async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
779
- if (filePath.length === 0) {
780
- throw new Error("filePath must not be empty.");
781
- }
782
- if (filePath.includes("\0")) {
783
- throw new Error("filePath must not contain null bytes.");
784
- }
785
- const { promises: fs } = await import("fs");
786
- const path = await import("path");
787
- const resolved = path.resolve(filePath);
788
- const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
789
- if (baseDir === false) {
790
- return resolved;
791
- }
792
- await fs.mkdir(baseDir, { recursive: true });
793
- const realBaseDir = await fs.realpath(baseDir);
794
- if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
795
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
796
- }
797
- if (mode === "read") {
798
- const realTarget = await fs.realpath(resolved);
799
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
800
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
801
- }
802
- return realTarget;
803
- }
804
- const parentDir = path.dirname(resolved);
805
- const existingAncestor = await findExistingAncestor(parentDir, fs, path);
806
- const realExistingAncestor = await fs.realpath(existingAncestor);
807
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
808
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
809
- }
810
- await fs.mkdir(parentDir, { recursive: true });
811
- const realParentDir = await fs.realpath(parentDir);
812
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
813
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
814
- }
815
- const targetPath = path.join(realParentDir, path.basename(resolved));
816
- try {
817
- const existing = await fs.lstat(targetPath);
818
- if (existing.isSymbolicLink()) {
819
- throw new Error("filePath must not point to a symbolic link.");
820
- }
821
- } catch (error) {
822
- if (error.code !== "ENOENT") {
823
- throw error;
824
- }
825
- }
826
- return targetPath;
827
- }
828
- async function readUtf8HandleWithLimit(handle, byteLimit) {
829
- if (byteLimit === false) {
830
- return handle.readFile({ encoding: "utf8" });
831
- }
832
- const chunks = [];
833
- let totalBytes = 0;
834
- let position = 0;
835
- while (true) {
836
- const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
837
- const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
838
- if (bytesRead === 0) {
839
- break;
840
- }
841
- totalBytes += bytesRead;
842
- if (totalBytes > byteLimit) {
843
- throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
844
- }
845
- chunks.push(buffer.subarray(0, bytesRead));
846
- position += bytesRead;
847
- }
848
- return Buffer.concat(chunks).toString("utf8");
849
- }
850
-
851
755
  // ../../src/internal/CacheStackGeneration.ts
852
756
  var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
853
757
  function generationPrefix(generation) {
@@ -895,102 +799,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
895
799
  return batches;
896
800
  }
897
801
 
898
- // ../../src/internal/CacheStackMaintenance.ts
899
- var CacheStackMaintenance = class {
900
- keyEpochs = /* @__PURE__ */ new Map();
901
- writeBehindQueue = [];
902
- writeBehindTimer;
903
- writeBehindFlushPromise;
904
- generationCleanupPromise;
905
- clearEpoch = 0;
906
- initializeWriteBehindTimer(writeStrategy, options, flush) {
907
- if (writeStrategy !== "write-behind") {
908
- return;
909
- }
910
- const flushIntervalMs = options?.flushIntervalMs;
911
- if (!flushIntervalMs || flushIntervalMs <= 0) {
912
- return;
913
- }
914
- this.disposeWriteBehindTimer();
915
- this.writeBehindTimer = setInterval(() => {
916
- void flush();
917
- }, flushIntervalMs);
918
- this.writeBehindTimer.unref?.();
802
+ // ../../src/internal/CacheStackInvalidationSupport.ts
803
+ var CacheStackInvalidationSupport = class {
804
+ constructor(options) {
805
+ this.options = options;
919
806
  }
920
- disposeWriteBehindTimer() {
921
- if (!this.writeBehindTimer) {
922
- return;
807
+ options;
808
+ async collectKeysForTag(tag, maxKeys) {
809
+ const keys = /* @__PURE__ */ new Set();
810
+ if (this.options.tagIndex.forEachKeyForTag) {
811
+ await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
812
+ keys.add(key);
813
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
814
+ });
815
+ return [...keys];
923
816
  }
924
- clearInterval(this.writeBehindTimer);
925
- this.writeBehindTimer = void 0;
926
- }
927
- beginClearEpoch() {
928
- this.clearEpoch += 1;
929
- this.keyEpochs.clear();
930
- this.writeBehindQueue.length = 0;
931
- }
932
- currentClearEpoch() {
933
- return this.clearEpoch;
934
- }
935
- currentKeyEpoch(key) {
936
- return this.keyEpochs.get(key) ?? 0;
937
- }
938
- bumpKeyEpochs(keys) {
939
- for (const key of keys) {
940
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
817
+ for (const key of await this.options.tagIndex.keysForTag(tag)) {
818
+ keys.add(key);
819
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
941
820
  }
821
+ return [...keys];
942
822
  }
943
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
944
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
945
- return true;
946
- }
947
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
948
- return true;
823
+ intersectKeys(groups) {
824
+ if (groups.length === 0) {
825
+ return [];
949
826
  }
950
- return false;
827
+ const [firstGroup, ...rest] = groups;
828
+ const restSets = rest.map((group) => new Set(group));
829
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
951
830
  }
952
- async enqueueWriteBehind(operation, options, flushBatch) {
953
- this.writeBehindQueue.push(operation);
954
- const batchSize = options?.batchSize ?? 100;
955
- const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
956
- if (this.writeBehindQueue.length >= batchSize) {
957
- await this.flushWriteBehindQueue(options, flushBatch);
958
- return;
959
- }
960
- if (this.writeBehindQueue.length >= maxQueueSize) {
961
- await this.flushWriteBehindQueue(options, flushBatch);
962
- }
831
+ async deleteKeysFromLayers(layers, keys) {
832
+ await Promise.all(
833
+ layers.map(async (layer) => {
834
+ if (this.options.shouldSkipLayer(layer)) {
835
+ return;
836
+ }
837
+ if (layer.deleteMany) {
838
+ try {
839
+ await layer.deleteMany(keys);
840
+ } catch (error) {
841
+ await this.options.handleLayerFailure(layer, "delete", error);
842
+ }
843
+ return;
844
+ }
845
+ await Promise.all(
846
+ keys.map(async (key) => {
847
+ try {
848
+ await layer.delete(key);
849
+ } catch (error) {
850
+ await this.options.handleLayerFailure(layer, "delete", error);
851
+ }
852
+ })
853
+ );
854
+ })
855
+ );
963
856
  }
964
- async flushWriteBehindQueue(options, flushBatch) {
965
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
966
- await this.writeBehindFlushPromise;
967
- return;
968
- }
969
- const batchSize = options?.batchSize ?? 100;
970
- const batch = this.writeBehindQueue.splice(0, batchSize);
971
- this.writeBehindFlushPromise = flushBatch(batch);
972
- try {
973
- await this.writeBehindFlushPromise;
974
- } finally {
975
- this.writeBehindFlushPromise = void 0;
976
- }
977
- if (this.writeBehindQueue.length > 0) {
978
- await this.flushWriteBehindQueue(options, flushBatch);
857
+ assertWithinInvalidationKeyLimit(size, maxKeys) {
858
+ if (maxKeys !== false && size > maxKeys) {
859
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
979
860
  }
980
861
  }
981
- scheduleGenerationCleanup(generation, task, onError) {
982
- const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
983
- onError(generation, error);
984
- });
985
- this.generationCleanupPromise = scheduledTask.finally(() => {
986
- if (this.generationCleanupPromise === scheduledTask) {
987
- this.generationCleanupPromise = void 0;
988
- }
989
- });
990
- }
991
- async waitForGenerationCleanup() {
992
- await this.generationCleanupPromise;
993
- }
994
862
  };
995
863
 
996
864
  // ../../src/internal/StoredValue.ts
@@ -1120,72 +988,598 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
1120
988
  if (!isStoredValueEnvelope(stored)) {
1121
989
  return stored;
1122
990
  }
1123
- return createStoredValueEnvelope({
1124
- kind: stored.kind,
1125
- value: stored.value,
1126
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1127
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1128
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1129
- now
1130
- });
1131
- }
1132
- function maxExpiry(stored) {
1133
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1134
- (value) => value !== null
1135
- );
1136
- if (values.length === 0) {
1137
- return null;
991
+ return createStoredValueEnvelope({
992
+ kind: stored.kind,
993
+ value: stored.value,
994
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
995
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
996
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
997
+ now
998
+ });
999
+ }
1000
+ function maxExpiry(stored) {
1001
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1002
+ (value) => value !== null
1003
+ );
1004
+ if (values.length === 0) {
1005
+ return null;
1006
+ }
1007
+ return Math.max(...values);
1008
+ }
1009
+ function normalizePositiveSeconds(value) {
1010
+ if (!value || value <= 0) {
1011
+ return void 0;
1012
+ }
1013
+ return value;
1014
+ }
1015
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1016
+ if (value == null) {
1017
+ return true;
1018
+ }
1019
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1020
+ }
1021
+
1022
+ // ../../src/internal/CacheStackLayerWriter.ts
1023
+ var CacheStackLayerWriter = class {
1024
+ constructor(options) {
1025
+ this.options = options;
1026
+ }
1027
+ options;
1028
+ async writeAcrossLayers(key, kind, value, writeOptions) {
1029
+ const now = Date.now();
1030
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1031
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
1032
+ const immediateOperations = [];
1033
+ const deferredOperations = [];
1034
+ for (const layer of this.options.layers) {
1035
+ const operation = async () => {
1036
+ if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
1037
+ return;
1038
+ }
1039
+ if (this.options.shouldSkipLayer(layer)) {
1040
+ return;
1041
+ }
1042
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
1043
+ try {
1044
+ await layer.set(entry.key, entry.value, entry.ttl);
1045
+ } catch (error) {
1046
+ await this.options.handleLayerFailure(layer, "write", error);
1047
+ }
1048
+ };
1049
+ if (this.options.shouldWriteBehind(layer)) {
1050
+ deferredOperations.push(operation);
1051
+ } else {
1052
+ immediateOperations.push(operation);
1053
+ }
1054
+ }
1055
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1056
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1057
+ }
1058
+ async writeBatch(entries) {
1059
+ const now = Date.now();
1060
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1061
+ const entryEpochs = new Map(
1062
+ entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
1063
+ );
1064
+ const entriesByLayer = /* @__PURE__ */ new Map();
1065
+ const immediateOperations = [];
1066
+ const deferredOperations = [];
1067
+ for (const entry of entries) {
1068
+ for (const layer of this.options.layers) {
1069
+ if (this.options.shouldSkipLayer(layer)) {
1070
+ continue;
1071
+ }
1072
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1073
+ const bucket = entriesByLayer.get(layer) ?? [];
1074
+ bucket.push(layerEntry);
1075
+ entriesByLayer.set(layer, bucket);
1076
+ }
1077
+ }
1078
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1079
+ const operation = async () => {
1080
+ if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
1081
+ return;
1082
+ }
1083
+ const activeEntries = layerEntries.filter(
1084
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
1085
+ );
1086
+ if (activeEntries.length === 0) {
1087
+ return;
1088
+ }
1089
+ try {
1090
+ if (layer.setMany) {
1091
+ await layer.setMany(activeEntries);
1092
+ return;
1093
+ }
1094
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1095
+ } catch (error) {
1096
+ await this.options.handleLayerFailure(layer, "write", error);
1097
+ }
1098
+ };
1099
+ if (this.options.shouldWriteBehind(layer)) {
1100
+ deferredOperations.push(operation);
1101
+ } else {
1102
+ immediateOperations.push(operation);
1103
+ }
1104
+ }
1105
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1106
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1107
+ return { clearEpoch, entryEpochs };
1108
+ }
1109
+ async executeLayerOperations(operations, context) {
1110
+ if (this.options.writePolicy !== "best-effort") {
1111
+ await Promise.all(operations.map((operation) => operation()));
1112
+ return;
1113
+ }
1114
+ const results = await Promise.allSettled(operations.map((operation) => operation()));
1115
+ const failures = results.filter((result) => result.status === "rejected");
1116
+ const degraded = results.filter((result) => result.status === "fulfilled");
1117
+ if (failures.length === 0) {
1118
+ return;
1119
+ }
1120
+ this.options.onWriteFailures(
1121
+ context,
1122
+ failures.map((failure) => failure.reason)
1123
+ );
1124
+ if (failures.length === operations.length) {
1125
+ throw new AggregateError(
1126
+ failures.map((failure) => failure.reason),
1127
+ `${context.action} failed for every cache layer`
1128
+ );
1129
+ }
1130
+ }
1131
+ buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
1132
+ const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
1133
+ const staleWhileRevalidate = this.options.resolveLayerSeconds(
1134
+ layer.name,
1135
+ writeOptions?.staleWhileRevalidate,
1136
+ this.options.globalStaleWhileRevalidate
1137
+ );
1138
+ const staleIfError = this.options.resolveLayerSeconds(
1139
+ layer.name,
1140
+ writeOptions?.staleIfError,
1141
+ this.options.globalStaleIfError
1142
+ );
1143
+ const payload = createStoredValueEnvelope({
1144
+ kind,
1145
+ value,
1146
+ freshTtlSeconds: freshTtl,
1147
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
1148
+ staleIfErrorSeconds: staleIfError,
1149
+ now
1150
+ });
1151
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1152
+ return {
1153
+ key,
1154
+ value: payload,
1155
+ ttl
1156
+ };
1157
+ }
1158
+ };
1159
+
1160
+ // ../../src/internal/CacheStackMaintenance.ts
1161
+ var CacheStackMaintenance = class {
1162
+ keyEpochs = /* @__PURE__ */ new Map();
1163
+ writeBehindQueue = [];
1164
+ writeBehindTimer;
1165
+ writeBehindFlushPromise;
1166
+ generationCleanupPromise;
1167
+ clearEpoch = 0;
1168
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
1169
+ if (writeStrategy !== "write-behind") {
1170
+ return;
1171
+ }
1172
+ const flushIntervalMs = options?.flushIntervalMs;
1173
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
1174
+ return;
1175
+ }
1176
+ this.disposeWriteBehindTimer();
1177
+ this.writeBehindTimer = setInterval(() => {
1178
+ void flush();
1179
+ }, flushIntervalMs);
1180
+ this.writeBehindTimer.unref?.();
1181
+ }
1182
+ disposeWriteBehindTimer() {
1183
+ if (!this.writeBehindTimer) {
1184
+ return;
1185
+ }
1186
+ clearInterval(this.writeBehindTimer);
1187
+ this.writeBehindTimer = void 0;
1188
+ }
1189
+ beginClearEpoch() {
1190
+ this.clearEpoch += 1;
1191
+ this.keyEpochs.clear();
1192
+ this.writeBehindQueue.length = 0;
1193
+ }
1194
+ currentClearEpoch() {
1195
+ return this.clearEpoch;
1196
+ }
1197
+ currentKeyEpoch(key) {
1198
+ return this.keyEpochs.get(key) ?? 0;
1199
+ }
1200
+ bumpKeyEpochs(keys) {
1201
+ for (const key of keys) {
1202
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1203
+ }
1204
+ }
1205
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1206
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
1207
+ return true;
1208
+ }
1209
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
1210
+ return true;
1211
+ }
1212
+ return false;
1213
+ }
1214
+ async enqueueWriteBehind(operation, options, flushBatch) {
1215
+ this.writeBehindQueue.push(operation);
1216
+ const batchSize = options?.batchSize ?? 100;
1217
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
1218
+ if (this.writeBehindQueue.length >= batchSize) {
1219
+ await this.flushWriteBehindQueue(options, flushBatch);
1220
+ return;
1221
+ }
1222
+ if (this.writeBehindQueue.length >= maxQueueSize) {
1223
+ await this.flushWriteBehindQueue(options, flushBatch);
1224
+ }
1225
+ }
1226
+ async flushWriteBehindQueue(options, flushBatch) {
1227
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1228
+ await this.writeBehindFlushPromise;
1229
+ return;
1230
+ }
1231
+ const batchSize = options?.batchSize ?? 100;
1232
+ const batch = this.writeBehindQueue.splice(0, batchSize);
1233
+ this.writeBehindFlushPromise = flushBatch(batch);
1234
+ try {
1235
+ await this.writeBehindFlushPromise;
1236
+ } finally {
1237
+ this.writeBehindFlushPromise = void 0;
1238
+ }
1239
+ if (this.writeBehindQueue.length > 0) {
1240
+ await this.flushWriteBehindQueue(options, flushBatch);
1241
+ }
1242
+ }
1243
+ scheduleGenerationCleanup(generation, task, onError) {
1244
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
1245
+ onError(generation, error);
1246
+ });
1247
+ this.generationCleanupPromise = scheduledTask.finally(() => {
1248
+ if (this.generationCleanupPromise === scheduledTask) {
1249
+ this.generationCleanupPromise = void 0;
1250
+ }
1251
+ });
1252
+ }
1253
+ async waitForGenerationCleanup() {
1254
+ await this.generationCleanupPromise;
1255
+ }
1256
+ };
1257
+
1258
+ // ../../src/internal/CacheStackRuntimePolicy.ts
1259
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1260
+ return degradedUntil !== void 0 && degradedUntil > now;
1261
+ }
1262
+ function shouldStartBackgroundRefresh({
1263
+ isDisconnecting,
1264
+ hasRefreshInFlight
1265
+ }) {
1266
+ return !isDisconnecting && !hasRefreshInFlight;
1267
+ }
1268
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1269
+ if (!gracefulDegradation) {
1270
+ return { degrade: false };
1271
+ }
1272
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1273
+ return {
1274
+ degrade: true,
1275
+ degradedUntil: now + retryAfterMs
1276
+ };
1277
+ }
1278
+ function planFreshReadPolicies({
1279
+ stored,
1280
+ hasFetcher,
1281
+ slidingTtl,
1282
+ refreshAheadSeconds
1283
+ }) {
1284
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1285
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1286
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1287
+ return {
1288
+ refreshedStored,
1289
+ refreshedStoredTtl,
1290
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1291
+ };
1292
+ }
1293
+
1294
+ // ../../src/internal/CacheStackSnapshotManager.ts
1295
+ var import_node_crypto = require("crypto");
1296
+ var import_node_fs = require("fs");
1297
+ var import_node_path = __toESM(require("path"), 1);
1298
+
1299
+ // ../../src/internal/CacheSnapshotFile.ts
1300
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1301
+ const relative = path2.relative(realBaseDir, candidatePath);
1302
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1303
+ }
1304
+ async function findExistingAncestor(directory, fs2, path2) {
1305
+ let current = directory;
1306
+ while (true) {
1307
+ try {
1308
+ await fs2.lstat(current);
1309
+ return current;
1310
+ } catch (error) {
1311
+ if (error.code !== "ENOENT") {
1312
+ throw error;
1313
+ }
1314
+ }
1315
+ const parent = path2.dirname(current);
1316
+ if (parent === current) {
1317
+ return current;
1318
+ }
1319
+ current = parent;
1320
+ }
1321
+ }
1322
+ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
1323
+ if (filePath.length === 0) {
1324
+ throw new Error("filePath must not be empty.");
1325
+ }
1326
+ if (filePath.includes("\0")) {
1327
+ throw new Error("filePath must not contain null bytes.");
1328
+ }
1329
+ const { promises: fs2 } = await import("fs");
1330
+ const path2 = await import("path");
1331
+ const resolved = path2.resolve(filePath);
1332
+ const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1333
+ if (baseDir === false) {
1334
+ return resolved;
1335
+ }
1336
+ await fs2.mkdir(baseDir, { recursive: true });
1337
+ const realBaseDir = await fs2.realpath(baseDir);
1338
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1339
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1340
+ }
1341
+ if (mode === "read") {
1342
+ const realTarget = await fs2.realpath(resolved);
1343
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1344
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1345
+ }
1346
+ return realTarget;
1347
+ }
1348
+ const parentDir = path2.dirname(resolved);
1349
+ const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
1350
+ const realExistingAncestor = await fs2.realpath(existingAncestor);
1351
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1352
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1353
+ }
1354
+ await fs2.mkdir(parentDir, { recursive: true });
1355
+ const realParentDir = await fs2.realpath(parentDir);
1356
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1357
+ throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1358
+ }
1359
+ const targetPath = path2.join(realParentDir, path2.basename(resolved));
1360
+ try {
1361
+ const existing = await fs2.lstat(targetPath);
1362
+ if (existing.isSymbolicLink()) {
1363
+ throw new Error("filePath must not point to a symbolic link.");
1364
+ }
1365
+ } catch (error) {
1366
+ if (error.code !== "ENOENT") {
1367
+ throw error;
1368
+ }
1369
+ }
1370
+ return targetPath;
1371
+ }
1372
+ async function readUtf8HandleWithLimit(handle, byteLimit) {
1373
+ if (byteLimit === false) {
1374
+ return handle.readFile({ encoding: "utf8" });
1375
+ }
1376
+ const chunks = [];
1377
+ let totalBytes = 0;
1378
+ let position = 0;
1379
+ while (true) {
1380
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
1381
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
1382
+ if (bytesRead === 0) {
1383
+ break;
1384
+ }
1385
+ totalBytes += bytesRead;
1386
+ if (totalBytes > byteLimit) {
1387
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
1388
+ }
1389
+ chunks.push(buffer.subarray(0, bytesRead));
1390
+ position += bytesRead;
1391
+ }
1392
+ return Buffer.concat(chunks).toString("utf8");
1393
+ }
1394
+
1395
+ // ../../src/internal/StructuredDataSanitizer.ts
1396
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1397
+ function sanitizeStructuredData(value, options) {
1398
+ return sanitizeValue(value, 0, { count: 0 }, options);
1399
+ }
1400
+ function sanitizeValue(value, depth, state, options) {
1401
+ state.count += 1;
1402
+ if (state.count > options.maxNodes) {
1403
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1404
+ }
1405
+ if (depth > options.maxDepth) {
1406
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1407
+ }
1408
+ if (Array.isArray(value)) {
1409
+ const sanitized2 = [];
1410
+ for (const entry of value) {
1411
+ sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
1412
+ }
1413
+ return sanitized2;
1414
+ }
1415
+ if (!isPlainObject(value)) {
1416
+ return value;
1417
+ }
1418
+ const sanitized = options.createObject?.() ?? {};
1419
+ for (const [key, entry] of Object.entries(value)) {
1420
+ if (DANGEROUS_KEYS.has(key)) {
1421
+ continue;
1422
+ }
1423
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1424
+ }
1425
+ return sanitized;
1426
+ }
1427
+ function isPlainObject(value) {
1428
+ return Object.prototype.toString.call(value) === "[object Object]";
1429
+ }
1430
+
1431
+ // ../../src/internal/CacheStackSnapshotManager.ts
1432
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1433
+ var CacheStackSnapshotManager = class {
1434
+ constructor(options) {
1435
+ this.options = options;
1436
+ }
1437
+ options;
1438
+ async exportState(maxEntries) {
1439
+ const entries = [];
1440
+ await this.visitExportEntries(maxEntries, async (entry) => {
1441
+ entries.push(entry);
1442
+ });
1443
+ return entries;
1444
+ }
1445
+ async importState(entries) {
1446
+ const normalizedEntries = entries.map((entry) => ({
1447
+ key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
1448
+ value: entry.value,
1449
+ ttl: entry.ttl
1450
+ }));
1451
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1452
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1453
+ await Promise.all(
1454
+ batch.map(async (entry) => {
1455
+ await Promise.all(
1456
+ this.options.layers.map(async (layer) => {
1457
+ if (this.options.shouldSkipLayer(layer)) return;
1458
+ try {
1459
+ await layer.set(entry.key, entry.value, entry.ttl);
1460
+ } catch (error) {
1461
+ await this.options.handleLayerFailure(layer, "write", error);
1462
+ }
1463
+ })
1464
+ );
1465
+ await this.options.tagIndex.touch(entry.key);
1466
+ })
1467
+ );
1468
+ }
1469
+ }
1470
+ async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1471
+ const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1472
+ const tempPath = import_node_path.default.join(
1473
+ import_node_path.default.dirname(targetPath),
1474
+ `.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
1475
+ );
1476
+ let handle;
1477
+ try {
1478
+ handle = await import_node_fs.promises.open(tempPath, "wx");
1479
+ const openedHandle = handle;
1480
+ await openedHandle.writeFile("[", "utf8");
1481
+ let wroteAny = false;
1482
+ await this.visitExportEntries(maxEntries, async (entry) => {
1483
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1484
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1485
+ wroteAny = true;
1486
+ });
1487
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1488
+ await openedHandle.close();
1489
+ handle = void 0;
1490
+ await import_node_fs.promises.rename(tempPath, targetPath);
1491
+ } catch (error) {
1492
+ await handle?.close().catch(() => void 0);
1493
+ await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
1494
+ throw error;
1495
+ }
1496
+ }
1497
+ async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1498
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1499
+ const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
1500
+ let raw;
1501
+ try {
1502
+ if (maxBytes !== false) {
1503
+ const stat = await handle.stat();
1504
+ if (stat.size > maxBytes) {
1505
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1506
+ }
1507
+ }
1508
+ raw = await readUtf8HandleWithLimit(handle, maxBytes);
1509
+ } finally {
1510
+ await handle.close();
1511
+ }
1512
+ let parsed;
1513
+ try {
1514
+ parsed = JSON.parse(raw);
1515
+ } catch (cause) {
1516
+ throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1517
+ }
1518
+ if (!this.isCacheSnapshotEntries(parsed)) {
1519
+ throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1520
+ }
1521
+ await this.importState(
1522
+ parsed.map((entry) => ({
1523
+ key: entry.key,
1524
+ value: this.sanitizeSnapshotValue(entry.value),
1525
+ ttl: entry.ttl
1526
+ }))
1527
+ );
1138
1528
  }
1139
- return Math.max(...values);
1140
- }
1141
- function normalizePositiveSeconds(value) {
1142
- if (!value || value <= 0) {
1143
- return void 0;
1529
+ async visitExportEntries(maxEntries, visitor) {
1530
+ const exported = /* @__PURE__ */ new Set();
1531
+ for (const layer of this.options.layers) {
1532
+ if (!layer.keys && !layer.forEachKey) {
1533
+ continue;
1534
+ }
1535
+ const visitKey = async (key) => {
1536
+ const exportedKey = this.options.stripQualifiedKey(key);
1537
+ if (exported.has(exportedKey)) {
1538
+ return;
1539
+ }
1540
+ const stored = await this.options.readLayerEntry(layer, key);
1541
+ if (stored === null) {
1542
+ return;
1543
+ }
1544
+ exported.add(exportedKey);
1545
+ if (maxEntries !== false && exported.size > maxEntries) {
1546
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1547
+ }
1548
+ await visitor({
1549
+ key: exportedKey,
1550
+ value: stored,
1551
+ ttl: remainingStoredTtlSeconds(stored)
1552
+ });
1553
+ };
1554
+ if (layer.forEachKey) {
1555
+ await layer.forEachKey(visitKey);
1556
+ continue;
1557
+ }
1558
+ const keys = await layer.keys?.();
1559
+ for (const key of keys ?? []) {
1560
+ await visitKey(key);
1561
+ }
1562
+ }
1144
1563
  }
1145
- return value;
1146
- }
1147
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1148
- if (value == null) {
1149
- return true;
1564
+ isCacheSnapshotEntries(value) {
1565
+ return Array.isArray(value) && value.every((entry) => {
1566
+ if (!entry || typeof entry !== "object") {
1567
+ return false;
1568
+ }
1569
+ const candidate = entry;
1570
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1571
+ });
1150
1572
  }
1151
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1152
- }
1153
-
1154
- // ../../src/internal/CacheStackRuntimePolicy.ts
1155
- function shouldSkipLayer(degradedUntil, now = Date.now()) {
1156
- return degradedUntil !== void 0 && degradedUntil > now;
1157
- }
1158
- function shouldStartBackgroundRefresh({
1159
- isDisconnecting,
1160
- hasRefreshInFlight
1161
- }) {
1162
- return !isDisconnecting && !hasRefreshInFlight;
1163
- }
1164
- function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1165
- if (!gracefulDegradation) {
1166
- return { degrade: false };
1573
+ sanitizeSnapshotValue(value) {
1574
+ const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1575
+ return sanitizeStructuredData(roundTripped, {
1576
+ label: "Snapshot value",
1577
+ maxDepth: 64,
1578
+ maxNodes: 1e4,
1579
+ createObject: () => /* @__PURE__ */ Object.create(null)
1580
+ });
1167
1581
  }
1168
- const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1169
- return {
1170
- degrade: true,
1171
- degradedUntil: now + retryAfterMs
1172
- };
1173
- }
1174
- function planFreshReadPolicies({
1175
- stored,
1176
- hasFetcher,
1177
- slidingTtl,
1178
- refreshAheadSeconds
1179
- }) {
1180
- const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1181
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1182
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1183
- return {
1184
- refreshedStored,
1185
- refreshedStoredTtl,
1186
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1187
- };
1188
- }
1582
+ };
1189
1583
 
1190
1584
  // ../../src/internal/CacheStackValidation.ts
1191
1585
  var MAX_CACHE_KEY_LENGTH = 1024;
@@ -1414,7 +1808,11 @@ var FetchRateLimiter = class {
1414
1808
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
1415
1809
  nextFetcherBucketId = 0;
1416
1810
  drainTimer;
1811
+ isDisposed = false;
1417
1812
  async schedule(options, context, task) {
1813
+ if (this.isDisposed) {
1814
+ throw new Error("FetchRateLimiter has been disposed.");
1815
+ }
1418
1816
  if (!options) {
1419
1817
  return task();
1420
1818
  }
@@ -1437,6 +1835,27 @@ var FetchRateLimiter = class {
1437
1835
  this.drain();
1438
1836
  });
1439
1837
  }
1838
+ dispose() {
1839
+ this.isDisposed = true;
1840
+ if (this.drainTimer) {
1841
+ clearTimeout(this.drainTimer);
1842
+ this.drainTimer = void 0;
1843
+ }
1844
+ for (const bucket of this.buckets.values()) {
1845
+ if (bucket.cleanupTimer) {
1846
+ clearTimeout(bucket.cleanupTimer);
1847
+ bucket.cleanupTimer = void 0;
1848
+ }
1849
+ }
1850
+ for (const queue of this.queuesByBucket.values()) {
1851
+ for (const item of queue) {
1852
+ item.reject(new Error("FetchRateLimiter has been disposed."));
1853
+ }
1854
+ }
1855
+ this.queuesByBucket.clear();
1856
+ this.pendingBuckets.clear();
1857
+ this.buckets.clear();
1858
+ }
1440
1859
  normalize(options) {
1441
1860
  const maxConcurrent = options.maxConcurrent;
1442
1861
  const intervalMs = options.intervalMs;
@@ -1472,6 +1891,9 @@ var FetchRateLimiter = class {
1472
1891
  return "global";
1473
1892
  }
1474
1893
  drain() {
1894
+ if (this.isDisposed) {
1895
+ return;
1896
+ }
1475
1897
  if (this.drainTimer) {
1476
1898
  clearTimeout(this.drainTimer);
1477
1899
  this.drainTimer = void 0;
@@ -1535,7 +1957,13 @@ var FetchRateLimiter = class {
1535
1957
  this.pendingBuckets.add(next.bucketKey);
1536
1958
  }
1537
1959
  this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
1538
- this.drain();
1960
+ if (!this.drainTimer) {
1961
+ this.drainTimer = setTimeout(() => {
1962
+ this.drainTimer = void 0;
1963
+ this.drain();
1964
+ }, 0);
1965
+ this.drainTimer.unref?.();
1966
+ }
1539
1967
  });
1540
1968
  }
1541
1969
  }
@@ -1568,12 +1996,18 @@ var FetchRateLimiter = class {
1568
1996
  }
1569
1997
  }
1570
1998
  bucketState(bucketKey) {
1999
+ if (this.isDisposed) {
2000
+ throw new Error("FetchRateLimiter has been disposed.");
2001
+ }
1571
2002
  const existing = this.buckets.get(bucketKey);
1572
2003
  if (existing) {
1573
2004
  return existing;
1574
2005
  }
1575
2006
  if (this.buckets.size >= MAX_BUCKETS) {
1576
2007
  this.evictIdleBuckets();
2008
+ if (this.buckets.size >= MAX_BUCKETS) {
2009
+ throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
2010
+ }
1577
2011
  }
1578
2012
  const bucket = { active: 0, startedAt: [] };
1579
2013
  this.buckets.set(bucketKey, bucket);
@@ -2026,19 +2460,19 @@ var TagIndex = class {
2026
2460
  if (!this.knownKeys.delete(key)) {
2027
2461
  return;
2028
2462
  }
2029
- const path = [];
2463
+ const path2 = [];
2030
2464
  let node = this.root;
2031
2465
  for (const character of key) {
2032
2466
  const child = node.children.get(character);
2033
2467
  if (!child) {
2034
2468
  return;
2035
2469
  }
2036
- path.push([node, character]);
2470
+ path2.push([node, character]);
2037
2471
  node = child;
2038
2472
  }
2039
2473
  node.terminal = false;
2040
- for (let index = path.length - 1; index >= 0; index -= 1) {
2041
- const entry = path[index];
2474
+ for (let index = path2.length - 1; index >= 0; index -= 1) {
2475
+ const entry = path2[index];
2042
2476
  if (!entry) {
2043
2477
  continue;
2044
2478
  }
@@ -2053,44 +2487,19 @@ var TagIndex = class {
2053
2487
  };
2054
2488
 
2055
2489
  // ../../src/serialization/JsonSerializer.ts
2056
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2057
- var MAX_SANITIZE_NODES = 1e4;
2058
2490
  var JsonSerializer = class {
2059
2491
  serialize(value) {
2060
2492
  return JSON.stringify(value);
2061
2493
  }
2062
2494
  deserialize(payload) {
2063
2495
  const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2064
- return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
2496
+ return sanitizeStructuredData(JSON.parse(normalized), {
2497
+ label: "JSON payload",
2498
+ maxDepth: 200,
2499
+ maxNodes: 1e4
2500
+ });
2065
2501
  }
2066
2502
  };
2067
- var MAX_SANITIZE_DEPTH = 200;
2068
- function sanitizeJsonValue(value, depth, state) {
2069
- state.count += 1;
2070
- if (state.count > MAX_SANITIZE_NODES) {
2071
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
2072
- }
2073
- if (depth > MAX_SANITIZE_DEPTH) {
2074
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
2075
- }
2076
- if (Array.isArray(value)) {
2077
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
2078
- }
2079
- if (!isPlainObject(value)) {
2080
- return value;
2081
- }
2082
- const sanitized = {};
2083
- for (const [key, entry] of Object.entries(value)) {
2084
- if (DANGEROUS_JSON_KEYS.has(key)) {
2085
- continue;
2086
- }
2087
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
2088
- }
2089
- return sanitized;
2090
- }
2091
- function isPlainObject(value) {
2092
- return Object.prototype.toString.call(value) === "[object Object]";
2093
- }
2094
2503
 
2095
2504
  // ../../src/stampede/StampedeGuard.ts
2096
2505
  var StampedeGuard = class {
@@ -2135,7 +2544,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2135
2544
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2136
2545
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2137
2546
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2138
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
2139
2547
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
2140
2548
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
2141
2549
  var DebugLogger = class {
@@ -2192,6 +2600,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
2192
2600
  await this.handleLayerFailure(layer, operation, error);
2193
2601
  }
2194
2602
  });
2603
+ this.invalidation = new CacheStackInvalidationSupport({
2604
+ tagIndex: this.tagIndex,
2605
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2606
+ handleLayerFailure: async (layer, operation, error) => {
2607
+ await this.handleLayerFailure(layer, operation, error);
2608
+ }
2609
+ });
2610
+ this.layerWriter = new CacheStackLayerWriter({
2611
+ layers: this.layers,
2612
+ maintenance: this.maintenance,
2613
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2614
+ shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
2615
+ handleLayerFailure: async (layer, operation, error) => {
2616
+ await this.handleLayerFailure(layer, operation, error);
2617
+ },
2618
+ enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2619
+ resolveFreshTtl: this.resolveFreshTtl.bind(this),
2620
+ resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2621
+ globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2622
+ globalStaleIfError: this.options.staleIfError,
2623
+ writePolicy: this.options.writePolicy,
2624
+ onWriteFailures: (context, failures) => {
2625
+ this.metricsCollector.increment("writeFailures", failures.length);
2626
+ this.logger.debug?.("write-failure", {
2627
+ ...context,
2628
+ failures: failures.map((failure) => this.formatError(failure))
2629
+ });
2630
+ }
2631
+ });
2195
2632
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
2196
2633
  this.logger.warn?.(
2197
2634
  "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."
@@ -2207,6 +2644,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2207
2644
  "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
2208
2645
  );
2209
2646
  }
2647
+ this.snapshots = new CacheStackSnapshotManager({
2648
+ layers: this.layers,
2649
+ tagIndex: this.tagIndex,
2650
+ snapshotSerializer: this.snapshotSerializer,
2651
+ readLayerEntry: this.readLayerEntry.bind(this),
2652
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2653
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2654
+ qualifyKey: this.qualifyKey.bind(this),
2655
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
2656
+ validateCacheKey,
2657
+ formatError: this.formatError.bind(this)
2658
+ });
2210
2659
  this.initializeWriteBehind(options.writeBehind);
2211
2660
  this.startup = this.initialize();
2212
2661
  }
@@ -2222,11 +2671,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
2222
2671
  keyDiscovery;
2223
2672
  fetchRateLimiter = new FetchRateLimiter();
2224
2673
  snapshotSerializer = new JsonSerializer();
2674
+ invalidation;
2675
+ layerWriter;
2676
+ snapshots;
2225
2677
  backgroundRefreshes = /* @__PURE__ */ new Map();
2678
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
2226
2679
  layerDegradedUntil = /* @__PURE__ */ new Map();
2227
2680
  maintenance = new CacheStackMaintenance();
2228
2681
  ttlResolver;
2229
2682
  circuitBreakerManager;
2683
+ nextOperationId = 0;
2230
2684
  currentGeneration;
2231
2685
  isDisconnecting = false;
2232
2686
  disconnectPromise;
@@ -2237,10 +2691,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
2237
2691
  * and no `fetcher` is provided.
2238
2692
  */
2239
2693
  async get(key, fetcher, options) {
2240
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2241
- this.validateWriteOptions(options);
2242
- await this.awaitStartup("get");
2243
- return this.getPrepared(normalizedKey, fetcher, options);
2694
+ return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
2695
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2696
+ this.validateWriteOptions(options);
2697
+ await this.awaitStartup("get");
2698
+ return this.getPrepared(normalizedKey, fetcher, options);
2699
+ });
2244
2700
  }
2245
2701
  async getPrepared(normalizedKey, fetcher, options) {
2246
2702
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
@@ -2362,23 +2818,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
2362
2818
  * Stores a value in all cache layers. Overwrites any existing value.
2363
2819
  */
2364
2820
  async set(key, value, options) {
2365
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2366
- this.validateWriteOptions(options);
2367
- await this.awaitStartup("set");
2368
- await this.storeEntry(normalizedKey, "value", value, options);
2821
+ await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2822
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2823
+ this.validateWriteOptions(options);
2824
+ await this.awaitStartup("set");
2825
+ await this.storeEntry(normalizedKey, "value", value, options);
2826
+ });
2369
2827
  }
2370
2828
  /**
2371
2829
  * Deletes the key from all layers and publishes an invalidation message.
2372
2830
  */
2373
2831
  async delete(key) {
2374
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2375
- await this.awaitStartup("delete");
2376
- await this.deleteKeys([normalizedKey]);
2377
- await this.publishInvalidation({
2378
- scope: "key",
2379
- keys: [normalizedKey],
2380
- sourceId: this.instanceId,
2381
- operation: "delete"
2832
+ await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2833
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2834
+ await this.awaitStartup("delete");
2835
+ await this.deleteKeys([normalizedKey]);
2836
+ await this.publishInvalidation({
2837
+ scope: "key",
2838
+ keys: [normalizedKey],
2839
+ sourceId: this.instanceId,
2840
+ operation: "delete"
2841
+ });
2382
2842
  });
2383
2843
  }
2384
2844
  async clear() {
@@ -2411,95 +2871,102 @@ var CacheStack = class extends import_node_events.EventEmitter {
2411
2871
  });
2412
2872
  }
2413
2873
  async mget(entries) {
2414
- this.assertActive("mget");
2415
- if (entries.length === 0) {
2416
- return [];
2417
- }
2418
- const normalizedEntries = entries.map((entry) => ({
2419
- ...entry,
2420
- key: this.qualifyKey(validateCacheKey(entry.key))
2421
- }));
2422
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2423
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2424
- if (!canFastPath) {
2874
+ return this.observeOperation("layercache.mget", void 0, async () => {
2875
+ this.assertActive("mget");
2876
+ if (entries.length === 0) {
2877
+ return [];
2878
+ }
2879
+ const normalizedEntries = entries.map((entry) => ({
2880
+ ...entry,
2881
+ key: this.qualifyKey(validateCacheKey(entry.key))
2882
+ }));
2883
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2884
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2885
+ if (!canFastPath) {
2886
+ await this.awaitStartup("mget");
2887
+ const pendingReads = /* @__PURE__ */ new Map();
2888
+ return Promise.all(
2889
+ normalizedEntries.map((entry) => {
2890
+ const optionsSignature = serializeOptions(entry.options);
2891
+ const existing = pendingReads.get(entry.key);
2892
+ if (!existing) {
2893
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2894
+ pendingReads.set(entry.key, {
2895
+ promise,
2896
+ fetch: entry.fetch,
2897
+ optionsSignature
2898
+ });
2899
+ return promise;
2900
+ }
2901
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2902
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2903
+ }
2904
+ return existing.promise;
2905
+ })
2906
+ );
2907
+ }
2425
2908
  await this.awaitStartup("mget");
2426
- const pendingReads = /* @__PURE__ */ new Map();
2427
- return Promise.all(
2428
- normalizedEntries.map((entry) => {
2429
- const optionsSignature = serializeOptions(entry.options);
2430
- const existing = pendingReads.get(entry.key);
2431
- if (!existing) {
2432
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2433
- pendingReads.set(entry.key, {
2434
- promise,
2435
- fetch: entry.fetch,
2436
- optionsSignature
2437
- });
2438
- return promise;
2909
+ const pending = /* @__PURE__ */ new Set();
2910
+ const indexesByKey = /* @__PURE__ */ new Map();
2911
+ const resultsByKey = /* @__PURE__ */ new Map();
2912
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
2913
+ const entry = normalizedEntries[index];
2914
+ if (!entry) continue;
2915
+ const key = entry.key;
2916
+ const indexes = indexesByKey.get(key) ?? [];
2917
+ indexes.push(index);
2918
+ indexesByKey.set(key, indexes);
2919
+ pending.add(key);
2920
+ }
2921
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2922
+ const layer = this.layers[layerIndex];
2923
+ if (!layer || this.shouldSkipLayer(layer)) continue;
2924
+ const keys = [...pending];
2925
+ if (keys.length === 0) {
2926
+ break;
2927
+ }
2928
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2929
+ for (let offset = 0; offset < values.length; offset += 1) {
2930
+ const key = keys[offset];
2931
+ const stored = values[offset];
2932
+ if (!key || stored === null) {
2933
+ continue;
2439
2934
  }
2440
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2441
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2935
+ const resolved = resolveStoredValue(stored);
2936
+ if (resolved.state === "expired") {
2937
+ await layer.delete(key);
2938
+ continue;
2442
2939
  }
2443
- return existing.promise;
2444
- })
2445
- );
2446
- }
2447
- await this.awaitStartup("mget");
2448
- const pending = /* @__PURE__ */ new Set();
2449
- const indexesByKey = /* @__PURE__ */ new Map();
2450
- const resultsByKey = /* @__PURE__ */ new Map();
2451
- for (let index = 0; index < normalizedEntries.length; index += 1) {
2452
- const entry = normalizedEntries[index];
2453
- if (!entry) continue;
2454
- const key = entry.key;
2455
- const indexes = indexesByKey.get(key) ?? [];
2456
- indexes.push(index);
2457
- indexesByKey.set(key, indexes);
2458
- pending.add(key);
2459
- }
2460
- for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2461
- const layer = this.layers[layerIndex];
2462
- if (!layer) continue;
2463
- const keys = [...pending];
2464
- if (keys.length === 0) {
2465
- break;
2466
- }
2467
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2468
- for (let offset = 0; offset < values.length; offset += 1) {
2469
- const key = keys[offset];
2470
- const stored = values[offset];
2471
- if (!key || stored === null) {
2472
- continue;
2473
- }
2474
- const resolved = resolveStoredValue(stored);
2475
- if (resolved.state === "expired") {
2476
- await layer.delete(key);
2477
- continue;
2940
+ if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
2941
+ this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2942
+ }
2943
+ await this.tagIndex.touch(key);
2944
+ await this.backfill(key, stored, layerIndex - 1);
2945
+ resultsByKey.set(key, resolved.value);
2946
+ pending.delete(key);
2947
+ this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2478
2948
  }
2479
- await this.tagIndex.touch(key);
2480
- await this.backfill(key, stored, layerIndex - 1);
2481
- resultsByKey.set(key, resolved.value);
2482
- pending.delete(key);
2483
- this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2484
2949
  }
2485
- }
2486
- if (pending.size > 0) {
2487
- for (const key of pending) {
2488
- await this.tagIndex.remove(key);
2489
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2950
+ if (pending.size > 0) {
2951
+ for (const key of pending) {
2952
+ await this.tagIndex.remove(key);
2953
+ this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2954
+ }
2490
2955
  }
2491
- }
2492
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2956
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2957
+ });
2493
2958
  }
2494
2959
  async mset(entries) {
2495
- this.assertActive("mset");
2496
- const normalizedEntries = entries.map((entry) => ({
2497
- ...entry,
2498
- key: this.qualifyKey(validateCacheKey(entry.key))
2499
- }));
2500
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2501
- await this.awaitStartup("mset");
2502
- await this.writeBatch(normalizedEntries);
2960
+ await this.observeOperation("layercache.mset", void 0, async () => {
2961
+ this.assertActive("mset");
2962
+ const normalizedEntries = entries.map((entry) => ({
2963
+ ...entry,
2964
+ key: this.qualifyKey(validateCacheKey(entry.key))
2965
+ }));
2966
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2967
+ await this.awaitStartup("mset");
2968
+ await this.writeBatch(normalizedEntries);
2969
+ });
2503
2970
  }
2504
2971
  async warm(entries, options = {}) {
2505
2972
  this.assertActive("warm");
@@ -2552,40 +3019,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
2552
3019
  return new CacheNamespace(this, prefix);
2553
3020
  }
2554
3021
  async invalidateByTag(tag) {
2555
- validateTag(tag);
2556
- await this.awaitStartup("invalidateByTag");
2557
- const keys = await this.collectKeysForTag(tag);
2558
- await this.deleteKeys(keys);
2559
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3022
+ await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
3023
+ validateTag(tag);
3024
+ await this.awaitStartup("invalidateByTag");
3025
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
3026
+ await this.deleteKeys(keys);
3027
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3028
+ });
2560
3029
  }
2561
3030
  async invalidateByTags(tags, mode = "any") {
2562
- if (tags.length === 0) {
2563
- return;
2564
- }
2565
- validateTags(tags);
2566
- await this.awaitStartup("invalidateByTags");
2567
- const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
2568
- const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2569
- this.assertWithinInvalidationKeyLimit(keys.length);
2570
- await this.deleteKeys(keys);
2571
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3031
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
3032
+ if (tags.length === 0) {
3033
+ return;
3034
+ }
3035
+ validateTags(tags);
3036
+ await this.awaitStartup("invalidateByTags");
3037
+ const keysByTag = await Promise.all(
3038
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
3039
+ );
3040
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
3041
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
3042
+ await this.deleteKeys(keys);
3043
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3044
+ });
2572
3045
  }
2573
3046
  async invalidateByPattern(pattern) {
2574
- validatePattern(pattern);
2575
- await this.awaitStartup("invalidateByPattern");
2576
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2577
- this.qualifyPattern(pattern),
2578
- this.invalidationMaxKeys()
2579
- );
2580
- await this.deleteKeys(keys);
2581
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3047
+ await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
3048
+ validatePattern(pattern);
3049
+ await this.awaitStartup("invalidateByPattern");
3050
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
3051
+ this.qualifyPattern(pattern),
3052
+ this.invalidationMaxKeys()
3053
+ );
3054
+ await this.deleteKeys(keys);
3055
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3056
+ });
2582
3057
  }
2583
3058
  async invalidateByPrefix(prefix) {
2584
- await this.awaitStartup("invalidateByPrefix");
2585
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2586
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2587
- await this.deleteKeys(keys);
2588
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3059
+ await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
3060
+ await this.awaitStartup("invalidateByPrefix");
3061
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
3062
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
3063
+ await this.deleteKeys(keys);
3064
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3065
+ });
2589
3066
  }
2590
3067
  getMetrics() {
2591
3068
  return this.metricsCollector.snapshot;
@@ -2696,95 +3173,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
2696
3173
  }
2697
3174
  async exportState() {
2698
3175
  await this.awaitStartup("exportState");
2699
- const entries = [];
2700
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2701
- entries.push(entry);
2702
- });
2703
- return entries;
3176
+ return this.snapshots.exportState(this.snapshotMaxEntries());
2704
3177
  }
2705
3178
  async importState(entries) {
2706
3179
  await this.awaitStartup("importState");
2707
- const normalizedEntries = entries.map((entry) => ({
2708
- key: this.qualifyKey(validateCacheKey(entry.key)),
2709
- value: entry.value,
2710
- ttl: entry.ttl
2711
- }));
2712
- for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
2713
- const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
2714
- await Promise.all(
2715
- batch.map(async (entry) => {
2716
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
2717
- await this.tagIndex.touch(entry.key);
2718
- })
2719
- );
2720
- }
3180
+ await this.snapshots.importState(entries);
2721
3181
  }
2722
3182
  async persistToFile(filePath) {
2723
3183
  this.assertActive("persistToFile");
2724
- const { promises: fs } = await import("fs");
2725
- const path = await import("path");
2726
- const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
2727
- const tempPath = path.join(
2728
- path.dirname(targetPath),
2729
- `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
2730
- );
2731
- let handle;
2732
- try {
2733
- handle = await fs.open(tempPath, "wx");
2734
- const openedHandle = handle;
2735
- await openedHandle.writeFile("[", "utf8");
2736
- let wroteAny = false;
2737
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
2738
- await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
2739
- await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
2740
- wroteAny = true;
2741
- });
2742
- await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
2743
- await openedHandle.close();
2744
- handle = void 0;
2745
- await fs.rename(tempPath, targetPath);
2746
- } catch (error) {
2747
- await handle?.close().catch(() => void 0);
2748
- await fs.unlink(tempPath).catch(() => void 0);
2749
- throw error;
2750
- }
3184
+ await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
2751
3185
  }
2752
3186
  async restoreFromFile(filePath) {
2753
3187
  this.assertActive("restoreFromFile");
2754
- const { promises: fs, constants } = await import("fs");
2755
- const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
2756
- const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
2757
- const snapshotMaxBytes = this.snapshotMaxBytes();
2758
- let raw;
2759
- try {
2760
- if (snapshotMaxBytes !== false) {
2761
- const stat = await handle.stat();
2762
- if (stat.size > snapshotMaxBytes) {
2763
- throw new Error(
2764
- `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
2765
- );
2766
- }
2767
- }
2768
- raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
2769
- } finally {
2770
- await handle.close();
2771
- }
2772
- let parsed;
2773
- try {
2774
- parsed = JSON.parse(raw);
2775
- } catch (cause) {
2776
- throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
2777
- }
2778
- if (!this.isCacheSnapshotEntries(parsed)) {
2779
- throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
2780
- }
2781
- await this.importState(
2782
- parsed.map((entry) => ({
2783
- key: entry.key,
2784
- value: this.sanitizeSnapshotValue(entry.value),
2785
- ttl: entry.ttl
2786
- }))
2787
- );
3188
+ await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
2788
3189
  }
2789
3190
  async disconnect() {
2790
3191
  if (!this.disconnectPromise) {
@@ -2794,8 +3195,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
2794
3195
  await this.unsubscribeInvalidation?.();
2795
3196
  await this.flushWriteBehindQueue();
2796
3197
  await this.maintenance.waitForGenerationCleanup();
2797
- await Promise.allSettled([...this.backgroundRefreshes.values()]);
3198
+ for (const key of this.backgroundRefreshAbort.keys()) {
3199
+ this.backgroundRefreshAbort.set(key, true);
3200
+ }
3201
+ await Promise.allSettled(
3202
+ [...this.backgroundRefreshes.values()].map((promise) => {
3203
+ let timer;
3204
+ return Promise.race([
3205
+ promise,
3206
+ new Promise((resolve) => {
3207
+ timer = setTimeout(resolve, 5e3);
3208
+ timer.unref?.();
3209
+ })
3210
+ ]).finally(() => {
3211
+ if (timer) clearTimeout(timer);
3212
+ });
3213
+ })
3214
+ );
3215
+ this.backgroundRefreshes.clear();
3216
+ this.backgroundRefreshAbort.clear();
2798
3217
  this.maintenance.disposeWriteBehindTimer();
3218
+ this.fetchRateLimiter.dispose();
2799
3219
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2800
3220
  })();
2801
3221
  }
@@ -2909,7 +3329,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2909
3329
  async storeEntry(key, kind, value, options) {
2910
3330
  const clearEpoch = this.maintenance.currentClearEpoch();
2911
3331
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
2912
- await this.writeAcrossLayers(key, kind, value, options);
3332
+ await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2913
3333
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2914
3334
  return;
2915
3335
  }
@@ -2926,52 +3346,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2926
3346
  }
2927
3347
  }
2928
3348
  async writeBatch(entries) {
2929
- const now = Date.now();
2930
- const clearEpoch = this.maintenance.currentClearEpoch();
2931
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2932
- const entriesByLayer = /* @__PURE__ */ new Map();
2933
- const immediateOperations = [];
2934
- const deferredOperations = [];
2935
- for (const entry of entries) {
2936
- for (const layer of this.layers) {
2937
- if (this.shouldSkipLayer(layer)) {
2938
- continue;
2939
- }
2940
- const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
2941
- const bucket = entriesByLayer.get(layer) ?? [];
2942
- bucket.push(layerEntry);
2943
- entriesByLayer.set(layer, bucket);
2944
- }
2945
- }
2946
- for (const [layer, layerEntries] of entriesByLayer.entries()) {
2947
- const operation = async () => {
2948
- if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2949
- return;
2950
- }
2951
- const activeEntries = layerEntries.filter(
2952
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2953
- );
2954
- if (activeEntries.length === 0) {
2955
- return;
2956
- }
2957
- try {
2958
- if (layer.setMany) {
2959
- await layer.setMany(activeEntries);
2960
- return;
2961
- }
2962
- await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2963
- } catch (error) {
2964
- await this.handleLayerFailure(layer, "write", error);
2965
- }
2966
- };
2967
- if (this.shouldWriteBehind(layer)) {
2968
- deferredOperations.push(operation);
2969
- } else {
2970
- immediateOperations.push(operation);
2971
- }
2972
- }
2973
- await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2974
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
3349
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
2975
3350
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2976
3351
  return;
2977
3352
  }
@@ -3078,58 +3453,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3078
3453
  this.emit("backfill", { key, layer: layer.name });
3079
3454
  }
3080
3455
  }
3081
- async writeAcrossLayers(key, kind, value, options) {
3082
- const now = Date.now();
3083
- const clearEpoch = this.maintenance.currentClearEpoch();
3084
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3085
- const immediateOperations = [];
3086
- const deferredOperations = [];
3087
- for (const layer of this.layers) {
3088
- const operation = async () => {
3089
- if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3090
- return;
3091
- }
3092
- if (this.shouldSkipLayer(layer)) {
3093
- return;
3094
- }
3095
- const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
3096
- try {
3097
- await layer.set(entry.key, entry.value, entry.ttl);
3098
- } catch (error) {
3099
- await this.handleLayerFailure(layer, "write", error);
3100
- }
3101
- };
3102
- if (this.shouldWriteBehind(layer)) {
3103
- deferredOperations.push(operation);
3104
- } else {
3105
- immediateOperations.push(operation);
3106
- }
3107
- }
3108
- await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
3109
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
3110
- }
3111
- async executeLayerOperations(operations, context) {
3112
- if (this.options.writePolicy !== "best-effort") {
3113
- await Promise.all(operations.map((operation) => operation()));
3114
- return;
3115
- }
3116
- const results = await Promise.allSettled(operations.map((operation) => operation()));
3117
- const failures = results.filter((result) => result.status === "rejected");
3118
- if (failures.length === 0) {
3119
- return;
3120
- }
3121
- this.metricsCollector.increment("writeFailures", failures.length);
3122
- this.logger.debug?.("write-failure", {
3123
- ...context,
3124
- failures: failures.map((failure) => this.formatError(failure.reason))
3125
- });
3126
- if (failures.length === operations.length) {
3127
- throw new AggregateError(
3128
- failures.map((failure) => failure.reason),
3129
- `${context.action} failed for every cache layer`
3130
- );
3131
- }
3132
- }
3133
3456
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3134
3457
  return this.ttlResolver.resolveFreshTtl(
3135
3458
  key,
@@ -3157,15 +3480,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
3157
3480
  }
3158
3481
  const clearEpoch = this.maintenance.currentClearEpoch();
3159
3482
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3483
+ this.backgroundRefreshAbort.set(key, false);
3160
3484
  const refresh = (async () => {
3161
3485
  this.metricsCollector.increment("refreshes");
3162
3486
  try {
3487
+ if (this.backgroundRefreshAbort.get(key)) return;
3163
3488
  await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3164
3489
  } catch (error) {
3490
+ if (this.backgroundRefreshAbort.get(key)) return;
3165
3491
  this.metricsCollector.increment("refreshErrors");
3166
3492
  this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
3167
3493
  } finally {
3168
3494
  this.backgroundRefreshes.delete(key);
3495
+ this.backgroundRefreshAbort.delete(key);
3169
3496
  }
3170
3497
  })();
3171
3498
  this.backgroundRefreshes.set(key, refresh);
@@ -3195,7 +3522,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3195
3522
  return;
3196
3523
  }
3197
3524
  this.maintenance.bumpKeyEpochs(keys);
3198
- await this.deleteKeysFromLayers(this.layers, keys);
3525
+ await this.invalidation.deleteKeysFromLayers(this.layers, keys);
3199
3526
  for (const key of keys) {
3200
3527
  await this.tagIndex.remove(key);
3201
3528
  this.ttlResolver.deleteProfile(key);
@@ -3227,7 +3554,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3227
3554
  }
3228
3555
  const keys = message.keys ?? [];
3229
3556
  this.maintenance.bumpKeyEpochs(keys);
3230
- await this.deleteKeysFromLayers(localLayers, keys);
3557
+ await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3231
3558
  if (message.operation !== "write") {
3232
3559
  for (const key of keys) {
3233
3560
  await this.tagIndex.remove(key);
@@ -3268,7 +3595,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3268
3595
  timer.unref?.();
3269
3596
  })
3270
3597
  ]);
3271
- if (result && typeof result === "object" && "kind" in result) {
3598
+ if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
3272
3599
  if (result.kind === "error") {
3273
3600
  throw result.error;
3274
3601
  }
@@ -3284,6 +3611,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
3284
3611
  shouldBroadcastL1Invalidation() {
3285
3612
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3286
3613
  }
3614
+ async observeOperation(name, attributes, execute) {
3615
+ const id = this.nextOperationId;
3616
+ this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
3617
+ this.emit("operation-start", { id, name, attributes });
3618
+ try {
3619
+ const result = await execute();
3620
+ this.emit("operation-end", {
3621
+ id,
3622
+ name,
3623
+ attributes,
3624
+ success: true,
3625
+ result: result === null ? "null" : void 0
3626
+ });
3627
+ return result;
3628
+ } catch (error) {
3629
+ this.emit("operation-end", {
3630
+ id,
3631
+ name,
3632
+ attributes,
3633
+ success: false,
3634
+ error
3635
+ });
3636
+ throw error;
3637
+ }
3638
+ }
3287
3639
  scheduleGenerationCleanup(generation) {
3288
3640
  this.maintenance.scheduleGenerationCleanup(
3289
3641
  generation,
@@ -3339,37 +3691,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3339
3691
  });
3340
3692
  this.emitError("write-behind", { failed: failures.length, total: batch.length });
3341
3693
  }
3342
- buildLayerSetEntry(layer, key, kind, value, options, now) {
3343
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
3344
- const staleWhileRevalidate = this.resolveLayerSeconds(
3345
- layer.name,
3346
- options?.staleWhileRevalidate,
3347
- this.options.staleWhileRevalidate
3348
- );
3349
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
3350
- const payload = createStoredValueEnvelope({
3351
- kind,
3352
- value,
3353
- freshTtlSeconds: freshTtl,
3354
- staleWhileRevalidateSeconds: staleWhileRevalidate,
3355
- staleIfErrorSeconds: staleIfError,
3356
- now
3357
- });
3358
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
3359
- return {
3360
- key,
3361
- value: payload,
3362
- ttl
3363
- };
3364
- }
3365
- intersectKeys(groups) {
3366
- if (groups.length === 0) {
3367
- return [];
3368
- }
3369
- const [firstGroup, ...rest] = groups;
3370
- const restSets = rest.map((group) => new Set(group));
3371
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3372
- }
3373
3694
  qualifyKey(key) {
3374
3695
  return qualifyGenerationKey(key, this.currentGeneration);
3375
3696
  }
@@ -3379,32 +3700,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3379
3700
  stripQualifiedKey(key) {
3380
3701
  return stripGenerationPrefix(key, this.currentGeneration);
3381
3702
  }
3382
- async deleteKeysFromLayers(layers, keys) {
3383
- await Promise.all(
3384
- layers.map(async (layer) => {
3385
- if (this.shouldSkipLayer(layer)) {
3386
- return;
3387
- }
3388
- if (layer.deleteMany) {
3389
- try {
3390
- await layer.deleteMany(keys);
3391
- } catch (error) {
3392
- await this.handleLayerFailure(layer, "delete", error);
3393
- }
3394
- return;
3395
- }
3396
- await Promise.all(
3397
- keys.map(async (key) => {
3398
- try {
3399
- await layer.delete(key);
3400
- } catch (error) {
3401
- await this.handleLayerFailure(layer, "delete", error);
3402
- }
3403
- })
3404
- );
3405
- })
3406
- );
3407
- }
3408
3703
  validateConfiguration() {
3409
3704
  if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
3410
3705
  throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
@@ -3535,18 +3830,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3535
3830
  this.emit("error", { operation, ...context });
3536
3831
  }
3537
3832
  }
3538
- isCacheSnapshotEntries(value) {
3539
- return Array.isArray(value) && value.every((entry) => {
3540
- if (!entry || typeof entry !== "object") {
3541
- return false;
3542
- }
3543
- const candidate = entry;
3544
- return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
3545
- });
3546
- }
3547
- sanitizeSnapshotValue(value) {
3548
- return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
3549
- }
3550
3833
  snapshotMaxBytes() {
3551
3834
  return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3552
3835
  }
@@ -3556,62 +3839,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3556
3839
  invalidationMaxKeys() {
3557
3840
  return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3558
3841
  }
3559
- async collectKeysForTag(tag) {
3560
- const keys = /* @__PURE__ */ new Set();
3561
- if (this.tagIndex.forEachKeyForTag) {
3562
- await this.tagIndex.forEachKeyForTag(tag, async (key) => {
3563
- keys.add(key);
3564
- this.assertWithinInvalidationKeyLimit(keys.size);
3565
- });
3566
- return [...keys];
3567
- }
3568
- for (const key of await this.tagIndex.keysForTag(tag)) {
3569
- keys.add(key);
3570
- this.assertWithinInvalidationKeyLimit(keys.size);
3571
- }
3572
- return [...keys];
3573
- }
3574
- assertWithinInvalidationKeyLimit(size) {
3575
- const maxKeys = this.invalidationMaxKeys();
3576
- if (maxKeys !== false && size > maxKeys) {
3577
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
3578
- }
3579
- }
3580
- async visitExportEntries(maxEntries, visitor) {
3581
- const exported = /* @__PURE__ */ new Set();
3582
- for (const layer of this.layers) {
3583
- if (!layer.keys && !layer.forEachKey) {
3584
- continue;
3585
- }
3586
- const visitKey = async (key) => {
3587
- const exportedKey = this.stripQualifiedKey(key);
3588
- if (exported.has(exportedKey)) {
3589
- return;
3590
- }
3591
- const stored = await this.readLayerEntry(layer, key);
3592
- if (stored === null) {
3593
- return;
3594
- }
3595
- exported.add(exportedKey);
3596
- if (maxEntries !== false && exported.size > maxEntries) {
3597
- throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
3598
- }
3599
- await visitor({
3600
- key: exportedKey,
3601
- value: stored,
3602
- ttl: remainingStoredTtlSeconds(stored)
3603
- });
3604
- };
3605
- if (layer.forEachKey) {
3606
- await layer.forEachKey(visitKey);
3607
- continue;
3608
- }
3609
- const keys = await layer.keys?.();
3610
- for (const key of keys ?? []) {
3611
- await visitKey(key);
3612
- }
3613
- }
3614
- }
3615
3842
  };
3616
3843
 
3617
3844
  // src/module.ts