layercache 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.cjs +26 -2
- package/dist/cli.js +26 -2
- package/dist/{edge-BMmPVqaD.d.cts → edge-BXWTKlI1.d.cts} +21 -10
- package/dist/{edge-BMmPVqaD.d.ts → edge-BXWTKlI1.d.ts} +21 -10
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +1016 -834
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +919 -737
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +942 -715
- package/packages/nestjs/dist/index.d.cts +21 -10
- package/packages/nestjs/dist/index.d.ts +21 -10
- package/packages/nestjs/dist/index.js +942 -715
|
@@ -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/
|
|
899
|
-
var
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
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
|
-
|
|
944
|
-
if (
|
|
945
|
-
return
|
|
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
|
-
|
|
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
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
965
|
-
if (
|
|
966
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
2470
|
+
path2.push([node, character]);
|
|
2037
2471
|
node = child;
|
|
2038
2472
|
}
|
|
2039
2473
|
node.terminal = false;
|
|
2040
|
-
for (let index =
|
|
2041
|
-
const entry =
|
|
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
|
|
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
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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.
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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
|
-
|
|
2441
|
-
|
|
2935
|
+
const resolved = resolveStoredValue(stored);
|
|
2936
|
+
if (resolved.state === "expired") {
|
|
2937
|
+
await layer.delete(key);
|
|
2938
|
+
continue;
|
|
2442
2939
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
2956
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2957
|
+
});
|
|
2493
2958
|
}
|
|
2494
2959
|
async mset(entries) {
|
|
2495
|
-
this.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
this.
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
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.
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|