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