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
|
@@ -688,7 +688,7 @@ function normalizeForSerialization(value) {
|
|
|
688
688
|
}
|
|
689
689
|
function serializeKeyPart(value) {
|
|
690
690
|
if (typeof value === "string") {
|
|
691
|
-
return `s:${value}`;
|
|
691
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
692
692
|
}
|
|
693
693
|
if (typeof value === "number") {
|
|
694
694
|
return `n:${value}`;
|
|
@@ -716,102 +716,6 @@ function createInstanceId() {
|
|
|
716
716
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
717
717
|
}
|
|
718
718
|
|
|
719
|
-
// ../../src/internal/CacheSnapshotFile.ts
|
|
720
|
-
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
721
|
-
const relative = path.relative(realBaseDir, candidatePath);
|
|
722
|
-
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
723
|
-
}
|
|
724
|
-
async function findExistingAncestor(directory, fs, path) {
|
|
725
|
-
let current = directory;
|
|
726
|
-
while (true) {
|
|
727
|
-
try {
|
|
728
|
-
await fs.lstat(current);
|
|
729
|
-
return current;
|
|
730
|
-
} catch (error) {
|
|
731
|
-
if (error.code !== "ENOENT") {
|
|
732
|
-
throw error;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const parent = path.dirname(current);
|
|
736
|
-
if (parent === current) {
|
|
737
|
-
return current;
|
|
738
|
-
}
|
|
739
|
-
current = parent;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
743
|
-
if (filePath.length === 0) {
|
|
744
|
-
throw new Error("filePath must not be empty.");
|
|
745
|
-
}
|
|
746
|
-
if (filePath.includes("\0")) {
|
|
747
|
-
throw new Error("filePath must not contain null bytes.");
|
|
748
|
-
}
|
|
749
|
-
const { promises: fs } = await import("fs");
|
|
750
|
-
const path = await import("path");
|
|
751
|
-
const resolved = path.resolve(filePath);
|
|
752
|
-
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
753
|
-
if (baseDir === false) {
|
|
754
|
-
return resolved;
|
|
755
|
-
}
|
|
756
|
-
await fs.mkdir(baseDir, { recursive: true });
|
|
757
|
-
const realBaseDir = await fs.realpath(baseDir);
|
|
758
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
759
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
760
|
-
}
|
|
761
|
-
if (mode === "read") {
|
|
762
|
-
const realTarget = await fs.realpath(resolved);
|
|
763
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
764
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
765
|
-
}
|
|
766
|
-
return realTarget;
|
|
767
|
-
}
|
|
768
|
-
const parentDir = path.dirname(resolved);
|
|
769
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
770
|
-
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
771
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
772
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
773
|
-
}
|
|
774
|
-
await fs.mkdir(parentDir, { recursive: true });
|
|
775
|
-
const realParentDir = await fs.realpath(parentDir);
|
|
776
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
777
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
778
|
-
}
|
|
779
|
-
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
780
|
-
try {
|
|
781
|
-
const existing = await fs.lstat(targetPath);
|
|
782
|
-
if (existing.isSymbolicLink()) {
|
|
783
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
784
|
-
}
|
|
785
|
-
} catch (error) {
|
|
786
|
-
if (error.code !== "ENOENT") {
|
|
787
|
-
throw error;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return targetPath;
|
|
791
|
-
}
|
|
792
|
-
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
793
|
-
if (byteLimit === false) {
|
|
794
|
-
return handle.readFile({ encoding: "utf8" });
|
|
795
|
-
}
|
|
796
|
-
const chunks = [];
|
|
797
|
-
let totalBytes = 0;
|
|
798
|
-
let position = 0;
|
|
799
|
-
while (true) {
|
|
800
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
801
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
802
|
-
if (bytesRead === 0) {
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
totalBytes += bytesRead;
|
|
806
|
-
if (totalBytes > byteLimit) {
|
|
807
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
808
|
-
}
|
|
809
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
810
|
-
position += bytesRead;
|
|
811
|
-
}
|
|
812
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
813
|
-
}
|
|
814
|
-
|
|
815
719
|
// ../../src/internal/CacheStackGeneration.ts
|
|
816
720
|
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
817
721
|
function generationPrefix(generation) {
|
|
@@ -859,102 +763,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
|
859
763
|
return batches;
|
|
860
764
|
}
|
|
861
765
|
|
|
862
|
-
// ../../src/internal/
|
|
863
|
-
var
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
writeBehindTimer;
|
|
867
|
-
writeBehindFlushPromise;
|
|
868
|
-
generationCleanupPromise;
|
|
869
|
-
clearEpoch = 0;
|
|
870
|
-
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
871
|
-
if (writeStrategy !== "write-behind") {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const flushIntervalMs = options?.flushIntervalMs;
|
|
875
|
-
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
this.disposeWriteBehindTimer();
|
|
879
|
-
this.writeBehindTimer = setInterval(() => {
|
|
880
|
-
void flush();
|
|
881
|
-
}, flushIntervalMs);
|
|
882
|
-
this.writeBehindTimer.unref?.();
|
|
766
|
+
// ../../src/internal/CacheStackInvalidationSupport.ts
|
|
767
|
+
var CacheStackInvalidationSupport = class {
|
|
768
|
+
constructor(options) {
|
|
769
|
+
this.options = options;
|
|
883
770
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
771
|
+
options;
|
|
772
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
773
|
+
const keys = /* @__PURE__ */ new Set();
|
|
774
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
775
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
776
|
+
keys.add(key);
|
|
777
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
778
|
+
});
|
|
779
|
+
return [...keys];
|
|
887
780
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
beginClearEpoch() {
|
|
892
|
-
this.clearEpoch += 1;
|
|
893
|
-
this.keyEpochs.clear();
|
|
894
|
-
this.writeBehindQueue.length = 0;
|
|
895
|
-
}
|
|
896
|
-
currentClearEpoch() {
|
|
897
|
-
return this.clearEpoch;
|
|
898
|
-
}
|
|
899
|
-
currentKeyEpoch(key) {
|
|
900
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
901
|
-
}
|
|
902
|
-
bumpKeyEpochs(keys) {
|
|
903
|
-
for (const key of keys) {
|
|
904
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
781
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
782
|
+
keys.add(key);
|
|
783
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
905
784
|
}
|
|
785
|
+
return [...keys];
|
|
906
786
|
}
|
|
907
|
-
|
|
908
|
-
if (
|
|
909
|
-
return
|
|
910
|
-
}
|
|
911
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
912
|
-
return true;
|
|
787
|
+
intersectKeys(groups) {
|
|
788
|
+
if (groups.length === 0) {
|
|
789
|
+
return [];
|
|
913
790
|
}
|
|
914
|
-
|
|
791
|
+
const [firstGroup, ...rest] = groups;
|
|
792
|
+
const restSets = rest.map((group) => new Set(group));
|
|
793
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
915
794
|
}
|
|
916
|
-
async
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
795
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
796
|
+
await Promise.all(
|
|
797
|
+
layers.map(async (layer) => {
|
|
798
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (layer.deleteMany) {
|
|
802
|
+
try {
|
|
803
|
+
await layer.deleteMany(keys);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
await Promise.all(
|
|
810
|
+
keys.map(async (key) => {
|
|
811
|
+
try {
|
|
812
|
+
await layer.delete(key);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
})
|
|
819
|
+
);
|
|
927
820
|
}
|
|
928
|
-
|
|
929
|
-
if (
|
|
930
|
-
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
const batchSize = options?.batchSize ?? 100;
|
|
934
|
-
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
935
|
-
this.writeBehindFlushPromise = flushBatch(batch);
|
|
936
|
-
try {
|
|
937
|
-
await this.writeBehindFlushPromise;
|
|
938
|
-
} finally {
|
|
939
|
-
this.writeBehindFlushPromise = void 0;
|
|
940
|
-
}
|
|
941
|
-
if (this.writeBehindQueue.length > 0) {
|
|
942
|
-
await this.flushWriteBehindQueue(options, flushBatch);
|
|
821
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
822
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
823
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
943
824
|
}
|
|
944
825
|
}
|
|
945
|
-
scheduleGenerationCleanup(generation, task, onError) {
|
|
946
|
-
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
947
|
-
onError(generation, error);
|
|
948
|
-
});
|
|
949
|
-
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
950
|
-
if (this.generationCleanupPromise === scheduledTask) {
|
|
951
|
-
this.generationCleanupPromise = void 0;
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
async waitForGenerationCleanup() {
|
|
956
|
-
await this.generationCleanupPromise;
|
|
957
|
-
}
|
|
958
826
|
};
|
|
959
827
|
|
|
960
828
|
// ../../src/internal/StoredValue.ts
|
|
@@ -1084,72 +952,598 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
|
1084
952
|
if (!isStoredValueEnvelope(stored)) {
|
|
1085
953
|
return stored;
|
|
1086
954
|
}
|
|
1087
|
-
return createStoredValueEnvelope({
|
|
1088
|
-
kind: stored.kind,
|
|
1089
|
-
value: stored.value,
|
|
1090
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1091
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1092
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1093
|
-
now
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
function maxExpiry(stored) {
|
|
1097
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1098
|
-
(value) => value !== null
|
|
1099
|
-
);
|
|
1100
|
-
if (values.length === 0) {
|
|
1101
|
-
return null;
|
|
955
|
+
return createStoredValueEnvelope({
|
|
956
|
+
kind: stored.kind,
|
|
957
|
+
value: stored.value,
|
|
958
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
959
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
960
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
961
|
+
now
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
function maxExpiry(stored) {
|
|
965
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
966
|
+
(value) => value !== null
|
|
967
|
+
);
|
|
968
|
+
if (values.length === 0) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
return Math.max(...values);
|
|
972
|
+
}
|
|
973
|
+
function normalizePositiveSeconds(value) {
|
|
974
|
+
if (!value || value <= 0) {
|
|
975
|
+
return void 0;
|
|
976
|
+
}
|
|
977
|
+
return value;
|
|
978
|
+
}
|
|
979
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
980
|
+
if (value == null) {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ../../src/internal/CacheStackLayerWriter.ts
|
|
987
|
+
var CacheStackLayerWriter = class {
|
|
988
|
+
constructor(options) {
|
|
989
|
+
this.options = options;
|
|
990
|
+
}
|
|
991
|
+
options;
|
|
992
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
993
|
+
const now = Date.now();
|
|
994
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
995
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
996
|
+
const immediateOperations = [];
|
|
997
|
+
const deferredOperations = [];
|
|
998
|
+
for (const layer of this.options.layers) {
|
|
999
|
+
const operation = async () => {
|
|
1000
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
1007
|
+
try {
|
|
1008
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1014
|
+
deferredOperations.push(operation);
|
|
1015
|
+
} else {
|
|
1016
|
+
immediateOperations.push(operation);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1020
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1021
|
+
}
|
|
1022
|
+
async writeBatch(entries) {
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1025
|
+
const entryEpochs = new Map(
|
|
1026
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
1027
|
+
);
|
|
1028
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1029
|
+
const immediateOperations = [];
|
|
1030
|
+
const deferredOperations = [];
|
|
1031
|
+
for (const entry of entries) {
|
|
1032
|
+
for (const layer of this.options.layers) {
|
|
1033
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1037
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1038
|
+
bucket.push(layerEntry);
|
|
1039
|
+
entriesByLayer.set(layer, bucket);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1043
|
+
const operation = async () => {
|
|
1044
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const activeEntries = layerEntries.filter(
|
|
1048
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
1049
|
+
);
|
|
1050
|
+
if (activeEntries.length === 0) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
if (layer.setMany) {
|
|
1055
|
+
await layer.setMany(activeEntries);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1064
|
+
deferredOperations.push(operation);
|
|
1065
|
+
} else {
|
|
1066
|
+
immediateOperations.push(operation);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1070
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1071
|
+
return { clearEpoch, entryEpochs };
|
|
1072
|
+
}
|
|
1073
|
+
async executeLayerOperations(operations, context) {
|
|
1074
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1075
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1079
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1080
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
1081
|
+
if (failures.length === 0) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
this.options.onWriteFailures(
|
|
1085
|
+
context,
|
|
1086
|
+
failures.map((failure) => failure.reason)
|
|
1087
|
+
);
|
|
1088
|
+
if (failures.length === operations.length) {
|
|
1089
|
+
throw new AggregateError(
|
|
1090
|
+
failures.map((failure) => failure.reason),
|
|
1091
|
+
`${context.action} failed for every cache layer`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
1096
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
1097
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
1098
|
+
layer.name,
|
|
1099
|
+
writeOptions?.staleWhileRevalidate,
|
|
1100
|
+
this.options.globalStaleWhileRevalidate
|
|
1101
|
+
);
|
|
1102
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
1103
|
+
layer.name,
|
|
1104
|
+
writeOptions?.staleIfError,
|
|
1105
|
+
this.options.globalStaleIfError
|
|
1106
|
+
);
|
|
1107
|
+
const payload = createStoredValueEnvelope({
|
|
1108
|
+
kind,
|
|
1109
|
+
value,
|
|
1110
|
+
freshTtlSeconds: freshTtl,
|
|
1111
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1112
|
+
staleIfErrorSeconds: staleIfError,
|
|
1113
|
+
now
|
|
1114
|
+
});
|
|
1115
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1116
|
+
return {
|
|
1117
|
+
key,
|
|
1118
|
+
value: payload,
|
|
1119
|
+
ttl
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
1125
|
+
var CacheStackMaintenance = class {
|
|
1126
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1127
|
+
writeBehindQueue = [];
|
|
1128
|
+
writeBehindTimer;
|
|
1129
|
+
writeBehindFlushPromise;
|
|
1130
|
+
generationCleanupPromise;
|
|
1131
|
+
clearEpoch = 0;
|
|
1132
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
1133
|
+
if (writeStrategy !== "write-behind") {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1137
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
this.disposeWriteBehindTimer();
|
|
1141
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1142
|
+
void flush();
|
|
1143
|
+
}, flushIntervalMs);
|
|
1144
|
+
this.writeBehindTimer.unref?.();
|
|
1145
|
+
}
|
|
1146
|
+
disposeWriteBehindTimer() {
|
|
1147
|
+
if (!this.writeBehindTimer) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
clearInterval(this.writeBehindTimer);
|
|
1151
|
+
this.writeBehindTimer = void 0;
|
|
1152
|
+
}
|
|
1153
|
+
beginClearEpoch() {
|
|
1154
|
+
this.clearEpoch += 1;
|
|
1155
|
+
this.keyEpochs.clear();
|
|
1156
|
+
this.writeBehindQueue.length = 0;
|
|
1157
|
+
}
|
|
1158
|
+
currentClearEpoch() {
|
|
1159
|
+
return this.clearEpoch;
|
|
1160
|
+
}
|
|
1161
|
+
currentKeyEpoch(key) {
|
|
1162
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1163
|
+
}
|
|
1164
|
+
bumpKeyEpochs(keys) {
|
|
1165
|
+
for (const key of keys) {
|
|
1166
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1170
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1179
|
+
this.writeBehindQueue.push(operation);
|
|
1180
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1181
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1182
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1183
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1187
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1191
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1192
|
+
await this.writeBehindFlushPromise;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1196
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1197
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1198
|
+
try {
|
|
1199
|
+
await this.writeBehindFlushPromise;
|
|
1200
|
+
} finally {
|
|
1201
|
+
this.writeBehindFlushPromise = void 0;
|
|
1202
|
+
}
|
|
1203
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1204
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1208
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1209
|
+
onError(generation, error);
|
|
1210
|
+
});
|
|
1211
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1212
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1213
|
+
this.generationCleanupPromise = void 0;
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
async waitForGenerationCleanup() {
|
|
1218
|
+
await this.generationCleanupPromise;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1223
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1224
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1225
|
+
}
|
|
1226
|
+
function shouldStartBackgroundRefresh({
|
|
1227
|
+
isDisconnecting,
|
|
1228
|
+
hasRefreshInFlight
|
|
1229
|
+
}) {
|
|
1230
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1231
|
+
}
|
|
1232
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1233
|
+
if (!gracefulDegradation) {
|
|
1234
|
+
return { degrade: false };
|
|
1235
|
+
}
|
|
1236
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1237
|
+
return {
|
|
1238
|
+
degrade: true,
|
|
1239
|
+
degradedUntil: now + retryAfterMs
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function planFreshReadPolicies({
|
|
1243
|
+
stored,
|
|
1244
|
+
hasFetcher,
|
|
1245
|
+
slidingTtl,
|
|
1246
|
+
refreshAheadSeconds
|
|
1247
|
+
}) {
|
|
1248
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1249
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1250
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1251
|
+
return {
|
|
1252
|
+
refreshedStored,
|
|
1253
|
+
refreshedStoredTtl,
|
|
1254
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1259
|
+
import { randomBytes } from "crypto";
|
|
1260
|
+
import { constants, promises as fs } from "fs";
|
|
1261
|
+
import path from "path";
|
|
1262
|
+
|
|
1263
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
1264
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1265
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1266
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1267
|
+
}
|
|
1268
|
+
async function findExistingAncestor(directory, fs2, path2) {
|
|
1269
|
+
let current = directory;
|
|
1270
|
+
while (true) {
|
|
1271
|
+
try {
|
|
1272
|
+
await fs2.lstat(current);
|
|
1273
|
+
return current;
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
if (error.code !== "ENOENT") {
|
|
1276
|
+
throw error;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const parent = path2.dirname(current);
|
|
1280
|
+
if (parent === current) {
|
|
1281
|
+
return current;
|
|
1282
|
+
}
|
|
1283
|
+
current = parent;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1287
|
+
if (filePath.length === 0) {
|
|
1288
|
+
throw new Error("filePath must not be empty.");
|
|
1289
|
+
}
|
|
1290
|
+
if (filePath.includes("\0")) {
|
|
1291
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1292
|
+
}
|
|
1293
|
+
const { promises: fs2 } = await import("fs");
|
|
1294
|
+
const path2 = await import("path");
|
|
1295
|
+
const resolved = path2.resolve(filePath);
|
|
1296
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1297
|
+
if (baseDir === false) {
|
|
1298
|
+
return resolved;
|
|
1299
|
+
}
|
|
1300
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
1301
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
1302
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1303
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1304
|
+
}
|
|
1305
|
+
if (mode === "read") {
|
|
1306
|
+
const realTarget = await fs2.realpath(resolved);
|
|
1307
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1308
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1309
|
+
}
|
|
1310
|
+
return realTarget;
|
|
1311
|
+
}
|
|
1312
|
+
const parentDir = path2.dirname(resolved);
|
|
1313
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
|
|
1314
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
1315
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1316
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1317
|
+
}
|
|
1318
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
1319
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
1320
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1321
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1322
|
+
}
|
|
1323
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1324
|
+
try {
|
|
1325
|
+
const existing = await fs2.lstat(targetPath);
|
|
1326
|
+
if (existing.isSymbolicLink()) {
|
|
1327
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1328
|
+
}
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (error.code !== "ENOENT") {
|
|
1331
|
+
throw error;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return targetPath;
|
|
1335
|
+
}
|
|
1336
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1337
|
+
if (byteLimit === false) {
|
|
1338
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1339
|
+
}
|
|
1340
|
+
const chunks = [];
|
|
1341
|
+
let totalBytes = 0;
|
|
1342
|
+
let position = 0;
|
|
1343
|
+
while (true) {
|
|
1344
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1345
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1346
|
+
if (bytesRead === 0) {
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
totalBytes += bytesRead;
|
|
1350
|
+
if (totalBytes > byteLimit) {
|
|
1351
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1352
|
+
}
|
|
1353
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1354
|
+
position += bytesRead;
|
|
1355
|
+
}
|
|
1356
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
1360
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1361
|
+
function sanitizeStructuredData(value, options) {
|
|
1362
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1363
|
+
}
|
|
1364
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1365
|
+
state.count += 1;
|
|
1366
|
+
if (state.count > options.maxNodes) {
|
|
1367
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1368
|
+
}
|
|
1369
|
+
if (depth > options.maxDepth) {
|
|
1370
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1371
|
+
}
|
|
1372
|
+
if (Array.isArray(value)) {
|
|
1373
|
+
const sanitized2 = [];
|
|
1374
|
+
for (const entry of value) {
|
|
1375
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1376
|
+
}
|
|
1377
|
+
return sanitized2;
|
|
1378
|
+
}
|
|
1379
|
+
if (!isPlainObject(value)) {
|
|
1380
|
+
return value;
|
|
1381
|
+
}
|
|
1382
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1383
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1384
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1388
|
+
}
|
|
1389
|
+
return sanitized;
|
|
1390
|
+
}
|
|
1391
|
+
function isPlainObject(value) {
|
|
1392
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1396
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1397
|
+
var CacheStackSnapshotManager = class {
|
|
1398
|
+
constructor(options) {
|
|
1399
|
+
this.options = options;
|
|
1400
|
+
}
|
|
1401
|
+
options;
|
|
1402
|
+
async exportState(maxEntries) {
|
|
1403
|
+
const entries = [];
|
|
1404
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1405
|
+
entries.push(entry);
|
|
1406
|
+
});
|
|
1407
|
+
return entries;
|
|
1408
|
+
}
|
|
1409
|
+
async importState(entries) {
|
|
1410
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1411
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1412
|
+
value: entry.value,
|
|
1413
|
+
ttl: entry.ttl
|
|
1414
|
+
}));
|
|
1415
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1416
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1417
|
+
await Promise.all(
|
|
1418
|
+
batch.map(async (entry) => {
|
|
1419
|
+
await Promise.all(
|
|
1420
|
+
this.options.layers.map(async (layer) => {
|
|
1421
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1422
|
+
try {
|
|
1423
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1426
|
+
}
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1429
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1430
|
+
})
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1435
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1436
|
+
const tempPath = path.join(
|
|
1437
|
+
path.dirname(targetPath),
|
|
1438
|
+
`.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
|
|
1439
|
+
);
|
|
1440
|
+
let handle;
|
|
1441
|
+
try {
|
|
1442
|
+
handle = await fs.open(tempPath, "wx");
|
|
1443
|
+
const openedHandle = handle;
|
|
1444
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1445
|
+
let wroteAny = false;
|
|
1446
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1447
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1448
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1449
|
+
wroteAny = true;
|
|
1450
|
+
});
|
|
1451
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1452
|
+
await openedHandle.close();
|
|
1453
|
+
handle = void 0;
|
|
1454
|
+
await fs.rename(tempPath, targetPath);
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
await handle?.close().catch(() => void 0);
|
|
1457
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1462
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1463
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1464
|
+
let raw;
|
|
1465
|
+
try {
|
|
1466
|
+
if (maxBytes !== false) {
|
|
1467
|
+
const stat = await handle.stat();
|
|
1468
|
+
if (stat.size > maxBytes) {
|
|
1469
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1473
|
+
} finally {
|
|
1474
|
+
await handle.close();
|
|
1475
|
+
}
|
|
1476
|
+
let parsed;
|
|
1477
|
+
try {
|
|
1478
|
+
parsed = JSON.parse(raw);
|
|
1479
|
+
} catch (cause) {
|
|
1480
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1481
|
+
}
|
|
1482
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1483
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1484
|
+
}
|
|
1485
|
+
await this.importState(
|
|
1486
|
+
parsed.map((entry) => ({
|
|
1487
|
+
key: entry.key,
|
|
1488
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1489
|
+
ttl: entry.ttl
|
|
1490
|
+
}))
|
|
1491
|
+
);
|
|
1102
1492
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1493
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1494
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1495
|
+
for (const layer of this.options.layers) {
|
|
1496
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const visitKey = async (key) => {
|
|
1500
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1501
|
+
if (exported.has(exportedKey)) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1505
|
+
if (stored === null) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
exported.add(exportedKey);
|
|
1509
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1510
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1511
|
+
}
|
|
1512
|
+
await visitor({
|
|
1513
|
+
key: exportedKey,
|
|
1514
|
+
value: stored,
|
|
1515
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1516
|
+
});
|
|
1517
|
+
};
|
|
1518
|
+
if (layer.forEachKey) {
|
|
1519
|
+
await layer.forEachKey(visitKey);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
const keys = await layer.keys?.();
|
|
1523
|
+
for (const key of keys ?? []) {
|
|
1524
|
+
await visitKey(key);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1108
1527
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1528
|
+
isCacheSnapshotEntries(value) {
|
|
1529
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1530
|
+
if (!entry || typeof entry !== "object") {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
const candidate = entry;
|
|
1534
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1535
|
+
});
|
|
1114
1536
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
isDisconnecting,
|
|
1124
|
-
hasRefreshInFlight
|
|
1125
|
-
}) {
|
|
1126
|
-
return !isDisconnecting && !hasRefreshInFlight;
|
|
1127
|
-
}
|
|
1128
|
-
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1129
|
-
if (!gracefulDegradation) {
|
|
1130
|
-
return { degrade: false };
|
|
1537
|
+
sanitizeSnapshotValue(value) {
|
|
1538
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1539
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1540
|
+
label: "Snapshot value",
|
|
1541
|
+
maxDepth: 64,
|
|
1542
|
+
maxNodes: 1e4,
|
|
1543
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1544
|
+
});
|
|
1131
1545
|
}
|
|
1132
|
-
|
|
1133
|
-
return {
|
|
1134
|
-
degrade: true,
|
|
1135
|
-
degradedUntil: now + retryAfterMs
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
function planFreshReadPolicies({
|
|
1139
|
-
stored,
|
|
1140
|
-
hasFetcher,
|
|
1141
|
-
slidingTtl,
|
|
1142
|
-
refreshAheadSeconds
|
|
1143
|
-
}) {
|
|
1144
|
-
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1145
|
-
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1146
|
-
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1147
|
-
return {
|
|
1148
|
-
refreshedStored,
|
|
1149
|
-
refreshedStoredTtl,
|
|
1150
|
-
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1546
|
+
};
|
|
1153
1547
|
|
|
1154
1548
|
// ../../src/internal/CacheStackValidation.ts
|
|
1155
1549
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -1378,7 +1772,11 @@ var FetchRateLimiter = class {
|
|
|
1378
1772
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1379
1773
|
nextFetcherBucketId = 0;
|
|
1380
1774
|
drainTimer;
|
|
1775
|
+
isDisposed = false;
|
|
1381
1776
|
async schedule(options, context, task) {
|
|
1777
|
+
if (this.isDisposed) {
|
|
1778
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1779
|
+
}
|
|
1382
1780
|
if (!options) {
|
|
1383
1781
|
return task();
|
|
1384
1782
|
}
|
|
@@ -1401,6 +1799,27 @@ var FetchRateLimiter = class {
|
|
|
1401
1799
|
this.drain();
|
|
1402
1800
|
});
|
|
1403
1801
|
}
|
|
1802
|
+
dispose() {
|
|
1803
|
+
this.isDisposed = true;
|
|
1804
|
+
if (this.drainTimer) {
|
|
1805
|
+
clearTimeout(this.drainTimer);
|
|
1806
|
+
this.drainTimer = void 0;
|
|
1807
|
+
}
|
|
1808
|
+
for (const bucket of this.buckets.values()) {
|
|
1809
|
+
if (bucket.cleanupTimer) {
|
|
1810
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1811
|
+
bucket.cleanupTimer = void 0;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1815
|
+
for (const item of queue) {
|
|
1816
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
this.queuesByBucket.clear();
|
|
1820
|
+
this.pendingBuckets.clear();
|
|
1821
|
+
this.buckets.clear();
|
|
1822
|
+
}
|
|
1404
1823
|
normalize(options) {
|
|
1405
1824
|
const maxConcurrent = options.maxConcurrent;
|
|
1406
1825
|
const intervalMs = options.intervalMs;
|
|
@@ -1436,6 +1855,9 @@ var FetchRateLimiter = class {
|
|
|
1436
1855
|
return "global";
|
|
1437
1856
|
}
|
|
1438
1857
|
drain() {
|
|
1858
|
+
if (this.isDisposed) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1439
1861
|
if (this.drainTimer) {
|
|
1440
1862
|
clearTimeout(this.drainTimer);
|
|
1441
1863
|
this.drainTimer = void 0;
|
|
@@ -1499,7 +1921,13 @@ var FetchRateLimiter = class {
|
|
|
1499
1921
|
this.pendingBuckets.add(next.bucketKey);
|
|
1500
1922
|
}
|
|
1501
1923
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1502
|
-
this.
|
|
1924
|
+
if (!this.drainTimer) {
|
|
1925
|
+
this.drainTimer = setTimeout(() => {
|
|
1926
|
+
this.drainTimer = void 0;
|
|
1927
|
+
this.drain();
|
|
1928
|
+
}, 0);
|
|
1929
|
+
this.drainTimer.unref?.();
|
|
1930
|
+
}
|
|
1503
1931
|
});
|
|
1504
1932
|
}
|
|
1505
1933
|
}
|
|
@@ -1532,12 +1960,18 @@ var FetchRateLimiter = class {
|
|
|
1532
1960
|
}
|
|
1533
1961
|
}
|
|
1534
1962
|
bucketState(bucketKey) {
|
|
1963
|
+
if (this.isDisposed) {
|
|
1964
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1965
|
+
}
|
|
1535
1966
|
const existing = this.buckets.get(bucketKey);
|
|
1536
1967
|
if (existing) {
|
|
1537
1968
|
return existing;
|
|
1538
1969
|
}
|
|
1539
1970
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1540
1971
|
this.evictIdleBuckets();
|
|
1972
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1973
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1974
|
+
}
|
|
1541
1975
|
}
|
|
1542
1976
|
const bucket = { active: 0, startedAt: [] };
|
|
1543
1977
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -1990,19 +2424,19 @@ var TagIndex = class {
|
|
|
1990
2424
|
if (!this.knownKeys.delete(key)) {
|
|
1991
2425
|
return;
|
|
1992
2426
|
}
|
|
1993
|
-
const
|
|
2427
|
+
const path2 = [];
|
|
1994
2428
|
let node = this.root;
|
|
1995
2429
|
for (const character of key) {
|
|
1996
2430
|
const child = node.children.get(character);
|
|
1997
2431
|
if (!child) {
|
|
1998
2432
|
return;
|
|
1999
2433
|
}
|
|
2000
|
-
|
|
2434
|
+
path2.push([node, character]);
|
|
2001
2435
|
node = child;
|
|
2002
2436
|
}
|
|
2003
2437
|
node.terminal = false;
|
|
2004
|
-
for (let index =
|
|
2005
|
-
const entry =
|
|
2438
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2439
|
+
const entry = path2[index];
|
|
2006
2440
|
if (!entry) {
|
|
2007
2441
|
continue;
|
|
2008
2442
|
}
|
|
@@ -2017,44 +2451,19 @@ var TagIndex = class {
|
|
|
2017
2451
|
};
|
|
2018
2452
|
|
|
2019
2453
|
// ../../src/serialization/JsonSerializer.ts
|
|
2020
|
-
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2021
|
-
var MAX_SANITIZE_NODES = 1e4;
|
|
2022
2454
|
var JsonSerializer = class {
|
|
2023
2455
|
serialize(value) {
|
|
2024
2456
|
return JSON.stringify(value);
|
|
2025
2457
|
}
|
|
2026
2458
|
deserialize(payload) {
|
|
2027
2459
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2028
|
-
return
|
|
2460
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2461
|
+
label: "JSON payload",
|
|
2462
|
+
maxDepth: 200,
|
|
2463
|
+
maxNodes: 1e4
|
|
2464
|
+
});
|
|
2029
2465
|
}
|
|
2030
2466
|
};
|
|
2031
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
2032
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2033
|
-
state.count += 1;
|
|
2034
|
-
if (state.count > MAX_SANITIZE_NODES) {
|
|
2035
|
-
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
2036
|
-
}
|
|
2037
|
-
if (depth > MAX_SANITIZE_DEPTH) {
|
|
2038
|
-
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
2039
|
-
}
|
|
2040
|
-
if (Array.isArray(value)) {
|
|
2041
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
2042
|
-
}
|
|
2043
|
-
if (!isPlainObject(value)) {
|
|
2044
|
-
return value;
|
|
2045
|
-
}
|
|
2046
|
-
const sanitized = {};
|
|
2047
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2048
|
-
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
2052
|
-
}
|
|
2053
|
-
return sanitized;
|
|
2054
|
-
}
|
|
2055
|
-
function isPlainObject(value) {
|
|
2056
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2057
|
-
}
|
|
2058
2467
|
|
|
2059
2468
|
// ../../src/stampede/StampedeGuard.ts
|
|
2060
2469
|
var StampedeGuard = class {
|
|
@@ -2099,7 +2508,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
2099
2508
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
2100
2509
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2101
2510
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
2102
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
2103
2511
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
2104
2512
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
2105
2513
|
var DebugLogger = class {
|
|
@@ -2156,6 +2564,35 @@ var CacheStack = class extends EventEmitter {
|
|
|
2156
2564
|
await this.handleLayerFailure(layer, operation, error);
|
|
2157
2565
|
}
|
|
2158
2566
|
});
|
|
2567
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2568
|
+
tagIndex: this.tagIndex,
|
|
2569
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2570
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2571
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2575
|
+
layers: this.layers,
|
|
2576
|
+
maintenance: this.maintenance,
|
|
2577
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2578
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2579
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2580
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2581
|
+
},
|
|
2582
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2583
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2584
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2585
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2586
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2587
|
+
writePolicy: this.options.writePolicy,
|
|
2588
|
+
onWriteFailures: (context, failures) => {
|
|
2589
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2590
|
+
this.logger.debug?.("write-failure", {
|
|
2591
|
+
...context,
|
|
2592
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2159
2596
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
2160
2597
|
this.logger.warn?.(
|
|
2161
2598
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -2171,6 +2608,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
2171
2608
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
2172
2609
|
);
|
|
2173
2610
|
}
|
|
2611
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2612
|
+
layers: this.layers,
|
|
2613
|
+
tagIndex: this.tagIndex,
|
|
2614
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2615
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2616
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2617
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2618
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2619
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2620
|
+
validateCacheKey,
|
|
2621
|
+
formatError: this.formatError.bind(this)
|
|
2622
|
+
});
|
|
2174
2623
|
this.initializeWriteBehind(options.writeBehind);
|
|
2175
2624
|
this.startup = this.initialize();
|
|
2176
2625
|
}
|
|
@@ -2186,11 +2635,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2186
2635
|
keyDiscovery;
|
|
2187
2636
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2188
2637
|
snapshotSerializer = new JsonSerializer();
|
|
2638
|
+
invalidation;
|
|
2639
|
+
layerWriter;
|
|
2640
|
+
snapshots;
|
|
2189
2641
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2642
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2190
2643
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2191
2644
|
maintenance = new CacheStackMaintenance();
|
|
2192
2645
|
ttlResolver;
|
|
2193
2646
|
circuitBreakerManager;
|
|
2647
|
+
nextOperationId = 0;
|
|
2194
2648
|
currentGeneration;
|
|
2195
2649
|
isDisconnecting = false;
|
|
2196
2650
|
disconnectPromise;
|
|
@@ -2201,10 +2655,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2201
2655
|
* and no `fetcher` is provided.
|
|
2202
2656
|
*/
|
|
2203
2657
|
async get(key, fetcher, options) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2658
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2659
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2660
|
+
this.validateWriteOptions(options);
|
|
2661
|
+
await this.awaitStartup("get");
|
|
2662
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2663
|
+
});
|
|
2208
2664
|
}
|
|
2209
2665
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2210
2666
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2326,23 +2782,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
2326
2782
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2327
2783
|
*/
|
|
2328
2784
|
async set(key, value, options) {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2785
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2786
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2787
|
+
this.validateWriteOptions(options);
|
|
2788
|
+
await this.awaitStartup("set");
|
|
2789
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2790
|
+
});
|
|
2333
2791
|
}
|
|
2334
2792
|
/**
|
|
2335
2793
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2336
2794
|
*/
|
|
2337
2795
|
async delete(key) {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2796
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2797
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2798
|
+
await this.awaitStartup("delete");
|
|
2799
|
+
await this.deleteKeys([normalizedKey]);
|
|
2800
|
+
await this.publishInvalidation({
|
|
2801
|
+
scope: "key",
|
|
2802
|
+
keys: [normalizedKey],
|
|
2803
|
+
sourceId: this.instanceId,
|
|
2804
|
+
operation: "delete"
|
|
2805
|
+
});
|
|
2346
2806
|
});
|
|
2347
2807
|
}
|
|
2348
2808
|
async clear() {
|
|
@@ -2375,95 +2835,102 @@ var CacheStack = class extends EventEmitter {
|
|
|
2375
2835
|
});
|
|
2376
2836
|
}
|
|
2377
2837
|
async mget(entries) {
|
|
2378
|
-
this.
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2838
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2839
|
+
this.assertActive("mget");
|
|
2840
|
+
if (entries.length === 0) {
|
|
2841
|
+
return [];
|
|
2842
|
+
}
|
|
2843
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2844
|
+
...entry,
|
|
2845
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2846
|
+
}));
|
|
2847
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2848
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2849
|
+
if (!canFastPath) {
|
|
2850
|
+
await this.awaitStartup("mget");
|
|
2851
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2852
|
+
return Promise.all(
|
|
2853
|
+
normalizedEntries.map((entry) => {
|
|
2854
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2855
|
+
const existing = pendingReads.get(entry.key);
|
|
2856
|
+
if (!existing) {
|
|
2857
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2858
|
+
pendingReads.set(entry.key, {
|
|
2859
|
+
promise,
|
|
2860
|
+
fetch: entry.fetch,
|
|
2861
|
+
optionsSignature
|
|
2862
|
+
});
|
|
2863
|
+
return promise;
|
|
2864
|
+
}
|
|
2865
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2866
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2867
|
+
}
|
|
2868
|
+
return existing.promise;
|
|
2869
|
+
})
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2389
2872
|
await this.awaitStartup("mget");
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2873
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2874
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2875
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2876
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2877
|
+
const entry = normalizedEntries[index];
|
|
2878
|
+
if (!entry) continue;
|
|
2879
|
+
const key = entry.key;
|
|
2880
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2881
|
+
indexes.push(index);
|
|
2882
|
+
indexesByKey.set(key, indexes);
|
|
2883
|
+
pending.add(key);
|
|
2884
|
+
}
|
|
2885
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2886
|
+
const layer = this.layers[layerIndex];
|
|
2887
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2888
|
+
const keys = [...pending];
|
|
2889
|
+
if (keys.length === 0) {
|
|
2890
|
+
break;
|
|
2891
|
+
}
|
|
2892
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2893
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2894
|
+
const key = keys[offset];
|
|
2895
|
+
const stored = values[offset];
|
|
2896
|
+
if (!key || stored === null) {
|
|
2897
|
+
continue;
|
|
2403
2898
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2899
|
+
const resolved = resolveStoredValue(stored);
|
|
2900
|
+
if (resolved.state === "expired") {
|
|
2901
|
+
await layer.delete(key);
|
|
2902
|
+
continue;
|
|
2406
2903
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2416
|
-
const entry = normalizedEntries[index];
|
|
2417
|
-
if (!entry) continue;
|
|
2418
|
-
const key = entry.key;
|
|
2419
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2420
|
-
indexes.push(index);
|
|
2421
|
-
indexesByKey.set(key, indexes);
|
|
2422
|
-
pending.add(key);
|
|
2423
|
-
}
|
|
2424
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2425
|
-
const layer = this.layers[layerIndex];
|
|
2426
|
-
if (!layer) continue;
|
|
2427
|
-
const keys = [...pending];
|
|
2428
|
-
if (keys.length === 0) {
|
|
2429
|
-
break;
|
|
2430
|
-
}
|
|
2431
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2432
|
-
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2433
|
-
const key = keys[offset];
|
|
2434
|
-
const stored = values[offset];
|
|
2435
|
-
if (!key || stored === null) {
|
|
2436
|
-
continue;
|
|
2437
|
-
}
|
|
2438
|
-
const resolved = resolveStoredValue(stored);
|
|
2439
|
-
if (resolved.state === "expired") {
|
|
2440
|
-
await layer.delete(key);
|
|
2441
|
-
continue;
|
|
2904
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2905
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2906
|
+
}
|
|
2907
|
+
await this.tagIndex.touch(key);
|
|
2908
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2909
|
+
resultsByKey.set(key, resolved.value);
|
|
2910
|
+
pending.delete(key);
|
|
2911
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2442
2912
|
}
|
|
2443
|
-
await this.tagIndex.touch(key);
|
|
2444
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2445
|
-
resultsByKey.set(key, resolved.value);
|
|
2446
|
-
pending.delete(key);
|
|
2447
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2448
2913
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2914
|
+
if (pending.size > 0) {
|
|
2915
|
+
for (const key of pending) {
|
|
2916
|
+
await this.tagIndex.remove(key);
|
|
2917
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2918
|
+
}
|
|
2454
2919
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2920
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2921
|
+
});
|
|
2457
2922
|
}
|
|
2458
2923
|
async mset(entries) {
|
|
2459
|
-
this.
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2924
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2925
|
+
this.assertActive("mset");
|
|
2926
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2927
|
+
...entry,
|
|
2928
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2929
|
+
}));
|
|
2930
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2931
|
+
await this.awaitStartup("mset");
|
|
2932
|
+
await this.writeBatch(normalizedEntries);
|
|
2933
|
+
});
|
|
2467
2934
|
}
|
|
2468
2935
|
async warm(entries, options = {}) {
|
|
2469
2936
|
this.assertActive("warm");
|
|
@@ -2516,40 +2983,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2516
2983
|
return new CacheNamespace(this, prefix);
|
|
2517
2984
|
}
|
|
2518
2985
|
async invalidateByTag(tag) {
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2986
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2987
|
+
validateTag(tag);
|
|
2988
|
+
await this.awaitStartup("invalidateByTag");
|
|
2989
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2990
|
+
await this.deleteKeys(keys);
|
|
2991
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2992
|
+
});
|
|
2524
2993
|
}
|
|
2525
2994
|
async invalidateByTags(tags, mode = "any") {
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2995
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2996
|
+
if (tags.length === 0) {
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
validateTags(tags);
|
|
3000
|
+
await this.awaitStartup("invalidateByTags");
|
|
3001
|
+
const keysByTag = await Promise.all(
|
|
3002
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
3003
|
+
);
|
|
3004
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
3005
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
3006
|
+
await this.deleteKeys(keys);
|
|
3007
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3008
|
+
});
|
|
2536
3009
|
}
|
|
2537
3010
|
async invalidateByPattern(pattern) {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
this.
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
3011
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
3012
|
+
validatePattern(pattern);
|
|
3013
|
+
await this.awaitStartup("invalidateByPattern");
|
|
3014
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
3015
|
+
this.qualifyPattern(pattern),
|
|
3016
|
+
this.invalidationMaxKeys()
|
|
3017
|
+
);
|
|
3018
|
+
await this.deleteKeys(keys);
|
|
3019
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3020
|
+
});
|
|
2546
3021
|
}
|
|
2547
3022
|
async invalidateByPrefix(prefix) {
|
|
2548
|
-
await this.
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
3023
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
3024
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
3025
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
3026
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
3027
|
+
await this.deleteKeys(keys);
|
|
3028
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3029
|
+
});
|
|
2553
3030
|
}
|
|
2554
3031
|
getMetrics() {
|
|
2555
3032
|
return this.metricsCollector.snapshot;
|
|
@@ -2660,95 +3137,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
2660
3137
|
}
|
|
2661
3138
|
async exportState() {
|
|
2662
3139
|
await this.awaitStartup("exportState");
|
|
2663
|
-
|
|
2664
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2665
|
-
entries.push(entry);
|
|
2666
|
-
});
|
|
2667
|
-
return entries;
|
|
3140
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2668
3141
|
}
|
|
2669
3142
|
async importState(entries) {
|
|
2670
3143
|
await this.awaitStartup("importState");
|
|
2671
|
-
|
|
2672
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2673
|
-
value: entry.value,
|
|
2674
|
-
ttl: entry.ttl
|
|
2675
|
-
}));
|
|
2676
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2677
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2678
|
-
await Promise.all(
|
|
2679
|
-
batch.map(async (entry) => {
|
|
2680
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2681
|
-
await this.tagIndex.touch(entry.key);
|
|
2682
|
-
})
|
|
2683
|
-
);
|
|
2684
|
-
}
|
|
3144
|
+
await this.snapshots.importState(entries);
|
|
2685
3145
|
}
|
|
2686
3146
|
async persistToFile(filePath) {
|
|
2687
3147
|
this.assertActive("persistToFile");
|
|
2688
|
-
|
|
2689
|
-
const path = await import("path");
|
|
2690
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2691
|
-
const tempPath = path.join(
|
|
2692
|
-
path.dirname(targetPath),
|
|
2693
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2694
|
-
);
|
|
2695
|
-
let handle;
|
|
2696
|
-
try {
|
|
2697
|
-
handle = await fs.open(tempPath, "wx");
|
|
2698
|
-
const openedHandle = handle;
|
|
2699
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2700
|
-
let wroteAny = false;
|
|
2701
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2702
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2703
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2704
|
-
wroteAny = true;
|
|
2705
|
-
});
|
|
2706
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2707
|
-
await openedHandle.close();
|
|
2708
|
-
handle = void 0;
|
|
2709
|
-
await fs.rename(tempPath, targetPath);
|
|
2710
|
-
} catch (error) {
|
|
2711
|
-
await handle?.close().catch(() => void 0);
|
|
2712
|
-
await fs.unlink(tempPath).catch(() => void 0);
|
|
2713
|
-
throw error;
|
|
2714
|
-
}
|
|
3148
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2715
3149
|
}
|
|
2716
3150
|
async restoreFromFile(filePath) {
|
|
2717
3151
|
this.assertActive("restoreFromFile");
|
|
2718
|
-
|
|
2719
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2720
|
-
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2721
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2722
|
-
let raw;
|
|
2723
|
-
try {
|
|
2724
|
-
if (snapshotMaxBytes !== false) {
|
|
2725
|
-
const stat = await handle.stat();
|
|
2726
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2727
|
-
throw new Error(
|
|
2728
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2729
|
-
);
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2733
|
-
} finally {
|
|
2734
|
-
await handle.close();
|
|
2735
|
-
}
|
|
2736
|
-
let parsed;
|
|
2737
|
-
try {
|
|
2738
|
-
parsed = JSON.parse(raw);
|
|
2739
|
-
} catch (cause) {
|
|
2740
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2741
|
-
}
|
|
2742
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2743
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2744
|
-
}
|
|
2745
|
-
await this.importState(
|
|
2746
|
-
parsed.map((entry) => ({
|
|
2747
|
-
key: entry.key,
|
|
2748
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2749
|
-
ttl: entry.ttl
|
|
2750
|
-
}))
|
|
2751
|
-
);
|
|
3152
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2752
3153
|
}
|
|
2753
3154
|
async disconnect() {
|
|
2754
3155
|
if (!this.disconnectPromise) {
|
|
@@ -2758,8 +3159,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
2758
3159
|
await this.unsubscribeInvalidation?.();
|
|
2759
3160
|
await this.flushWriteBehindQueue();
|
|
2760
3161
|
await this.maintenance.waitForGenerationCleanup();
|
|
2761
|
-
|
|
3162
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3163
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3164
|
+
}
|
|
3165
|
+
await Promise.allSettled(
|
|
3166
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3167
|
+
let timer;
|
|
3168
|
+
return Promise.race([
|
|
3169
|
+
promise,
|
|
3170
|
+
new Promise((resolve) => {
|
|
3171
|
+
timer = setTimeout(resolve, 5e3);
|
|
3172
|
+
timer.unref?.();
|
|
3173
|
+
})
|
|
3174
|
+
]).finally(() => {
|
|
3175
|
+
if (timer) clearTimeout(timer);
|
|
3176
|
+
});
|
|
3177
|
+
})
|
|
3178
|
+
);
|
|
3179
|
+
this.backgroundRefreshes.clear();
|
|
3180
|
+
this.backgroundRefreshAbort.clear();
|
|
2762
3181
|
this.maintenance.disposeWriteBehindTimer();
|
|
3182
|
+
this.fetchRateLimiter.dispose();
|
|
2763
3183
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2764
3184
|
})();
|
|
2765
3185
|
}
|
|
@@ -2873,7 +3293,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2873
3293
|
async storeEntry(key, kind, value, options) {
|
|
2874
3294
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2875
3295
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2876
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
3296
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2877
3297
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2878
3298
|
return;
|
|
2879
3299
|
}
|
|
@@ -2890,52 +3310,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2890
3310
|
}
|
|
2891
3311
|
}
|
|
2892
3312
|
async writeBatch(entries) {
|
|
2893
|
-
const
|
|
2894
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2895
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2896
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2897
|
-
const immediateOperations = [];
|
|
2898
|
-
const deferredOperations = [];
|
|
2899
|
-
for (const entry of entries) {
|
|
2900
|
-
for (const layer of this.layers) {
|
|
2901
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2902
|
-
continue;
|
|
2903
|
-
}
|
|
2904
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2905
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2906
|
-
bucket.push(layerEntry);
|
|
2907
|
-
entriesByLayer.set(layer, bucket);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2911
|
-
const operation = async () => {
|
|
2912
|
-
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
const activeEntries = layerEntries.filter(
|
|
2916
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2917
|
-
);
|
|
2918
|
-
if (activeEntries.length === 0) {
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
try {
|
|
2922
|
-
if (layer.setMany) {
|
|
2923
|
-
await layer.setMany(activeEntries);
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2927
|
-
} catch (error) {
|
|
2928
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2929
|
-
}
|
|
2930
|
-
};
|
|
2931
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2932
|
-
deferredOperations.push(operation);
|
|
2933
|
-
} else {
|
|
2934
|
-
immediateOperations.push(operation);
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2938
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
3313
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2939
3314
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2940
3315
|
return;
|
|
2941
3316
|
}
|
|
@@ -3042,58 +3417,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3042
3417
|
this.emit("backfill", { key, layer: layer.name });
|
|
3043
3418
|
}
|
|
3044
3419
|
}
|
|
3045
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
3046
|
-
const now = Date.now();
|
|
3047
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3048
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3049
|
-
const immediateOperations = [];
|
|
3050
|
-
const deferredOperations = [];
|
|
3051
|
-
for (const layer of this.layers) {
|
|
3052
|
-
const operation = async () => {
|
|
3053
|
-
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3054
|
-
return;
|
|
3055
|
-
}
|
|
3056
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3057
|
-
return;
|
|
3058
|
-
}
|
|
3059
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
3060
|
-
try {
|
|
3061
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
3062
|
-
} catch (error) {
|
|
3063
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
3064
|
-
}
|
|
3065
|
-
};
|
|
3066
|
-
if (this.shouldWriteBehind(layer)) {
|
|
3067
|
-
deferredOperations.push(operation);
|
|
3068
|
-
} else {
|
|
3069
|
-
immediateOperations.push(operation);
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
3073
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
3074
|
-
}
|
|
3075
|
-
async executeLayerOperations(operations, context) {
|
|
3076
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
3077
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
3078
|
-
return;
|
|
3079
|
-
}
|
|
3080
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
3081
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
3082
|
-
if (failures.length === 0) {
|
|
3083
|
-
return;
|
|
3084
|
-
}
|
|
3085
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3086
|
-
this.logger.debug?.("write-failure", {
|
|
3087
|
-
...context,
|
|
3088
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
3089
|
-
});
|
|
3090
|
-
if (failures.length === operations.length) {
|
|
3091
|
-
throw new AggregateError(
|
|
3092
|
-
failures.map((failure) => failure.reason),
|
|
3093
|
-
`${context.action} failed for every cache layer`
|
|
3094
|
-
);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
3420
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
3098
3421
|
return this.ttlResolver.resolveFreshTtl(
|
|
3099
3422
|
key,
|
|
@@ -3121,15 +3444,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
3121
3444
|
}
|
|
3122
3445
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3123
3446
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3447
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3124
3448
|
const refresh = (async () => {
|
|
3125
3449
|
this.metricsCollector.increment("refreshes");
|
|
3126
3450
|
try {
|
|
3451
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3127
3452
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3128
3453
|
} catch (error) {
|
|
3454
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3129
3455
|
this.metricsCollector.increment("refreshErrors");
|
|
3130
3456
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3131
3457
|
} finally {
|
|
3132
3458
|
this.backgroundRefreshes.delete(key);
|
|
3459
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3133
3460
|
}
|
|
3134
3461
|
})();
|
|
3135
3462
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3159,7 +3486,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3159
3486
|
return;
|
|
3160
3487
|
}
|
|
3161
3488
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3162
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3489
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
3163
3490
|
for (const key of keys) {
|
|
3164
3491
|
await this.tagIndex.remove(key);
|
|
3165
3492
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -3191,7 +3518,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3191
3518
|
}
|
|
3192
3519
|
const keys = message.keys ?? [];
|
|
3193
3520
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3194
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3521
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3195
3522
|
if (message.operation !== "write") {
|
|
3196
3523
|
for (const key of keys) {
|
|
3197
3524
|
await this.tagIndex.remove(key);
|
|
@@ -3232,7 +3559,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3232
3559
|
timer.unref?.();
|
|
3233
3560
|
})
|
|
3234
3561
|
]);
|
|
3235
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3562
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3236
3563
|
if (result.kind === "error") {
|
|
3237
3564
|
throw result.error;
|
|
3238
3565
|
}
|
|
@@ -3248,6 +3575,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
3248
3575
|
shouldBroadcastL1Invalidation() {
|
|
3249
3576
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3250
3577
|
}
|
|
3578
|
+
async observeOperation(name, attributes, execute) {
|
|
3579
|
+
const id = this.nextOperationId;
|
|
3580
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3581
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3582
|
+
try {
|
|
3583
|
+
const result = await execute();
|
|
3584
|
+
this.emit("operation-end", {
|
|
3585
|
+
id,
|
|
3586
|
+
name,
|
|
3587
|
+
attributes,
|
|
3588
|
+
success: true,
|
|
3589
|
+
result: result === null ? "null" : void 0
|
|
3590
|
+
});
|
|
3591
|
+
return result;
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
this.emit("operation-end", {
|
|
3594
|
+
id,
|
|
3595
|
+
name,
|
|
3596
|
+
attributes,
|
|
3597
|
+
success: false,
|
|
3598
|
+
error
|
|
3599
|
+
});
|
|
3600
|
+
throw error;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3251
3603
|
scheduleGenerationCleanup(generation) {
|
|
3252
3604
|
this.maintenance.scheduleGenerationCleanup(
|
|
3253
3605
|
generation,
|
|
@@ -3303,37 +3655,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3303
3655
|
});
|
|
3304
3656
|
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3305
3657
|
}
|
|
3306
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3307
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3308
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3309
|
-
layer.name,
|
|
3310
|
-
options?.staleWhileRevalidate,
|
|
3311
|
-
this.options.staleWhileRevalidate
|
|
3312
|
-
);
|
|
3313
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3314
|
-
const payload = createStoredValueEnvelope({
|
|
3315
|
-
kind,
|
|
3316
|
-
value,
|
|
3317
|
-
freshTtlSeconds: freshTtl,
|
|
3318
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3319
|
-
staleIfErrorSeconds: staleIfError,
|
|
3320
|
-
now
|
|
3321
|
-
});
|
|
3322
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
3323
|
-
return {
|
|
3324
|
-
key,
|
|
3325
|
-
value: payload,
|
|
3326
|
-
ttl
|
|
3327
|
-
};
|
|
3328
|
-
}
|
|
3329
|
-
intersectKeys(groups) {
|
|
3330
|
-
if (groups.length === 0) {
|
|
3331
|
-
return [];
|
|
3332
|
-
}
|
|
3333
|
-
const [firstGroup, ...rest] = groups;
|
|
3334
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3335
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3336
|
-
}
|
|
3337
3658
|
qualifyKey(key) {
|
|
3338
3659
|
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3339
3660
|
}
|
|
@@ -3343,32 +3664,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3343
3664
|
stripQualifiedKey(key) {
|
|
3344
3665
|
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3345
3666
|
}
|
|
3346
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3347
|
-
await Promise.all(
|
|
3348
|
-
layers.map(async (layer) => {
|
|
3349
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3350
|
-
return;
|
|
3351
|
-
}
|
|
3352
|
-
if (layer.deleteMany) {
|
|
3353
|
-
try {
|
|
3354
|
-
await layer.deleteMany(keys);
|
|
3355
|
-
} catch (error) {
|
|
3356
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3357
|
-
}
|
|
3358
|
-
return;
|
|
3359
|
-
}
|
|
3360
|
-
await Promise.all(
|
|
3361
|
-
keys.map(async (key) => {
|
|
3362
|
-
try {
|
|
3363
|
-
await layer.delete(key);
|
|
3364
|
-
} catch (error) {
|
|
3365
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3366
|
-
}
|
|
3367
|
-
})
|
|
3368
|
-
);
|
|
3369
|
-
})
|
|
3370
|
-
);
|
|
3371
|
-
}
|
|
3372
3667
|
validateConfiguration() {
|
|
3373
3668
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
3374
3669
|
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
@@ -3499,18 +3794,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3499
3794
|
this.emit("error", { operation, ...context });
|
|
3500
3795
|
}
|
|
3501
3796
|
}
|
|
3502
|
-
isCacheSnapshotEntries(value) {
|
|
3503
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3504
|
-
if (!entry || typeof entry !== "object") {
|
|
3505
|
-
return false;
|
|
3506
|
-
}
|
|
3507
|
-
const candidate = entry;
|
|
3508
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3509
|
-
});
|
|
3510
|
-
}
|
|
3511
|
-
sanitizeSnapshotValue(value) {
|
|
3512
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3513
|
-
}
|
|
3514
3797
|
snapshotMaxBytes() {
|
|
3515
3798
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3516
3799
|
}
|
|
@@ -3520,62 +3803,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3520
3803
|
invalidationMaxKeys() {
|
|
3521
3804
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3522
3805
|
}
|
|
3523
|
-
async collectKeysForTag(tag) {
|
|
3524
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3525
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3526
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3527
|
-
keys.add(key);
|
|
3528
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3529
|
-
});
|
|
3530
|
-
return [...keys];
|
|
3531
|
-
}
|
|
3532
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3533
|
-
keys.add(key);
|
|
3534
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3535
|
-
}
|
|
3536
|
-
return [...keys];
|
|
3537
|
-
}
|
|
3538
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3539
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3540
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3541
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3545
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3546
|
-
for (const layer of this.layers) {
|
|
3547
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3548
|
-
continue;
|
|
3549
|
-
}
|
|
3550
|
-
const visitKey = async (key) => {
|
|
3551
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3552
|
-
if (exported.has(exportedKey)) {
|
|
3553
|
-
return;
|
|
3554
|
-
}
|
|
3555
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3556
|
-
if (stored === null) {
|
|
3557
|
-
return;
|
|
3558
|
-
}
|
|
3559
|
-
exported.add(exportedKey);
|
|
3560
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3561
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3562
|
-
}
|
|
3563
|
-
await visitor({
|
|
3564
|
-
key: exportedKey,
|
|
3565
|
-
value: stored,
|
|
3566
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3567
|
-
});
|
|
3568
|
-
};
|
|
3569
|
-
if (layer.forEachKey) {
|
|
3570
|
-
await layer.forEachKey(visitKey);
|
|
3571
|
-
continue;
|
|
3572
|
-
}
|
|
3573
|
-
const keys = await layer.keys?.();
|
|
3574
|
-
for (const key of keys ?? []) {
|
|
3575
|
-
await visitKey(key);
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
3806
|
};
|
|
3580
3807
|
|
|
3581
3808
|
// src/module.ts
|