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
|
@@ -752,102 +752,6 @@ function createInstanceId() {
|
|
|
752
752
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
753
753
|
}
|
|
754
754
|
|
|
755
|
-
// ../../src/internal/CacheSnapshotFile.ts
|
|
756
|
-
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
757
|
-
const relative = path.relative(realBaseDir, candidatePath);
|
|
758
|
-
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
759
|
-
}
|
|
760
|
-
async function findExistingAncestor(directory, fs, path) {
|
|
761
|
-
let current = directory;
|
|
762
|
-
while (true) {
|
|
763
|
-
try {
|
|
764
|
-
await fs.lstat(current);
|
|
765
|
-
return current;
|
|
766
|
-
} catch (error) {
|
|
767
|
-
if (error.code !== "ENOENT") {
|
|
768
|
-
throw error;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
const parent = path.dirname(current);
|
|
772
|
-
if (parent === current) {
|
|
773
|
-
return current;
|
|
774
|
-
}
|
|
775
|
-
current = parent;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
779
|
-
if (filePath.length === 0) {
|
|
780
|
-
throw new Error("filePath must not be empty.");
|
|
781
|
-
}
|
|
782
|
-
if (filePath.includes("\0")) {
|
|
783
|
-
throw new Error("filePath must not contain null bytes.");
|
|
784
|
-
}
|
|
785
|
-
const { promises: fs } = await import("fs");
|
|
786
|
-
const path = await import("path");
|
|
787
|
-
const resolved = path.resolve(filePath);
|
|
788
|
-
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
789
|
-
if (baseDir === false) {
|
|
790
|
-
return resolved;
|
|
791
|
-
}
|
|
792
|
-
await fs.mkdir(baseDir, { recursive: true });
|
|
793
|
-
const realBaseDir = await fs.realpath(baseDir);
|
|
794
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
795
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
796
|
-
}
|
|
797
|
-
if (mode === "read") {
|
|
798
|
-
const realTarget = await fs.realpath(resolved);
|
|
799
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
800
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
801
|
-
}
|
|
802
|
-
return realTarget;
|
|
803
|
-
}
|
|
804
|
-
const parentDir = path.dirname(resolved);
|
|
805
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
806
|
-
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
807
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
808
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
809
|
-
}
|
|
810
|
-
await fs.mkdir(parentDir, { recursive: true });
|
|
811
|
-
const realParentDir = await fs.realpath(parentDir);
|
|
812
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
813
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
814
|
-
}
|
|
815
|
-
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
816
|
-
try {
|
|
817
|
-
const existing = await fs.lstat(targetPath);
|
|
818
|
-
if (existing.isSymbolicLink()) {
|
|
819
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
820
|
-
}
|
|
821
|
-
} catch (error) {
|
|
822
|
-
if (error.code !== "ENOENT") {
|
|
823
|
-
throw error;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return targetPath;
|
|
827
|
-
}
|
|
828
|
-
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
829
|
-
if (byteLimit === false) {
|
|
830
|
-
return handle.readFile({ encoding: "utf8" });
|
|
831
|
-
}
|
|
832
|
-
const chunks = [];
|
|
833
|
-
let totalBytes = 0;
|
|
834
|
-
let position = 0;
|
|
835
|
-
while (true) {
|
|
836
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
837
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
838
|
-
if (bytesRead === 0) {
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
totalBytes += bytesRead;
|
|
842
|
-
if (totalBytes > byteLimit) {
|
|
843
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
844
|
-
}
|
|
845
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
846
|
-
position += bytesRead;
|
|
847
|
-
}
|
|
848
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
849
|
-
}
|
|
850
|
-
|
|
851
755
|
// ../../src/internal/CacheStackGeneration.ts
|
|
852
756
|
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
853
757
|
function generationPrefix(generation) {
|
|
@@ -895,102 +799,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
|
895
799
|
return batches;
|
|
896
800
|
}
|
|
897
801
|
|
|
898
|
-
// ../../src/internal/
|
|
899
|
-
var
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
writeBehindTimer;
|
|
903
|
-
writeBehindFlushPromise;
|
|
904
|
-
generationCleanupPromise;
|
|
905
|
-
clearEpoch = 0;
|
|
906
|
-
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
907
|
-
if (writeStrategy !== "write-behind") {
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
const flushIntervalMs = options?.flushIntervalMs;
|
|
911
|
-
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
this.disposeWriteBehindTimer();
|
|
915
|
-
this.writeBehindTimer = setInterval(() => {
|
|
916
|
-
void flush();
|
|
917
|
-
}, flushIntervalMs);
|
|
918
|
-
this.writeBehindTimer.unref?.();
|
|
802
|
+
// ../../src/internal/CacheStackInvalidationSupport.ts
|
|
803
|
+
var CacheStackInvalidationSupport = class {
|
|
804
|
+
constructor(options) {
|
|
805
|
+
this.options = options;
|
|
919
806
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
807
|
+
options;
|
|
808
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
809
|
+
const keys = /* @__PURE__ */ new Set();
|
|
810
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
811
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
812
|
+
keys.add(key);
|
|
813
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
814
|
+
});
|
|
815
|
+
return [...keys];
|
|
923
816
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
beginClearEpoch() {
|
|
928
|
-
this.clearEpoch += 1;
|
|
929
|
-
this.keyEpochs.clear();
|
|
930
|
-
this.writeBehindQueue.length = 0;
|
|
931
|
-
}
|
|
932
|
-
currentClearEpoch() {
|
|
933
|
-
return this.clearEpoch;
|
|
934
|
-
}
|
|
935
|
-
currentKeyEpoch(key) {
|
|
936
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
937
|
-
}
|
|
938
|
-
bumpKeyEpochs(keys) {
|
|
939
|
-
for (const key of keys) {
|
|
940
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
817
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
818
|
+
keys.add(key);
|
|
819
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
941
820
|
}
|
|
821
|
+
return [...keys];
|
|
942
822
|
}
|
|
943
|
-
|
|
944
|
-
if (
|
|
945
|
-
return
|
|
946
|
-
}
|
|
947
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
948
|
-
return true;
|
|
823
|
+
intersectKeys(groups) {
|
|
824
|
+
if (groups.length === 0) {
|
|
825
|
+
return [];
|
|
949
826
|
}
|
|
950
|
-
|
|
827
|
+
const [firstGroup, ...rest] = groups;
|
|
828
|
+
const restSets = rest.map((group) => new Set(group));
|
|
829
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
951
830
|
}
|
|
952
|
-
async
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
831
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
832
|
+
await Promise.all(
|
|
833
|
+
layers.map(async (layer) => {
|
|
834
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (layer.deleteMany) {
|
|
838
|
+
try {
|
|
839
|
+
await layer.deleteMany(keys);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
await Promise.all(
|
|
846
|
+
keys.map(async (key) => {
|
|
847
|
+
try {
|
|
848
|
+
await layer.delete(key);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
})
|
|
855
|
+
);
|
|
963
856
|
}
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const batchSize = options?.batchSize ?? 100;
|
|
970
|
-
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
971
|
-
this.writeBehindFlushPromise = flushBatch(batch);
|
|
972
|
-
try {
|
|
973
|
-
await this.writeBehindFlushPromise;
|
|
974
|
-
} finally {
|
|
975
|
-
this.writeBehindFlushPromise = void 0;
|
|
976
|
-
}
|
|
977
|
-
if (this.writeBehindQueue.length > 0) {
|
|
978
|
-
await this.flushWriteBehindQueue(options, flushBatch);
|
|
857
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
858
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
859
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
979
860
|
}
|
|
980
861
|
}
|
|
981
|
-
scheduleGenerationCleanup(generation, task, onError) {
|
|
982
|
-
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
983
|
-
onError(generation, error);
|
|
984
|
-
});
|
|
985
|
-
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
986
|
-
if (this.generationCleanupPromise === scheduledTask) {
|
|
987
|
-
this.generationCleanupPromise = void 0;
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
async waitForGenerationCleanup() {
|
|
992
|
-
await this.generationCleanupPromise;
|
|
993
|
-
}
|
|
994
862
|
};
|
|
995
863
|
|
|
996
864
|
// ../../src/internal/StoredValue.ts
|
|
@@ -1114,78 +982,551 @@ function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
|
1114
982
|
if (remainingMs <= 0) {
|
|
1115
983
|
return 0;
|
|
1116
984
|
}
|
|
1117
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1118
|
-
}
|
|
1119
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1120
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1121
|
-
return stored;
|
|
985
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
986
|
+
}
|
|
987
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
988
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
989
|
+
return stored;
|
|
990
|
+
}
|
|
991
|
+
return createStoredValueEnvelope({
|
|
992
|
+
kind: stored.kind,
|
|
993
|
+
value: stored.value,
|
|
994
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
995
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
996
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
997
|
+
now
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function maxExpiry(stored) {
|
|
1001
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1002
|
+
(value) => value !== null
|
|
1003
|
+
);
|
|
1004
|
+
if (values.length === 0) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
return Math.max(...values);
|
|
1008
|
+
}
|
|
1009
|
+
function normalizePositiveSeconds(value) {
|
|
1010
|
+
if (!value || value <= 0) {
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
return value;
|
|
1014
|
+
}
|
|
1015
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1016
|
+
if (value == null) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ../../src/internal/CacheStackLayerWriter.ts
|
|
1023
|
+
var CacheStackLayerWriter = class {
|
|
1024
|
+
constructor(options) {
|
|
1025
|
+
this.options = options;
|
|
1026
|
+
}
|
|
1027
|
+
options;
|
|
1028
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1031
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
1032
|
+
const immediateOperations = [];
|
|
1033
|
+
const deferredOperations = [];
|
|
1034
|
+
for (const layer of this.options.layers) {
|
|
1035
|
+
const operation = async () => {
|
|
1036
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
1043
|
+
try {
|
|
1044
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1050
|
+
deferredOperations.push(operation);
|
|
1051
|
+
} else {
|
|
1052
|
+
immediateOperations.push(operation);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1056
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1057
|
+
}
|
|
1058
|
+
async writeBatch(entries) {
|
|
1059
|
+
const now = Date.now();
|
|
1060
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1061
|
+
const entryEpochs = new Map(
|
|
1062
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
1063
|
+
);
|
|
1064
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1065
|
+
const immediateOperations = [];
|
|
1066
|
+
const deferredOperations = [];
|
|
1067
|
+
for (const entry of entries) {
|
|
1068
|
+
for (const layer of this.options.layers) {
|
|
1069
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1073
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1074
|
+
bucket.push(layerEntry);
|
|
1075
|
+
entriesByLayer.set(layer, bucket);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1079
|
+
const operation = async () => {
|
|
1080
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const activeEntries = layerEntries.filter(
|
|
1084
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
1085
|
+
);
|
|
1086
|
+
if (activeEntries.length === 0) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
if (layer.setMany) {
|
|
1091
|
+
await layer.setMany(activeEntries);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1100
|
+
deferredOperations.push(operation);
|
|
1101
|
+
} else {
|
|
1102
|
+
immediateOperations.push(operation);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1106
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1107
|
+
return { clearEpoch, entryEpochs };
|
|
1108
|
+
}
|
|
1109
|
+
async executeLayerOperations(operations, context) {
|
|
1110
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1111
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1115
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1116
|
+
if (failures.length === 0) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
this.options.onWriteFailures(
|
|
1120
|
+
context,
|
|
1121
|
+
failures.map((failure) => failure.reason)
|
|
1122
|
+
);
|
|
1123
|
+
if (failures.length === operations.length) {
|
|
1124
|
+
throw new AggregateError(
|
|
1125
|
+
failures.map((failure) => failure.reason),
|
|
1126
|
+
`${context.action} failed for every cache layer`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
1131
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
1132
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
1133
|
+
layer.name,
|
|
1134
|
+
writeOptions?.staleWhileRevalidate,
|
|
1135
|
+
this.options.globalStaleWhileRevalidate
|
|
1136
|
+
);
|
|
1137
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
1138
|
+
layer.name,
|
|
1139
|
+
writeOptions?.staleIfError,
|
|
1140
|
+
this.options.globalStaleIfError
|
|
1141
|
+
);
|
|
1142
|
+
const payload = createStoredValueEnvelope({
|
|
1143
|
+
kind,
|
|
1144
|
+
value,
|
|
1145
|
+
freshTtlSeconds: freshTtl,
|
|
1146
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1147
|
+
staleIfErrorSeconds: staleIfError,
|
|
1148
|
+
now
|
|
1149
|
+
});
|
|
1150
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1151
|
+
return {
|
|
1152
|
+
key,
|
|
1153
|
+
value: payload,
|
|
1154
|
+
ttl
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
1160
|
+
var CacheStackMaintenance = class {
|
|
1161
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1162
|
+
writeBehindQueue = [];
|
|
1163
|
+
writeBehindTimer;
|
|
1164
|
+
writeBehindFlushPromise;
|
|
1165
|
+
generationCleanupPromise;
|
|
1166
|
+
clearEpoch = 0;
|
|
1167
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
1168
|
+
if (writeStrategy !== "write-behind") {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1172
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
this.disposeWriteBehindTimer();
|
|
1176
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1177
|
+
void flush();
|
|
1178
|
+
}, flushIntervalMs);
|
|
1179
|
+
this.writeBehindTimer.unref?.();
|
|
1180
|
+
}
|
|
1181
|
+
disposeWriteBehindTimer() {
|
|
1182
|
+
if (!this.writeBehindTimer) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
clearInterval(this.writeBehindTimer);
|
|
1186
|
+
this.writeBehindTimer = void 0;
|
|
1187
|
+
}
|
|
1188
|
+
beginClearEpoch() {
|
|
1189
|
+
this.clearEpoch += 1;
|
|
1190
|
+
this.keyEpochs.clear();
|
|
1191
|
+
this.writeBehindQueue.length = 0;
|
|
1192
|
+
}
|
|
1193
|
+
currentClearEpoch() {
|
|
1194
|
+
return this.clearEpoch;
|
|
1195
|
+
}
|
|
1196
|
+
currentKeyEpoch(key) {
|
|
1197
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1198
|
+
}
|
|
1199
|
+
bumpKeyEpochs(keys) {
|
|
1200
|
+
for (const key of keys) {
|
|
1201
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1205
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1214
|
+
this.writeBehindQueue.push(operation);
|
|
1215
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1216
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1217
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1218
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1222
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1226
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1227
|
+
await this.writeBehindFlushPromise;
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1231
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1232
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1233
|
+
try {
|
|
1234
|
+
await this.writeBehindFlushPromise;
|
|
1235
|
+
} finally {
|
|
1236
|
+
this.writeBehindFlushPromise = void 0;
|
|
1237
|
+
}
|
|
1238
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1239
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1243
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1244
|
+
onError(generation, error);
|
|
1245
|
+
});
|
|
1246
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1247
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1248
|
+
this.generationCleanupPromise = void 0;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
async waitForGenerationCleanup() {
|
|
1253
|
+
await this.generationCleanupPromise;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1258
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1259
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1260
|
+
}
|
|
1261
|
+
function shouldStartBackgroundRefresh({
|
|
1262
|
+
isDisconnecting,
|
|
1263
|
+
hasRefreshInFlight
|
|
1264
|
+
}) {
|
|
1265
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1266
|
+
}
|
|
1267
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1268
|
+
if (!gracefulDegradation) {
|
|
1269
|
+
return { degrade: false };
|
|
1270
|
+
}
|
|
1271
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1272
|
+
return {
|
|
1273
|
+
degrade: true,
|
|
1274
|
+
degradedUntil: now + retryAfterMs
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function planFreshReadPolicies({
|
|
1278
|
+
stored,
|
|
1279
|
+
hasFetcher,
|
|
1280
|
+
slidingTtl,
|
|
1281
|
+
refreshAheadSeconds
|
|
1282
|
+
}) {
|
|
1283
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1284
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1285
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1286
|
+
return {
|
|
1287
|
+
refreshedStored,
|
|
1288
|
+
refreshedStoredTtl,
|
|
1289
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1294
|
+
var import_node_fs = require("fs");
|
|
1295
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1296
|
+
|
|
1297
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
1298
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1299
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1300
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1301
|
+
}
|
|
1302
|
+
async function findExistingAncestor(directory, fs2, path2) {
|
|
1303
|
+
let current = directory;
|
|
1304
|
+
while (true) {
|
|
1305
|
+
try {
|
|
1306
|
+
await fs2.lstat(current);
|
|
1307
|
+
return current;
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (error.code !== "ENOENT") {
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const parent = path2.dirname(current);
|
|
1314
|
+
if (parent === current) {
|
|
1315
|
+
return current;
|
|
1316
|
+
}
|
|
1317
|
+
current = parent;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1321
|
+
if (filePath.length === 0) {
|
|
1322
|
+
throw new Error("filePath must not be empty.");
|
|
1323
|
+
}
|
|
1324
|
+
if (filePath.includes("\0")) {
|
|
1325
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1326
|
+
}
|
|
1327
|
+
const { promises: fs2 } = await import("fs");
|
|
1328
|
+
const path2 = await import("path");
|
|
1329
|
+
const resolved = path2.resolve(filePath);
|
|
1330
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1331
|
+
if (baseDir === false) {
|
|
1332
|
+
return resolved;
|
|
1333
|
+
}
|
|
1334
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
1335
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
1336
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1337
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (mode === "read") {
|
|
1340
|
+
const realTarget = await fs2.realpath(resolved);
|
|
1341
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1342
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1343
|
+
}
|
|
1344
|
+
return realTarget;
|
|
1345
|
+
}
|
|
1346
|
+
const parentDir = path2.dirname(resolved);
|
|
1347
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
|
|
1348
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
1349
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1350
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1351
|
+
}
|
|
1352
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
1353
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
1354
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1355
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1356
|
+
}
|
|
1357
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1358
|
+
try {
|
|
1359
|
+
const existing = await fs2.lstat(targetPath);
|
|
1360
|
+
if (existing.isSymbolicLink()) {
|
|
1361
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1362
|
+
}
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error.code !== "ENOENT") {
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return targetPath;
|
|
1369
|
+
}
|
|
1370
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1371
|
+
if (byteLimit === false) {
|
|
1372
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1373
|
+
}
|
|
1374
|
+
const chunks = [];
|
|
1375
|
+
let totalBytes = 0;
|
|
1376
|
+
let position = 0;
|
|
1377
|
+
while (true) {
|
|
1378
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1379
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1380
|
+
if (bytesRead === 0) {
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
totalBytes += bytesRead;
|
|
1384
|
+
if (totalBytes > byteLimit) {
|
|
1385
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1386
|
+
}
|
|
1387
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1388
|
+
position += bytesRead;
|
|
1389
|
+
}
|
|
1390
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1394
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1395
|
+
var CacheStackSnapshotManager = class {
|
|
1396
|
+
constructor(options) {
|
|
1397
|
+
this.options = options;
|
|
1398
|
+
}
|
|
1399
|
+
options;
|
|
1400
|
+
async exportState(maxEntries) {
|
|
1401
|
+
const entries = [];
|
|
1402
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1403
|
+
entries.push(entry);
|
|
1404
|
+
});
|
|
1405
|
+
return entries;
|
|
1406
|
+
}
|
|
1407
|
+
async importState(entries) {
|
|
1408
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1409
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1410
|
+
value: entry.value,
|
|
1411
|
+
ttl: entry.ttl
|
|
1412
|
+
}));
|
|
1413
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1414
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1415
|
+
await Promise.all(
|
|
1416
|
+
batch.map(async (entry) => {
|
|
1417
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1418
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1419
|
+
})
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1424
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1425
|
+
const tempPath = import_node_path.default.join(
|
|
1426
|
+
import_node_path.default.dirname(targetPath),
|
|
1427
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1428
|
+
);
|
|
1429
|
+
let handle;
|
|
1430
|
+
try {
|
|
1431
|
+
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
1432
|
+
const openedHandle = handle;
|
|
1433
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1434
|
+
let wroteAny = false;
|
|
1435
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1436
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1437
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1438
|
+
wroteAny = true;
|
|
1439
|
+
});
|
|
1440
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1441
|
+
await openedHandle.close();
|
|
1442
|
+
handle = void 0;
|
|
1443
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
await handle?.close().catch(() => void 0);
|
|
1446
|
+
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
1447
|
+
throw error;
|
|
1448
|
+
}
|
|
1122
1449
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1450
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1451
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1452
|
+
const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1453
|
+
let raw;
|
|
1454
|
+
try {
|
|
1455
|
+
if (maxBytes !== false) {
|
|
1456
|
+
const stat = await handle.stat();
|
|
1457
|
+
if (stat.size > maxBytes) {
|
|
1458
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1462
|
+
} finally {
|
|
1463
|
+
await handle.close();
|
|
1464
|
+
}
|
|
1465
|
+
let parsed;
|
|
1466
|
+
try {
|
|
1467
|
+
parsed = JSON.parse(raw);
|
|
1468
|
+
} catch (cause) {
|
|
1469
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1470
|
+
}
|
|
1471
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1472
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1473
|
+
}
|
|
1474
|
+
await this.importState(
|
|
1475
|
+
parsed.map((entry) => ({
|
|
1476
|
+
key: entry.key,
|
|
1477
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1478
|
+
ttl: entry.ttl
|
|
1479
|
+
}))
|
|
1480
|
+
);
|
|
1138
1481
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1482
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1483
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1484
|
+
for (const layer of this.options.layers) {
|
|
1485
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const visitKey = async (key) => {
|
|
1489
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1490
|
+
if (exported.has(exportedKey)) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1494
|
+
if (stored === null) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
exported.add(exportedKey);
|
|
1498
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1499
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1500
|
+
}
|
|
1501
|
+
await visitor({
|
|
1502
|
+
key: exportedKey,
|
|
1503
|
+
value: stored,
|
|
1504
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1505
|
+
});
|
|
1506
|
+
};
|
|
1507
|
+
if (layer.forEachKey) {
|
|
1508
|
+
await layer.forEachKey(visitKey);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const keys = await layer.keys?.();
|
|
1512
|
+
for (const key of keys ?? []) {
|
|
1513
|
+
await visitKey(key);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1144
1516
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1517
|
+
isCacheSnapshotEntries(value) {
|
|
1518
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1519
|
+
if (!entry || typeof entry !== "object") {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
const candidate = entry;
|
|
1523
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1524
|
+
});
|
|
1150
1525
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1155
|
-
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1156
|
-
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1157
|
-
}
|
|
1158
|
-
function shouldStartBackgroundRefresh({
|
|
1159
|
-
isDisconnecting,
|
|
1160
|
-
hasRefreshInFlight
|
|
1161
|
-
}) {
|
|
1162
|
-
return !isDisconnecting && !hasRefreshInFlight;
|
|
1163
|
-
}
|
|
1164
|
-
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1165
|
-
if (!gracefulDegradation) {
|
|
1166
|
-
return { degrade: false };
|
|
1526
|
+
sanitizeSnapshotValue(value) {
|
|
1527
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1167
1528
|
}
|
|
1168
|
-
|
|
1169
|
-
return {
|
|
1170
|
-
degrade: true,
|
|
1171
|
-
degradedUntil: now + retryAfterMs
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
function planFreshReadPolicies({
|
|
1175
|
-
stored,
|
|
1176
|
-
hasFetcher,
|
|
1177
|
-
slidingTtl,
|
|
1178
|
-
refreshAheadSeconds
|
|
1179
|
-
}) {
|
|
1180
|
-
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1181
|
-
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1182
|
-
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1183
|
-
return {
|
|
1184
|
-
refreshedStored,
|
|
1185
|
-
refreshedStoredTtl,
|
|
1186
|
-
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1529
|
+
};
|
|
1189
1530
|
|
|
1190
1531
|
// ../../src/internal/CacheStackValidation.ts
|
|
1191
1532
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -1414,7 +1755,11 @@ var FetchRateLimiter = class {
|
|
|
1414
1755
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1415
1756
|
nextFetcherBucketId = 0;
|
|
1416
1757
|
drainTimer;
|
|
1758
|
+
isDisposed = false;
|
|
1417
1759
|
async schedule(options, context, task) {
|
|
1760
|
+
if (this.isDisposed) {
|
|
1761
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1762
|
+
}
|
|
1418
1763
|
if (!options) {
|
|
1419
1764
|
return task();
|
|
1420
1765
|
}
|
|
@@ -1437,6 +1782,27 @@ var FetchRateLimiter = class {
|
|
|
1437
1782
|
this.drain();
|
|
1438
1783
|
});
|
|
1439
1784
|
}
|
|
1785
|
+
dispose() {
|
|
1786
|
+
this.isDisposed = true;
|
|
1787
|
+
if (this.drainTimer) {
|
|
1788
|
+
clearTimeout(this.drainTimer);
|
|
1789
|
+
this.drainTimer = void 0;
|
|
1790
|
+
}
|
|
1791
|
+
for (const bucket of this.buckets.values()) {
|
|
1792
|
+
if (bucket.cleanupTimer) {
|
|
1793
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1794
|
+
bucket.cleanupTimer = void 0;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1798
|
+
for (const item of queue) {
|
|
1799
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
this.queuesByBucket.clear();
|
|
1803
|
+
this.pendingBuckets.clear();
|
|
1804
|
+
this.buckets.clear();
|
|
1805
|
+
}
|
|
1440
1806
|
normalize(options) {
|
|
1441
1807
|
const maxConcurrent = options.maxConcurrent;
|
|
1442
1808
|
const intervalMs = options.intervalMs;
|
|
@@ -1472,6 +1838,9 @@ var FetchRateLimiter = class {
|
|
|
1472
1838
|
return "global";
|
|
1473
1839
|
}
|
|
1474
1840
|
drain() {
|
|
1841
|
+
if (this.isDisposed) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1475
1844
|
if (this.drainTimer) {
|
|
1476
1845
|
clearTimeout(this.drainTimer);
|
|
1477
1846
|
this.drainTimer = void 0;
|
|
@@ -1568,6 +1937,9 @@ var FetchRateLimiter = class {
|
|
|
1568
1937
|
}
|
|
1569
1938
|
}
|
|
1570
1939
|
bucketState(bucketKey) {
|
|
1940
|
+
if (this.isDisposed) {
|
|
1941
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1942
|
+
}
|
|
1571
1943
|
const existing = this.buckets.get(bucketKey);
|
|
1572
1944
|
if (existing) {
|
|
1573
1945
|
return existing;
|
|
@@ -2026,19 +2398,19 @@ var TagIndex = class {
|
|
|
2026
2398
|
if (!this.knownKeys.delete(key)) {
|
|
2027
2399
|
return;
|
|
2028
2400
|
}
|
|
2029
|
-
const
|
|
2401
|
+
const path2 = [];
|
|
2030
2402
|
let node = this.root;
|
|
2031
2403
|
for (const character of key) {
|
|
2032
2404
|
const child = node.children.get(character);
|
|
2033
2405
|
if (!child) {
|
|
2034
2406
|
return;
|
|
2035
2407
|
}
|
|
2036
|
-
|
|
2408
|
+
path2.push([node, character]);
|
|
2037
2409
|
node = child;
|
|
2038
2410
|
}
|
|
2039
2411
|
node.terminal = false;
|
|
2040
|
-
for (let index =
|
|
2041
|
-
const entry =
|
|
2412
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2413
|
+
const entry = path2[index];
|
|
2042
2414
|
if (!entry) {
|
|
2043
2415
|
continue;
|
|
2044
2416
|
}
|
|
@@ -2052,39 +2424,31 @@ var TagIndex = class {
|
|
|
2052
2424
|
}
|
|
2053
2425
|
};
|
|
2054
2426
|
|
|
2055
|
-
// ../../src/
|
|
2056
|
-
var
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
}
|
|
2062
|
-
deserialize(payload) {
|
|
2063
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2064
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
2065
|
-
}
|
|
2066
|
-
};
|
|
2067
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
2068
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2427
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2428
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2429
|
+
function sanitizeStructuredData(value, options) {
|
|
2430
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2431
|
+
}
|
|
2432
|
+
function sanitizeValue(value, depth, state, options) {
|
|
2069
2433
|
state.count += 1;
|
|
2070
|
-
if (state.count >
|
|
2071
|
-
throw new Error(
|
|
2434
|
+
if (state.count > options.maxNodes) {
|
|
2435
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2072
2436
|
}
|
|
2073
|
-
if (depth >
|
|
2074
|
-
throw new Error(
|
|
2437
|
+
if (depth > options.maxDepth) {
|
|
2438
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2075
2439
|
}
|
|
2076
2440
|
if (Array.isArray(value)) {
|
|
2077
|
-
return value.map((entry) =>
|
|
2441
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2078
2442
|
}
|
|
2079
2443
|
if (!isPlainObject(value)) {
|
|
2080
2444
|
return value;
|
|
2081
2445
|
}
|
|
2082
|
-
const sanitized = {};
|
|
2446
|
+
const sanitized = options.createObject?.() ?? {};
|
|
2083
2447
|
for (const [key, entry] of Object.entries(value)) {
|
|
2084
|
-
if (
|
|
2448
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
2085
2449
|
continue;
|
|
2086
2450
|
}
|
|
2087
|
-
sanitized[key] =
|
|
2451
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2088
2452
|
}
|
|
2089
2453
|
return sanitized;
|
|
2090
2454
|
}
|
|
@@ -2092,6 +2456,21 @@ function isPlainObject(value) {
|
|
|
2092
2456
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2093
2457
|
}
|
|
2094
2458
|
|
|
2459
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
2460
|
+
var JsonSerializer = class {
|
|
2461
|
+
serialize(value) {
|
|
2462
|
+
return JSON.stringify(value);
|
|
2463
|
+
}
|
|
2464
|
+
deserialize(payload) {
|
|
2465
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2466
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2467
|
+
label: "JSON payload",
|
|
2468
|
+
maxDepth: 200,
|
|
2469
|
+
maxNodes: 1e4
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2095
2474
|
// ../../src/stampede/StampedeGuard.ts
|
|
2096
2475
|
var StampedeGuard = class {
|
|
2097
2476
|
mutexes = /* @__PURE__ */ new Map();
|
|
@@ -2135,7 +2514,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
2135
2514
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
2136
2515
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2137
2516
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
2138
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
2139
2517
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
2140
2518
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
2141
2519
|
var DebugLogger = class {
|
|
@@ -2192,6 +2570,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2192
2570
|
await this.handleLayerFailure(layer, operation, error);
|
|
2193
2571
|
}
|
|
2194
2572
|
});
|
|
2573
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2574
|
+
tagIndex: this.tagIndex,
|
|
2575
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2576
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2577
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2581
|
+
layers: this.layers,
|
|
2582
|
+
maintenance: this.maintenance,
|
|
2583
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2584
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2585
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2586
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2587
|
+
},
|
|
2588
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2589
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2590
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2591
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2592
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2593
|
+
writePolicy: this.options.writePolicy,
|
|
2594
|
+
onWriteFailures: (context, failures) => {
|
|
2595
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2596
|
+
this.logger.debug?.("write-failure", {
|
|
2597
|
+
...context,
|
|
2598
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2195
2602
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
2196
2603
|
this.logger.warn?.(
|
|
2197
2604
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -2207,6 +2614,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2207
2614
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
2208
2615
|
);
|
|
2209
2616
|
}
|
|
2617
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2618
|
+
layers: this.layers,
|
|
2619
|
+
tagIndex: this.tagIndex,
|
|
2620
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2621
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2622
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2623
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2624
|
+
validateCacheKey,
|
|
2625
|
+
formatError: this.formatError.bind(this)
|
|
2626
|
+
});
|
|
2210
2627
|
this.initializeWriteBehind(options.writeBehind);
|
|
2211
2628
|
this.startup = this.initialize();
|
|
2212
2629
|
}
|
|
@@ -2222,11 +2639,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2222
2639
|
keyDiscovery;
|
|
2223
2640
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2224
2641
|
snapshotSerializer = new JsonSerializer();
|
|
2642
|
+
invalidation;
|
|
2643
|
+
layerWriter;
|
|
2644
|
+
snapshots;
|
|
2225
2645
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2226
2646
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2227
2647
|
maintenance = new CacheStackMaintenance();
|
|
2228
2648
|
ttlResolver;
|
|
2229
2649
|
circuitBreakerManager;
|
|
2650
|
+
nextOperationId = 0;
|
|
2230
2651
|
currentGeneration;
|
|
2231
2652
|
isDisconnecting = false;
|
|
2232
2653
|
disconnectPromise;
|
|
@@ -2237,10 +2658,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2237
2658
|
* and no `fetcher` is provided.
|
|
2238
2659
|
*/
|
|
2239
2660
|
async get(key, fetcher, options) {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2661
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2662
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2663
|
+
this.validateWriteOptions(options);
|
|
2664
|
+
await this.awaitStartup("get");
|
|
2665
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2666
|
+
});
|
|
2244
2667
|
}
|
|
2245
2668
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2246
2669
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2362,23 +2785,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2362
2785
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2363
2786
|
*/
|
|
2364
2787
|
async set(key, value, options) {
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2788
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2789
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2790
|
+
this.validateWriteOptions(options);
|
|
2791
|
+
await this.awaitStartup("set");
|
|
2792
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2793
|
+
});
|
|
2369
2794
|
}
|
|
2370
2795
|
/**
|
|
2371
2796
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2372
2797
|
*/
|
|
2373
2798
|
async delete(key) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2799
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2800
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2801
|
+
await this.awaitStartup("delete");
|
|
2802
|
+
await this.deleteKeys([normalizedKey]);
|
|
2803
|
+
await this.publishInvalidation({
|
|
2804
|
+
scope: "key",
|
|
2805
|
+
keys: [normalizedKey],
|
|
2806
|
+
sourceId: this.instanceId,
|
|
2807
|
+
operation: "delete"
|
|
2808
|
+
});
|
|
2382
2809
|
});
|
|
2383
2810
|
}
|
|
2384
2811
|
async clear() {
|
|
@@ -2411,95 +2838,99 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2411
2838
|
});
|
|
2412
2839
|
}
|
|
2413
2840
|
async mget(entries) {
|
|
2414
|
-
this.
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2841
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2842
|
+
this.assertActive("mget");
|
|
2843
|
+
if (entries.length === 0) {
|
|
2844
|
+
return [];
|
|
2845
|
+
}
|
|
2846
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2847
|
+
...entry,
|
|
2848
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2849
|
+
}));
|
|
2850
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2851
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2852
|
+
if (!canFastPath) {
|
|
2853
|
+
await this.awaitStartup("mget");
|
|
2854
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2855
|
+
return Promise.all(
|
|
2856
|
+
normalizedEntries.map((entry) => {
|
|
2857
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2858
|
+
const existing = pendingReads.get(entry.key);
|
|
2859
|
+
if (!existing) {
|
|
2860
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2861
|
+
pendingReads.set(entry.key, {
|
|
2862
|
+
promise,
|
|
2863
|
+
fetch: entry.fetch,
|
|
2864
|
+
optionsSignature
|
|
2865
|
+
});
|
|
2866
|
+
return promise;
|
|
2867
|
+
}
|
|
2868
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2869
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2870
|
+
}
|
|
2871
|
+
return existing.promise;
|
|
2872
|
+
})
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2425
2875
|
await this.awaitStartup("mget");
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2876
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2877
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2878
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2879
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2880
|
+
const entry = normalizedEntries[index];
|
|
2881
|
+
if (!entry) continue;
|
|
2882
|
+
const key = entry.key;
|
|
2883
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2884
|
+
indexes.push(index);
|
|
2885
|
+
indexesByKey.set(key, indexes);
|
|
2886
|
+
pending.add(key);
|
|
2887
|
+
}
|
|
2888
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2889
|
+
const layer = this.layers[layerIndex];
|
|
2890
|
+
if (!layer) continue;
|
|
2891
|
+
const keys = [...pending];
|
|
2892
|
+
if (keys.length === 0) {
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2896
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2897
|
+
const key = keys[offset];
|
|
2898
|
+
const stored = values[offset];
|
|
2899
|
+
if (!key || stored === null) {
|
|
2900
|
+
continue;
|
|
2439
2901
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2902
|
+
const resolved = resolveStoredValue(stored);
|
|
2903
|
+
if (resolved.state === "expired") {
|
|
2904
|
+
await layer.delete(key);
|
|
2905
|
+
continue;
|
|
2442
2906
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2449
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2450
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2451
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2452
|
-
const entry = normalizedEntries[index];
|
|
2453
|
-
if (!entry) continue;
|
|
2454
|
-
const key = entry.key;
|
|
2455
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2456
|
-
indexes.push(index);
|
|
2457
|
-
indexesByKey.set(key, indexes);
|
|
2458
|
-
pending.add(key);
|
|
2459
|
-
}
|
|
2460
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2461
|
-
const layer = this.layers[layerIndex];
|
|
2462
|
-
if (!layer) continue;
|
|
2463
|
-
const keys = [...pending];
|
|
2464
|
-
if (keys.length === 0) {
|
|
2465
|
-
break;
|
|
2466
|
-
}
|
|
2467
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2468
|
-
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2469
|
-
const key = keys[offset];
|
|
2470
|
-
const stored = values[offset];
|
|
2471
|
-
if (!key || stored === null) {
|
|
2472
|
-
continue;
|
|
2473
|
-
}
|
|
2474
|
-
const resolved = resolveStoredValue(stored);
|
|
2475
|
-
if (resolved.state === "expired") {
|
|
2476
|
-
await layer.delete(key);
|
|
2477
|
-
continue;
|
|
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);
|
|
2478
2912
|
}
|
|
2479
|
-
await this.tagIndex.touch(key);
|
|
2480
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2481
|
-
resultsByKey.set(key, resolved.value);
|
|
2482
|
-
pending.delete(key);
|
|
2483
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2484
2913
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
+
}
|
|
2490
2919
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2920
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2921
|
+
});
|
|
2493
2922
|
}
|
|
2494
2923
|
async mset(entries) {
|
|
2495
|
-
this.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
+
});
|
|
2503
2934
|
}
|
|
2504
2935
|
async warm(entries, options = {}) {
|
|
2505
2936
|
this.assertActive("warm");
|
|
@@ -2552,40 +2983,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2552
2983
|
return new CacheNamespace(this, prefix);
|
|
2553
2984
|
}
|
|
2554
2985
|
async invalidateByTag(tag) {
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
+
});
|
|
2560
2993
|
}
|
|
2561
2994
|
async invalidateByTags(tags, mode = "any") {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
+
});
|
|
2572
3009
|
}
|
|
2573
3010
|
async invalidateByPattern(pattern) {
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
this.
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
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
|
+
});
|
|
2582
3021
|
}
|
|
2583
3022
|
async invalidateByPrefix(prefix) {
|
|
2584
|
-
await this.
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
+
});
|
|
2589
3030
|
}
|
|
2590
3031
|
getMetrics() {
|
|
2591
3032
|
return this.metricsCollector.snapshot;
|
|
@@ -2696,95 +3137,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2696
3137
|
}
|
|
2697
3138
|
async exportState() {
|
|
2698
3139
|
await this.awaitStartup("exportState");
|
|
2699
|
-
|
|
2700
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2701
|
-
entries.push(entry);
|
|
2702
|
-
});
|
|
2703
|
-
return entries;
|
|
3140
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2704
3141
|
}
|
|
2705
3142
|
async importState(entries) {
|
|
2706
3143
|
await this.awaitStartup("importState");
|
|
2707
|
-
|
|
2708
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2709
|
-
value: entry.value,
|
|
2710
|
-
ttl: entry.ttl
|
|
2711
|
-
}));
|
|
2712
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2713
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2714
|
-
await Promise.all(
|
|
2715
|
-
batch.map(async (entry) => {
|
|
2716
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2717
|
-
await this.tagIndex.touch(entry.key);
|
|
2718
|
-
})
|
|
2719
|
-
);
|
|
2720
|
-
}
|
|
3144
|
+
await this.snapshots.importState(entries);
|
|
2721
3145
|
}
|
|
2722
3146
|
async persistToFile(filePath) {
|
|
2723
3147
|
this.assertActive("persistToFile");
|
|
2724
|
-
|
|
2725
|
-
const path = await import("path");
|
|
2726
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2727
|
-
const tempPath = path.join(
|
|
2728
|
-
path.dirname(targetPath),
|
|
2729
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2730
|
-
);
|
|
2731
|
-
let handle;
|
|
2732
|
-
try {
|
|
2733
|
-
handle = await fs.open(tempPath, "wx");
|
|
2734
|
-
const openedHandle = handle;
|
|
2735
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2736
|
-
let wroteAny = false;
|
|
2737
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2738
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2739
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2740
|
-
wroteAny = true;
|
|
2741
|
-
});
|
|
2742
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2743
|
-
await openedHandle.close();
|
|
2744
|
-
handle = void 0;
|
|
2745
|
-
await fs.rename(tempPath, targetPath);
|
|
2746
|
-
} catch (error) {
|
|
2747
|
-
await handle?.close().catch(() => void 0);
|
|
2748
|
-
await fs.unlink(tempPath).catch(() => void 0);
|
|
2749
|
-
throw error;
|
|
2750
|
-
}
|
|
3148
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2751
3149
|
}
|
|
2752
3150
|
async restoreFromFile(filePath) {
|
|
2753
3151
|
this.assertActive("restoreFromFile");
|
|
2754
|
-
|
|
2755
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2756
|
-
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2757
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2758
|
-
let raw;
|
|
2759
|
-
try {
|
|
2760
|
-
if (snapshotMaxBytes !== false) {
|
|
2761
|
-
const stat = await handle.stat();
|
|
2762
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2763
|
-
throw new Error(
|
|
2764
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2765
|
-
);
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2769
|
-
} finally {
|
|
2770
|
-
await handle.close();
|
|
2771
|
-
}
|
|
2772
|
-
let parsed;
|
|
2773
|
-
try {
|
|
2774
|
-
parsed = JSON.parse(raw);
|
|
2775
|
-
} catch (cause) {
|
|
2776
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2777
|
-
}
|
|
2778
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2779
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2780
|
-
}
|
|
2781
|
-
await this.importState(
|
|
2782
|
-
parsed.map((entry) => ({
|
|
2783
|
-
key: entry.key,
|
|
2784
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2785
|
-
ttl: entry.ttl
|
|
2786
|
-
}))
|
|
2787
|
-
);
|
|
3152
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2788
3153
|
}
|
|
2789
3154
|
async disconnect() {
|
|
2790
3155
|
if (!this.disconnectPromise) {
|
|
@@ -2796,6 +3161,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2796
3161
|
await this.maintenance.waitForGenerationCleanup();
|
|
2797
3162
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2798
3163
|
this.maintenance.disposeWriteBehindTimer();
|
|
3164
|
+
this.fetchRateLimiter.dispose();
|
|
2799
3165
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2800
3166
|
})();
|
|
2801
3167
|
}
|
|
@@ -2909,7 +3275,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2909
3275
|
async storeEntry(key, kind, value, options) {
|
|
2910
3276
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2911
3277
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2912
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
3278
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2913
3279
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2914
3280
|
return;
|
|
2915
3281
|
}
|
|
@@ -2926,52 +3292,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2926
3292
|
}
|
|
2927
3293
|
}
|
|
2928
3294
|
async writeBatch(entries) {
|
|
2929
|
-
const
|
|
2930
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2931
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2932
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2933
|
-
const immediateOperations = [];
|
|
2934
|
-
const deferredOperations = [];
|
|
2935
|
-
for (const entry of entries) {
|
|
2936
|
-
for (const layer of this.layers) {
|
|
2937
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2938
|
-
continue;
|
|
2939
|
-
}
|
|
2940
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2941
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2942
|
-
bucket.push(layerEntry);
|
|
2943
|
-
entriesByLayer.set(layer, bucket);
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2947
|
-
const operation = async () => {
|
|
2948
|
-
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2949
|
-
return;
|
|
2950
|
-
}
|
|
2951
|
-
const activeEntries = layerEntries.filter(
|
|
2952
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2953
|
-
);
|
|
2954
|
-
if (activeEntries.length === 0) {
|
|
2955
|
-
return;
|
|
2956
|
-
}
|
|
2957
|
-
try {
|
|
2958
|
-
if (layer.setMany) {
|
|
2959
|
-
await layer.setMany(activeEntries);
|
|
2960
|
-
return;
|
|
2961
|
-
}
|
|
2962
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2963
|
-
} catch (error) {
|
|
2964
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2965
|
-
}
|
|
2966
|
-
};
|
|
2967
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2968
|
-
deferredOperations.push(operation);
|
|
2969
|
-
} else {
|
|
2970
|
-
immediateOperations.push(operation);
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2974
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
3295
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2975
3296
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2976
3297
|
return;
|
|
2977
3298
|
}
|
|
@@ -3078,58 +3399,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3078
3399
|
this.emit("backfill", { key, layer: layer.name });
|
|
3079
3400
|
}
|
|
3080
3401
|
}
|
|
3081
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
3082
|
-
const now = Date.now();
|
|
3083
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3084
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3085
|
-
const immediateOperations = [];
|
|
3086
|
-
const deferredOperations = [];
|
|
3087
|
-
for (const layer of this.layers) {
|
|
3088
|
-
const operation = async () => {
|
|
3089
|
-
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3090
|
-
return;
|
|
3091
|
-
}
|
|
3092
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3093
|
-
return;
|
|
3094
|
-
}
|
|
3095
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
3096
|
-
try {
|
|
3097
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
3098
|
-
} catch (error) {
|
|
3099
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
3100
|
-
}
|
|
3101
|
-
};
|
|
3102
|
-
if (this.shouldWriteBehind(layer)) {
|
|
3103
|
-
deferredOperations.push(operation);
|
|
3104
|
-
} else {
|
|
3105
|
-
immediateOperations.push(operation);
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
3109
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
3110
|
-
}
|
|
3111
|
-
async executeLayerOperations(operations, context) {
|
|
3112
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
3113
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
3114
|
-
return;
|
|
3115
|
-
}
|
|
3116
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
3117
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
3118
|
-
if (failures.length === 0) {
|
|
3119
|
-
return;
|
|
3120
|
-
}
|
|
3121
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3122
|
-
this.logger.debug?.("write-failure", {
|
|
3123
|
-
...context,
|
|
3124
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
3125
|
-
});
|
|
3126
|
-
if (failures.length === operations.length) {
|
|
3127
|
-
throw new AggregateError(
|
|
3128
|
-
failures.map((failure) => failure.reason),
|
|
3129
|
-
`${context.action} failed for every cache layer`
|
|
3130
|
-
);
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
3402
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
3134
3403
|
return this.ttlResolver.resolveFreshTtl(
|
|
3135
3404
|
key,
|
|
@@ -3195,7 +3464,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3195
3464
|
return;
|
|
3196
3465
|
}
|
|
3197
3466
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3198
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3467
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
3199
3468
|
for (const key of keys) {
|
|
3200
3469
|
await this.tagIndex.remove(key);
|
|
3201
3470
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -3227,7 +3496,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3227
3496
|
}
|
|
3228
3497
|
const keys = message.keys ?? [];
|
|
3229
3498
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3230
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3499
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3231
3500
|
if (message.operation !== "write") {
|
|
3232
3501
|
for (const key of keys) {
|
|
3233
3502
|
await this.tagIndex.remove(key);
|
|
@@ -3284,6 +3553,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3284
3553
|
shouldBroadcastL1Invalidation() {
|
|
3285
3554
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3286
3555
|
}
|
|
3556
|
+
async observeOperation(name, attributes, execute) {
|
|
3557
|
+
const id = this.nextOperationId;
|
|
3558
|
+
this.nextOperationId += 1;
|
|
3559
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3560
|
+
try {
|
|
3561
|
+
const result = await execute();
|
|
3562
|
+
this.emit("operation-end", {
|
|
3563
|
+
id,
|
|
3564
|
+
name,
|
|
3565
|
+
attributes,
|
|
3566
|
+
success: true,
|
|
3567
|
+
result: result === null ? "null" : void 0
|
|
3568
|
+
});
|
|
3569
|
+
return result;
|
|
3570
|
+
} catch (error) {
|
|
3571
|
+
this.emit("operation-end", {
|
|
3572
|
+
id,
|
|
3573
|
+
name,
|
|
3574
|
+
attributes,
|
|
3575
|
+
success: false,
|
|
3576
|
+
error
|
|
3577
|
+
});
|
|
3578
|
+
throw error;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3287
3581
|
scheduleGenerationCleanup(generation) {
|
|
3288
3582
|
this.maintenance.scheduleGenerationCleanup(
|
|
3289
3583
|
generation,
|
|
@@ -3339,37 +3633,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3339
3633
|
});
|
|
3340
3634
|
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3341
3635
|
}
|
|
3342
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3343
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3344
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3345
|
-
layer.name,
|
|
3346
|
-
options?.staleWhileRevalidate,
|
|
3347
|
-
this.options.staleWhileRevalidate
|
|
3348
|
-
);
|
|
3349
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3350
|
-
const payload = createStoredValueEnvelope({
|
|
3351
|
-
kind,
|
|
3352
|
-
value,
|
|
3353
|
-
freshTtlSeconds: freshTtl,
|
|
3354
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3355
|
-
staleIfErrorSeconds: staleIfError,
|
|
3356
|
-
now
|
|
3357
|
-
});
|
|
3358
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
3359
|
-
return {
|
|
3360
|
-
key,
|
|
3361
|
-
value: payload,
|
|
3362
|
-
ttl
|
|
3363
|
-
};
|
|
3364
|
-
}
|
|
3365
|
-
intersectKeys(groups) {
|
|
3366
|
-
if (groups.length === 0) {
|
|
3367
|
-
return [];
|
|
3368
|
-
}
|
|
3369
|
-
const [firstGroup, ...rest] = groups;
|
|
3370
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3371
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3372
|
-
}
|
|
3373
3636
|
qualifyKey(key) {
|
|
3374
3637
|
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3375
3638
|
}
|
|
@@ -3379,32 +3642,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3379
3642
|
stripQualifiedKey(key) {
|
|
3380
3643
|
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3381
3644
|
}
|
|
3382
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3383
|
-
await Promise.all(
|
|
3384
|
-
layers.map(async (layer) => {
|
|
3385
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3386
|
-
return;
|
|
3387
|
-
}
|
|
3388
|
-
if (layer.deleteMany) {
|
|
3389
|
-
try {
|
|
3390
|
-
await layer.deleteMany(keys);
|
|
3391
|
-
} catch (error) {
|
|
3392
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3393
|
-
}
|
|
3394
|
-
return;
|
|
3395
|
-
}
|
|
3396
|
-
await Promise.all(
|
|
3397
|
-
keys.map(async (key) => {
|
|
3398
|
-
try {
|
|
3399
|
-
await layer.delete(key);
|
|
3400
|
-
} catch (error) {
|
|
3401
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3402
|
-
}
|
|
3403
|
-
})
|
|
3404
|
-
);
|
|
3405
|
-
})
|
|
3406
|
-
);
|
|
3407
|
-
}
|
|
3408
3645
|
validateConfiguration() {
|
|
3409
3646
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
3410
3647
|
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
@@ -3535,18 +3772,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3535
3772
|
this.emit("error", { operation, ...context });
|
|
3536
3773
|
}
|
|
3537
3774
|
}
|
|
3538
|
-
isCacheSnapshotEntries(value) {
|
|
3539
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3540
|
-
if (!entry || typeof entry !== "object") {
|
|
3541
|
-
return false;
|
|
3542
|
-
}
|
|
3543
|
-
const candidate = entry;
|
|
3544
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3545
|
-
});
|
|
3546
|
-
}
|
|
3547
|
-
sanitizeSnapshotValue(value) {
|
|
3548
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3549
|
-
}
|
|
3550
3775
|
snapshotMaxBytes() {
|
|
3551
3776
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3552
3777
|
}
|
|
@@ -3556,62 +3781,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3556
3781
|
invalidationMaxKeys() {
|
|
3557
3782
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3558
3783
|
}
|
|
3559
|
-
async collectKeysForTag(tag) {
|
|
3560
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3561
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3562
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3563
|
-
keys.add(key);
|
|
3564
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3565
|
-
});
|
|
3566
|
-
return [...keys];
|
|
3567
|
-
}
|
|
3568
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3569
|
-
keys.add(key);
|
|
3570
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3571
|
-
}
|
|
3572
|
-
return [...keys];
|
|
3573
|
-
}
|
|
3574
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3575
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3576
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3577
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3581
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3582
|
-
for (const layer of this.layers) {
|
|
3583
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3584
|
-
continue;
|
|
3585
|
-
}
|
|
3586
|
-
const visitKey = async (key) => {
|
|
3587
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3588
|
-
if (exported.has(exportedKey)) {
|
|
3589
|
-
return;
|
|
3590
|
-
}
|
|
3591
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3592
|
-
if (stored === null) {
|
|
3593
|
-
return;
|
|
3594
|
-
}
|
|
3595
|
-
exported.add(exportedKey);
|
|
3596
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3597
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3598
|
-
}
|
|
3599
|
-
await visitor({
|
|
3600
|
-
key: exportedKey,
|
|
3601
|
-
value: stored,
|
|
3602
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3603
|
-
});
|
|
3604
|
-
};
|
|
3605
|
-
if (layer.forEachKey) {
|
|
3606
|
-
await layer.forEachKey(visitKey);
|
|
3607
|
-
continue;
|
|
3608
|
-
}
|
|
3609
|
-
const keys = await layer.keys?.();
|
|
3610
|
-
for (const key of keys ?? []) {
|
|
3611
|
-
await visitKey(key);
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
3784
|
};
|
|
3616
3785
|
|
|
3617
3786
|
// src/module.ts
|