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
package/dist/index.cjs
CHANGED
|
@@ -556,102 +556,6 @@ function createInstanceId() {
|
|
|
556
556
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
// src/internal/CacheSnapshotFile.ts
|
|
560
|
-
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
561
|
-
const relative = path.relative(realBaseDir, candidatePath);
|
|
562
|
-
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
563
|
-
}
|
|
564
|
-
async function findExistingAncestor(directory, fs2, path) {
|
|
565
|
-
let current = directory;
|
|
566
|
-
while (true) {
|
|
567
|
-
try {
|
|
568
|
-
await fs2.lstat(current);
|
|
569
|
-
return current;
|
|
570
|
-
} catch (error) {
|
|
571
|
-
if (error.code !== "ENOENT") {
|
|
572
|
-
throw error;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
const parent = path.dirname(current);
|
|
576
|
-
if (parent === current) {
|
|
577
|
-
return current;
|
|
578
|
-
}
|
|
579
|
-
current = parent;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
583
|
-
if (filePath.length === 0) {
|
|
584
|
-
throw new Error("filePath must not be empty.");
|
|
585
|
-
}
|
|
586
|
-
if (filePath.includes("\0")) {
|
|
587
|
-
throw new Error("filePath must not contain null bytes.");
|
|
588
|
-
}
|
|
589
|
-
const { promises: fs2 } = await import("fs");
|
|
590
|
-
const path = await import("path");
|
|
591
|
-
const resolved = path.resolve(filePath);
|
|
592
|
-
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
593
|
-
if (baseDir === false) {
|
|
594
|
-
return resolved;
|
|
595
|
-
}
|
|
596
|
-
await fs2.mkdir(baseDir, { recursive: true });
|
|
597
|
-
const realBaseDir = await fs2.realpath(baseDir);
|
|
598
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
599
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
600
|
-
}
|
|
601
|
-
if (mode === "read") {
|
|
602
|
-
const realTarget = await fs2.realpath(resolved);
|
|
603
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
604
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
605
|
-
}
|
|
606
|
-
return realTarget;
|
|
607
|
-
}
|
|
608
|
-
const parentDir = path.dirname(resolved);
|
|
609
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
|
|
610
|
-
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
611
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
612
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
613
|
-
}
|
|
614
|
-
await fs2.mkdir(parentDir, { recursive: true });
|
|
615
|
-
const realParentDir = await fs2.realpath(parentDir);
|
|
616
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
617
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
618
|
-
}
|
|
619
|
-
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
620
|
-
try {
|
|
621
|
-
const existing = await fs2.lstat(targetPath);
|
|
622
|
-
if (existing.isSymbolicLink()) {
|
|
623
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
624
|
-
}
|
|
625
|
-
} catch (error) {
|
|
626
|
-
if (error.code !== "ENOENT") {
|
|
627
|
-
throw error;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return targetPath;
|
|
631
|
-
}
|
|
632
|
-
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
633
|
-
if (byteLimit === false) {
|
|
634
|
-
return handle.readFile({ encoding: "utf8" });
|
|
635
|
-
}
|
|
636
|
-
const chunks = [];
|
|
637
|
-
let totalBytes = 0;
|
|
638
|
-
let position = 0;
|
|
639
|
-
while (true) {
|
|
640
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
641
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
642
|
-
if (bytesRead === 0) {
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
totalBytes += bytesRead;
|
|
646
|
-
if (totalBytes > byteLimit) {
|
|
647
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
648
|
-
}
|
|
649
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
650
|
-
position += bytesRead;
|
|
651
|
-
}
|
|
652
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
559
|
// src/internal/CacheStackGeneration.ts
|
|
656
560
|
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
657
561
|
function generationPrefix(generation) {
|
|
@@ -699,102 +603,66 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
|
699
603
|
return batches;
|
|
700
604
|
}
|
|
701
605
|
|
|
702
|
-
// src/internal/
|
|
703
|
-
var
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
writeBehindTimer;
|
|
707
|
-
writeBehindFlushPromise;
|
|
708
|
-
generationCleanupPromise;
|
|
709
|
-
clearEpoch = 0;
|
|
710
|
-
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
711
|
-
if (writeStrategy !== "write-behind") {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
const flushIntervalMs = options?.flushIntervalMs;
|
|
715
|
-
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
this.disposeWriteBehindTimer();
|
|
719
|
-
this.writeBehindTimer = setInterval(() => {
|
|
720
|
-
void flush();
|
|
721
|
-
}, flushIntervalMs);
|
|
722
|
-
this.writeBehindTimer.unref?.();
|
|
606
|
+
// src/internal/CacheStackInvalidationSupport.ts
|
|
607
|
+
var CacheStackInvalidationSupport = class {
|
|
608
|
+
constructor(options) {
|
|
609
|
+
this.options = options;
|
|
723
610
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
611
|
+
options;
|
|
612
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
613
|
+
const keys = /* @__PURE__ */ new Set();
|
|
614
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
615
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
616
|
+
keys.add(key);
|
|
617
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
618
|
+
});
|
|
619
|
+
return [...keys];
|
|
727
620
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
beginClearEpoch() {
|
|
732
|
-
this.clearEpoch += 1;
|
|
733
|
-
this.keyEpochs.clear();
|
|
734
|
-
this.writeBehindQueue.length = 0;
|
|
735
|
-
}
|
|
736
|
-
currentClearEpoch() {
|
|
737
|
-
return this.clearEpoch;
|
|
738
|
-
}
|
|
739
|
-
currentKeyEpoch(key) {
|
|
740
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
741
|
-
}
|
|
742
|
-
bumpKeyEpochs(keys) {
|
|
743
|
-
for (const key of keys) {
|
|
744
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
621
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
622
|
+
keys.add(key);
|
|
623
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
745
624
|
}
|
|
625
|
+
return [...keys];
|
|
746
626
|
}
|
|
747
|
-
|
|
748
|
-
if (
|
|
749
|
-
return
|
|
750
|
-
}
|
|
751
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
752
|
-
return true;
|
|
627
|
+
intersectKeys(groups) {
|
|
628
|
+
if (groups.length === 0) {
|
|
629
|
+
return [];
|
|
753
630
|
}
|
|
754
|
-
|
|
631
|
+
const [firstGroup, ...rest] = groups;
|
|
632
|
+
const restSets = rest.map((group) => new Set(group));
|
|
633
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
755
634
|
}
|
|
756
|
-
async
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
635
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
636
|
+
await Promise.all(
|
|
637
|
+
layers.map(async (layer) => {
|
|
638
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (layer.deleteMany) {
|
|
642
|
+
try {
|
|
643
|
+
await layer.deleteMany(keys);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
await Promise.all(
|
|
650
|
+
keys.map(async (key) => {
|
|
651
|
+
try {
|
|
652
|
+
await layer.delete(key);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
})
|
|
659
|
+
);
|
|
767
660
|
}
|
|
768
|
-
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const batchSize = options?.batchSize ?? 100;
|
|
774
|
-
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
775
|
-
this.writeBehindFlushPromise = flushBatch(batch);
|
|
776
|
-
try {
|
|
777
|
-
await this.writeBehindFlushPromise;
|
|
778
|
-
} finally {
|
|
779
|
-
this.writeBehindFlushPromise = void 0;
|
|
780
|
-
}
|
|
781
|
-
if (this.writeBehindQueue.length > 0) {
|
|
782
|
-
await this.flushWriteBehindQueue(options, flushBatch);
|
|
661
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
662
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
663
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
783
664
|
}
|
|
784
665
|
}
|
|
785
|
-
scheduleGenerationCleanup(generation, task, onError) {
|
|
786
|
-
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
787
|
-
onError(generation, error);
|
|
788
|
-
});
|
|
789
|
-
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
790
|
-
if (this.generationCleanupPromise === scheduledTask) {
|
|
791
|
-
this.generationCleanupPromise = void 0;
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
async waitForGenerationCleanup() {
|
|
796
|
-
await this.generationCleanupPromise;
|
|
797
|
-
}
|
|
798
666
|
};
|
|
799
667
|
|
|
800
668
|
// src/internal/StoredValue.ts
|
|
@@ -946,50 +814,523 @@ function normalizePositiveSeconds(value) {
|
|
|
946
814
|
if (!value || value <= 0) {
|
|
947
815
|
return void 0;
|
|
948
816
|
}
|
|
949
|
-
return value;
|
|
950
|
-
}
|
|
951
|
-
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
952
|
-
if (value == null) {
|
|
953
|
-
return true;
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
820
|
+
if (value == null) {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/internal/CacheStackLayerWriter.ts
|
|
827
|
+
var CacheStackLayerWriter = class {
|
|
828
|
+
constructor(options) {
|
|
829
|
+
this.options = options;
|
|
830
|
+
}
|
|
831
|
+
options;
|
|
832
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
833
|
+
const now = Date.now();
|
|
834
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
835
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
836
|
+
const immediateOperations = [];
|
|
837
|
+
const deferredOperations = [];
|
|
838
|
+
for (const layer of this.options.layers) {
|
|
839
|
+
const operation = async () => {
|
|
840
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
847
|
+
try {
|
|
848
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
854
|
+
deferredOperations.push(operation);
|
|
855
|
+
} else {
|
|
856
|
+
immediateOperations.push(operation);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
860
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
861
|
+
}
|
|
862
|
+
async writeBatch(entries) {
|
|
863
|
+
const now = Date.now();
|
|
864
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
865
|
+
const entryEpochs = new Map(
|
|
866
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
867
|
+
);
|
|
868
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
869
|
+
const immediateOperations = [];
|
|
870
|
+
const deferredOperations = [];
|
|
871
|
+
for (const entry of entries) {
|
|
872
|
+
for (const layer of this.options.layers) {
|
|
873
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
877
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
878
|
+
bucket.push(layerEntry);
|
|
879
|
+
entriesByLayer.set(layer, bucket);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
883
|
+
const operation = async () => {
|
|
884
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const activeEntries = layerEntries.filter(
|
|
888
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
889
|
+
);
|
|
890
|
+
if (activeEntries.length === 0) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
if (layer.setMany) {
|
|
895
|
+
await layer.setMany(activeEntries);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
904
|
+
deferredOperations.push(operation);
|
|
905
|
+
} else {
|
|
906
|
+
immediateOperations.push(operation);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
910
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
911
|
+
return { clearEpoch, entryEpochs };
|
|
912
|
+
}
|
|
913
|
+
async executeLayerOperations(operations, context) {
|
|
914
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
915
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
919
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
920
|
+
if (failures.length === 0) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.options.onWriteFailures(
|
|
924
|
+
context,
|
|
925
|
+
failures.map((failure) => failure.reason)
|
|
926
|
+
);
|
|
927
|
+
if (failures.length === operations.length) {
|
|
928
|
+
throw new AggregateError(
|
|
929
|
+
failures.map((failure) => failure.reason),
|
|
930
|
+
`${context.action} failed for every cache layer`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
935
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
936
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
937
|
+
layer.name,
|
|
938
|
+
writeOptions?.staleWhileRevalidate,
|
|
939
|
+
this.options.globalStaleWhileRevalidate
|
|
940
|
+
);
|
|
941
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
942
|
+
layer.name,
|
|
943
|
+
writeOptions?.staleIfError,
|
|
944
|
+
this.options.globalStaleIfError
|
|
945
|
+
);
|
|
946
|
+
const payload = createStoredValueEnvelope({
|
|
947
|
+
kind,
|
|
948
|
+
value,
|
|
949
|
+
freshTtlSeconds: freshTtl,
|
|
950
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
951
|
+
staleIfErrorSeconds: staleIfError,
|
|
952
|
+
now
|
|
953
|
+
});
|
|
954
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
955
|
+
return {
|
|
956
|
+
key,
|
|
957
|
+
value: payload,
|
|
958
|
+
ttl
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// src/internal/CacheStackMaintenance.ts
|
|
964
|
+
var CacheStackMaintenance = class {
|
|
965
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
966
|
+
writeBehindQueue = [];
|
|
967
|
+
writeBehindTimer;
|
|
968
|
+
writeBehindFlushPromise;
|
|
969
|
+
generationCleanupPromise;
|
|
970
|
+
clearEpoch = 0;
|
|
971
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
972
|
+
if (writeStrategy !== "write-behind") {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
976
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
this.disposeWriteBehindTimer();
|
|
980
|
+
this.writeBehindTimer = setInterval(() => {
|
|
981
|
+
void flush();
|
|
982
|
+
}, flushIntervalMs);
|
|
983
|
+
this.writeBehindTimer.unref?.();
|
|
984
|
+
}
|
|
985
|
+
disposeWriteBehindTimer() {
|
|
986
|
+
if (!this.writeBehindTimer) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
clearInterval(this.writeBehindTimer);
|
|
990
|
+
this.writeBehindTimer = void 0;
|
|
991
|
+
}
|
|
992
|
+
beginClearEpoch() {
|
|
993
|
+
this.clearEpoch += 1;
|
|
994
|
+
this.keyEpochs.clear();
|
|
995
|
+
this.writeBehindQueue.length = 0;
|
|
996
|
+
}
|
|
997
|
+
currentClearEpoch() {
|
|
998
|
+
return this.clearEpoch;
|
|
999
|
+
}
|
|
1000
|
+
currentKeyEpoch(key) {
|
|
1001
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1002
|
+
}
|
|
1003
|
+
bumpKeyEpochs(keys) {
|
|
1004
|
+
for (const key of keys) {
|
|
1005
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1009
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1018
|
+
this.writeBehindQueue.push(operation);
|
|
1019
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1020
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1021
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1022
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1026
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1030
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1031
|
+
await this.writeBehindFlushPromise;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1035
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1036
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1037
|
+
try {
|
|
1038
|
+
await this.writeBehindFlushPromise;
|
|
1039
|
+
} finally {
|
|
1040
|
+
this.writeBehindFlushPromise = void 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1043
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1047
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1048
|
+
onError(generation, error);
|
|
1049
|
+
});
|
|
1050
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1051
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1052
|
+
this.generationCleanupPromise = void 0;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
async waitForGenerationCleanup() {
|
|
1057
|
+
await this.generationCleanupPromise;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
1062
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1063
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1064
|
+
}
|
|
1065
|
+
function shouldStartBackgroundRefresh({
|
|
1066
|
+
isDisconnecting,
|
|
1067
|
+
hasRefreshInFlight
|
|
1068
|
+
}) {
|
|
1069
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1070
|
+
}
|
|
1071
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1072
|
+
if (!gracefulDegradation) {
|
|
1073
|
+
return { degrade: false };
|
|
1074
|
+
}
|
|
1075
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1076
|
+
return {
|
|
1077
|
+
degrade: true,
|
|
1078
|
+
degradedUntil: now + retryAfterMs
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function planFreshReadPolicies({
|
|
1082
|
+
stored,
|
|
1083
|
+
hasFetcher,
|
|
1084
|
+
slidingTtl,
|
|
1085
|
+
refreshAheadSeconds
|
|
1086
|
+
}) {
|
|
1087
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1088
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1089
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1090
|
+
return {
|
|
1091
|
+
refreshedStored,
|
|
1092
|
+
refreshedStoredTtl,
|
|
1093
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1098
|
+
var import_node_fs = require("fs");
|
|
1099
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1100
|
+
|
|
1101
|
+
// src/internal/CacheSnapshotFile.ts
|
|
1102
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1103
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1104
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1105
|
+
}
|
|
1106
|
+
async function findExistingAncestor(directory, fs3, path2) {
|
|
1107
|
+
let current = directory;
|
|
1108
|
+
while (true) {
|
|
1109
|
+
try {
|
|
1110
|
+
await fs3.lstat(current);
|
|
1111
|
+
return current;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (error.code !== "ENOENT") {
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const parent = path2.dirname(current);
|
|
1118
|
+
if (parent === current) {
|
|
1119
|
+
return current;
|
|
1120
|
+
}
|
|
1121
|
+
current = parent;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1125
|
+
if (filePath.length === 0) {
|
|
1126
|
+
throw new Error("filePath must not be empty.");
|
|
1127
|
+
}
|
|
1128
|
+
if (filePath.includes("\0")) {
|
|
1129
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1130
|
+
}
|
|
1131
|
+
const { promises: fs3 } = await import("fs");
|
|
1132
|
+
const path2 = await import("path");
|
|
1133
|
+
const resolved = path2.resolve(filePath);
|
|
1134
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1135
|
+
if (baseDir === false) {
|
|
1136
|
+
return resolved;
|
|
1137
|
+
}
|
|
1138
|
+
await fs3.mkdir(baseDir, { recursive: true });
|
|
1139
|
+
const realBaseDir = await fs3.realpath(baseDir);
|
|
1140
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1141
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (mode === "read") {
|
|
1144
|
+
const realTarget = await fs3.realpath(resolved);
|
|
1145
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1146
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1147
|
+
}
|
|
1148
|
+
return realTarget;
|
|
1149
|
+
}
|
|
1150
|
+
const parentDir = path2.dirname(resolved);
|
|
1151
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
|
|
1152
|
+
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
1153
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1154
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1155
|
+
}
|
|
1156
|
+
await fs3.mkdir(parentDir, { recursive: true });
|
|
1157
|
+
const realParentDir = await fs3.realpath(parentDir);
|
|
1158
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1159
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1160
|
+
}
|
|
1161
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1162
|
+
try {
|
|
1163
|
+
const existing = await fs3.lstat(targetPath);
|
|
1164
|
+
if (existing.isSymbolicLink()) {
|
|
1165
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1166
|
+
}
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (error.code !== "ENOENT") {
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return targetPath;
|
|
1173
|
+
}
|
|
1174
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1175
|
+
if (byteLimit === false) {
|
|
1176
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1177
|
+
}
|
|
1178
|
+
const chunks = [];
|
|
1179
|
+
let totalBytes = 0;
|
|
1180
|
+
let position = 0;
|
|
1181
|
+
while (true) {
|
|
1182
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1183
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1184
|
+
if (bytesRead === 0) {
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
totalBytes += bytesRead;
|
|
1188
|
+
if (totalBytes > byteLimit) {
|
|
1189
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1190
|
+
}
|
|
1191
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1192
|
+
position += bytesRead;
|
|
1193
|
+
}
|
|
1194
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1198
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1199
|
+
var CacheStackSnapshotManager = class {
|
|
1200
|
+
constructor(options) {
|
|
1201
|
+
this.options = options;
|
|
1202
|
+
}
|
|
1203
|
+
options;
|
|
1204
|
+
async exportState(maxEntries) {
|
|
1205
|
+
const entries = [];
|
|
1206
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1207
|
+
entries.push(entry);
|
|
1208
|
+
});
|
|
1209
|
+
return entries;
|
|
1210
|
+
}
|
|
1211
|
+
async importState(entries) {
|
|
1212
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1213
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1214
|
+
value: entry.value,
|
|
1215
|
+
ttl: entry.ttl
|
|
1216
|
+
}));
|
|
1217
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1218
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1219
|
+
await Promise.all(
|
|
1220
|
+
batch.map(async (entry) => {
|
|
1221
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1222
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1228
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1229
|
+
const tempPath = import_node_path.default.join(
|
|
1230
|
+
import_node_path.default.dirname(targetPath),
|
|
1231
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1232
|
+
);
|
|
1233
|
+
let handle;
|
|
1234
|
+
try {
|
|
1235
|
+
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
1236
|
+
const openedHandle = handle;
|
|
1237
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1238
|
+
let wroteAny = false;
|
|
1239
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1240
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1241
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1242
|
+
wroteAny = true;
|
|
1243
|
+
});
|
|
1244
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1245
|
+
await openedHandle.close();
|
|
1246
|
+
handle = void 0;
|
|
1247
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
await handle?.close().catch(() => void 0);
|
|
1250
|
+
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1255
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1256
|
+
const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1257
|
+
let raw;
|
|
1258
|
+
try {
|
|
1259
|
+
if (maxBytes !== false) {
|
|
1260
|
+
const stat = await handle.stat();
|
|
1261
|
+
if (stat.size > maxBytes) {
|
|
1262
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1266
|
+
} finally {
|
|
1267
|
+
await handle.close();
|
|
1268
|
+
}
|
|
1269
|
+
let parsed;
|
|
1270
|
+
try {
|
|
1271
|
+
parsed = JSON.parse(raw);
|
|
1272
|
+
} catch (cause) {
|
|
1273
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1274
|
+
}
|
|
1275
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1276
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1277
|
+
}
|
|
1278
|
+
await this.importState(
|
|
1279
|
+
parsed.map((entry) => ({
|
|
1280
|
+
key: entry.key,
|
|
1281
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1282
|
+
ttl: entry.ttl
|
|
1283
|
+
}))
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1287
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const layer of this.options.layers) {
|
|
1289
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
const visitKey = async (key) => {
|
|
1293
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1294
|
+
if (exported.has(exportedKey)) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1298
|
+
if (stored === null) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
exported.add(exportedKey);
|
|
1302
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1303
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1304
|
+
}
|
|
1305
|
+
await visitor({
|
|
1306
|
+
key: exportedKey,
|
|
1307
|
+
value: stored,
|
|
1308
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1309
|
+
});
|
|
1310
|
+
};
|
|
1311
|
+
if (layer.forEachKey) {
|
|
1312
|
+
await layer.forEachKey(visitKey);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const keys = await layer.keys?.();
|
|
1316
|
+
for (const key of keys ?? []) {
|
|
1317
|
+
await visitKey(key);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
isCacheSnapshotEntries(value) {
|
|
1322
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1323
|
+
if (!entry || typeof entry !== "object") {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
const candidate = entry;
|
|
1327
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1328
|
+
});
|
|
954
1329
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
// src/internal/CacheStackRuntimePolicy.ts
|
|
959
|
-
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
960
|
-
return degradedUntil !== void 0 && degradedUntil > now;
|
|
961
|
-
}
|
|
962
|
-
function shouldStartBackgroundRefresh({
|
|
963
|
-
isDisconnecting,
|
|
964
|
-
hasRefreshInFlight
|
|
965
|
-
}) {
|
|
966
|
-
return !isDisconnecting && !hasRefreshInFlight;
|
|
967
|
-
}
|
|
968
|
-
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
969
|
-
if (!gracefulDegradation) {
|
|
970
|
-
return { degrade: false };
|
|
1330
|
+
sanitizeSnapshotValue(value) {
|
|
1331
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
971
1332
|
}
|
|
972
|
-
|
|
973
|
-
return {
|
|
974
|
-
degrade: true,
|
|
975
|
-
degradedUntil: now + retryAfterMs
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
function planFreshReadPolicies({
|
|
979
|
-
stored,
|
|
980
|
-
hasFetcher,
|
|
981
|
-
slidingTtl,
|
|
982
|
-
refreshAheadSeconds
|
|
983
|
-
}) {
|
|
984
|
-
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
985
|
-
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
986
|
-
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
987
|
-
return {
|
|
988
|
-
refreshedStored,
|
|
989
|
-
refreshedStoredTtl,
|
|
990
|
-
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
991
|
-
};
|
|
992
|
-
}
|
|
1333
|
+
};
|
|
993
1334
|
|
|
994
1335
|
// src/internal/CacheStackValidation.ts
|
|
995
1336
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -1218,7 +1559,11 @@ var FetchRateLimiter = class {
|
|
|
1218
1559
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1219
1560
|
nextFetcherBucketId = 0;
|
|
1220
1561
|
drainTimer;
|
|
1562
|
+
isDisposed = false;
|
|
1221
1563
|
async schedule(options, context, task) {
|
|
1564
|
+
if (this.isDisposed) {
|
|
1565
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1566
|
+
}
|
|
1222
1567
|
if (!options) {
|
|
1223
1568
|
return task();
|
|
1224
1569
|
}
|
|
@@ -1241,6 +1586,27 @@ var FetchRateLimiter = class {
|
|
|
1241
1586
|
this.drain();
|
|
1242
1587
|
});
|
|
1243
1588
|
}
|
|
1589
|
+
dispose() {
|
|
1590
|
+
this.isDisposed = true;
|
|
1591
|
+
if (this.drainTimer) {
|
|
1592
|
+
clearTimeout(this.drainTimer);
|
|
1593
|
+
this.drainTimer = void 0;
|
|
1594
|
+
}
|
|
1595
|
+
for (const bucket of this.buckets.values()) {
|
|
1596
|
+
if (bucket.cleanupTimer) {
|
|
1597
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1598
|
+
bucket.cleanupTimer = void 0;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1602
|
+
for (const item of queue) {
|
|
1603
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
this.queuesByBucket.clear();
|
|
1607
|
+
this.pendingBuckets.clear();
|
|
1608
|
+
this.buckets.clear();
|
|
1609
|
+
}
|
|
1244
1610
|
normalize(options) {
|
|
1245
1611
|
const maxConcurrent = options.maxConcurrent;
|
|
1246
1612
|
const intervalMs = options.intervalMs;
|
|
@@ -1276,6 +1642,9 @@ var FetchRateLimiter = class {
|
|
|
1276
1642
|
return "global";
|
|
1277
1643
|
}
|
|
1278
1644
|
drain() {
|
|
1645
|
+
if (this.isDisposed) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1279
1648
|
if (this.drainTimer) {
|
|
1280
1649
|
clearTimeout(this.drainTimer);
|
|
1281
1650
|
this.drainTimer = void 0;
|
|
@@ -1372,6 +1741,9 @@ var FetchRateLimiter = class {
|
|
|
1372
1741
|
}
|
|
1373
1742
|
}
|
|
1374
1743
|
bucketState(bucketKey) {
|
|
1744
|
+
if (this.isDisposed) {
|
|
1745
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1746
|
+
}
|
|
1375
1747
|
const existing = this.buckets.get(bucketKey);
|
|
1376
1748
|
if (existing) {
|
|
1377
1749
|
return existing;
|
|
@@ -1830,19 +2202,19 @@ var TagIndex = class {
|
|
|
1830
2202
|
if (!this.knownKeys.delete(key)) {
|
|
1831
2203
|
return;
|
|
1832
2204
|
}
|
|
1833
|
-
const
|
|
2205
|
+
const path2 = [];
|
|
1834
2206
|
let node = this.root;
|
|
1835
2207
|
for (const character of key) {
|
|
1836
2208
|
const child = node.children.get(character);
|
|
1837
2209
|
if (!child) {
|
|
1838
2210
|
return;
|
|
1839
2211
|
}
|
|
1840
|
-
|
|
2212
|
+
path2.push([node, character]);
|
|
1841
2213
|
node = child;
|
|
1842
2214
|
}
|
|
1843
2215
|
node.terminal = false;
|
|
1844
|
-
for (let index =
|
|
1845
|
-
const entry =
|
|
2216
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2217
|
+
const entry = path2[index];
|
|
1846
2218
|
if (!entry) {
|
|
1847
2219
|
continue;
|
|
1848
2220
|
}
|
|
@@ -1856,39 +2228,31 @@ var TagIndex = class {
|
|
|
1856
2228
|
}
|
|
1857
2229
|
};
|
|
1858
2230
|
|
|
1859
|
-
// src/
|
|
1860
|
-
var
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1866
|
-
deserialize(payload) {
|
|
1867
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1868
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1869
|
-
}
|
|
1870
|
-
};
|
|
1871
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1872
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2231
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
2232
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2233
|
+
function sanitizeStructuredData(value, options) {
|
|
2234
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2235
|
+
}
|
|
2236
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1873
2237
|
state.count += 1;
|
|
1874
|
-
if (state.count >
|
|
1875
|
-
throw new Error(
|
|
2238
|
+
if (state.count > options.maxNodes) {
|
|
2239
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1876
2240
|
}
|
|
1877
|
-
if (depth >
|
|
1878
|
-
throw new Error(
|
|
2241
|
+
if (depth > options.maxDepth) {
|
|
2242
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1879
2243
|
}
|
|
1880
2244
|
if (Array.isArray(value)) {
|
|
1881
|
-
return value.map((entry) =>
|
|
2245
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1882
2246
|
}
|
|
1883
2247
|
if (!isPlainObject(value)) {
|
|
1884
2248
|
return value;
|
|
1885
2249
|
}
|
|
1886
|
-
const sanitized = {};
|
|
2250
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1887
2251
|
for (const [key, entry] of Object.entries(value)) {
|
|
1888
|
-
if (
|
|
2252
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1889
2253
|
continue;
|
|
1890
2254
|
}
|
|
1891
|
-
sanitized[key] =
|
|
2255
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1892
2256
|
}
|
|
1893
2257
|
return sanitized;
|
|
1894
2258
|
}
|
|
@@ -1896,6 +2260,21 @@ function isPlainObject(value) {
|
|
|
1896
2260
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1897
2261
|
}
|
|
1898
2262
|
|
|
2263
|
+
// src/serialization/JsonSerializer.ts
|
|
2264
|
+
var JsonSerializer = class {
|
|
2265
|
+
serialize(value) {
|
|
2266
|
+
return JSON.stringify(value);
|
|
2267
|
+
}
|
|
2268
|
+
deserialize(payload) {
|
|
2269
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2270
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2271
|
+
label: "JSON payload",
|
|
2272
|
+
maxDepth: 200,
|
|
2273
|
+
maxNodes: 1e4
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
|
|
1899
2278
|
// src/stampede/StampedeGuard.ts
|
|
1900
2279
|
var import_async_mutex2 = require("async-mutex");
|
|
1901
2280
|
var StampedeGuard = class {
|
|
@@ -1940,7 +2319,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1940
2319
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1941
2320
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1942
2321
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1943
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1944
2322
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1945
2323
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1946
2324
|
var DebugLogger = class {
|
|
@@ -1997,6 +2375,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1997
2375
|
await this.handleLayerFailure(layer, operation, error);
|
|
1998
2376
|
}
|
|
1999
2377
|
});
|
|
2378
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2379
|
+
tagIndex: this.tagIndex,
|
|
2380
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2381
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2382
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2386
|
+
layers: this.layers,
|
|
2387
|
+
maintenance: this.maintenance,
|
|
2388
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2389
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2390
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2391
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2392
|
+
},
|
|
2393
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2394
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2395
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2396
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2397
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2398
|
+
writePolicy: this.options.writePolicy,
|
|
2399
|
+
onWriteFailures: (context, failures) => {
|
|
2400
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2401
|
+
this.logger.debug?.("write-failure", {
|
|
2402
|
+
...context,
|
|
2403
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2000
2407
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
2001
2408
|
this.logger.warn?.(
|
|
2002
2409
|
"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."
|
|
@@ -2012,6 +2419,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2012
2419
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
2013
2420
|
);
|
|
2014
2421
|
}
|
|
2422
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2423
|
+
layers: this.layers,
|
|
2424
|
+
tagIndex: this.tagIndex,
|
|
2425
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2426
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2427
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2428
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2429
|
+
validateCacheKey,
|
|
2430
|
+
formatError: this.formatError.bind(this)
|
|
2431
|
+
});
|
|
2015
2432
|
this.initializeWriteBehind(options.writeBehind);
|
|
2016
2433
|
this.startup = this.initialize();
|
|
2017
2434
|
}
|
|
@@ -2027,11 +2444,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2027
2444
|
keyDiscovery;
|
|
2028
2445
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2029
2446
|
snapshotSerializer = new JsonSerializer();
|
|
2447
|
+
invalidation;
|
|
2448
|
+
layerWriter;
|
|
2449
|
+
snapshots;
|
|
2030
2450
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2031
2451
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2032
2452
|
maintenance = new CacheStackMaintenance();
|
|
2033
2453
|
ttlResolver;
|
|
2034
2454
|
circuitBreakerManager;
|
|
2455
|
+
nextOperationId = 0;
|
|
2035
2456
|
currentGeneration;
|
|
2036
2457
|
isDisconnecting = false;
|
|
2037
2458
|
disconnectPromise;
|
|
@@ -2042,10 +2463,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2042
2463
|
* and no `fetcher` is provided.
|
|
2043
2464
|
*/
|
|
2044
2465
|
async get(key, fetcher, options) {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2466
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2467
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2468
|
+
this.validateWriteOptions(options);
|
|
2469
|
+
await this.awaitStartup("get");
|
|
2470
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2471
|
+
});
|
|
2049
2472
|
}
|
|
2050
2473
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2051
2474
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2167,23 +2590,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2167
2590
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2168
2591
|
*/
|
|
2169
2592
|
async set(key, value, options) {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2593
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2594
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2595
|
+
this.validateWriteOptions(options);
|
|
2596
|
+
await this.awaitStartup("set");
|
|
2597
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2598
|
+
});
|
|
2174
2599
|
}
|
|
2175
2600
|
/**
|
|
2176
2601
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2177
2602
|
*/
|
|
2178
2603
|
async delete(key) {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2604
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2605
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2606
|
+
await this.awaitStartup("delete");
|
|
2607
|
+
await this.deleteKeys([normalizedKey]);
|
|
2608
|
+
await this.publishInvalidation({
|
|
2609
|
+
scope: "key",
|
|
2610
|
+
keys: [normalizedKey],
|
|
2611
|
+
sourceId: this.instanceId,
|
|
2612
|
+
operation: "delete"
|
|
2613
|
+
});
|
|
2187
2614
|
});
|
|
2188
2615
|
}
|
|
2189
2616
|
async clear() {
|
|
@@ -2216,95 +2643,99 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2216
2643
|
});
|
|
2217
2644
|
}
|
|
2218
2645
|
async mget(entries) {
|
|
2219
|
-
this.
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2646
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2647
|
+
this.assertActive("mget");
|
|
2648
|
+
if (entries.length === 0) {
|
|
2649
|
+
return [];
|
|
2650
|
+
}
|
|
2651
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2652
|
+
...entry,
|
|
2653
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2654
|
+
}));
|
|
2655
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2656
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2657
|
+
if (!canFastPath) {
|
|
2658
|
+
await this.awaitStartup("mget");
|
|
2659
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2660
|
+
return Promise.all(
|
|
2661
|
+
normalizedEntries.map((entry) => {
|
|
2662
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2663
|
+
const existing = pendingReads.get(entry.key);
|
|
2664
|
+
if (!existing) {
|
|
2665
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2666
|
+
pendingReads.set(entry.key, {
|
|
2667
|
+
promise,
|
|
2668
|
+
fetch: entry.fetch,
|
|
2669
|
+
optionsSignature
|
|
2670
|
+
});
|
|
2671
|
+
return promise;
|
|
2672
|
+
}
|
|
2673
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2674
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2675
|
+
}
|
|
2676
|
+
return existing.promise;
|
|
2677
|
+
})
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2230
2680
|
await this.awaitStartup("mget");
|
|
2231
|
-
const
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
});
|
|
2243
|
-
return promise;
|
|
2244
|
-
}
|
|
2245
|
-
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2246
|
-
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2247
|
-
}
|
|
2248
|
-
return existing.promise;
|
|
2249
|
-
})
|
|
2250
|
-
);
|
|
2251
|
-
}
|
|
2252
|
-
await this.awaitStartup("mget");
|
|
2253
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2254
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2255
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2256
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2257
|
-
const entry = normalizedEntries[index];
|
|
2258
|
-
if (!entry) continue;
|
|
2259
|
-
const key = entry.key;
|
|
2260
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2261
|
-
indexes.push(index);
|
|
2262
|
-
indexesByKey.set(key, indexes);
|
|
2263
|
-
pending.add(key);
|
|
2264
|
-
}
|
|
2265
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2266
|
-
const layer = this.layers[layerIndex];
|
|
2267
|
-
if (!layer) continue;
|
|
2268
|
-
const keys = [...pending];
|
|
2269
|
-
if (keys.length === 0) {
|
|
2270
|
-
break;
|
|
2681
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2682
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2683
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2684
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2685
|
+
const entry = normalizedEntries[index];
|
|
2686
|
+
if (!entry) continue;
|
|
2687
|
+
const key = entry.key;
|
|
2688
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2689
|
+
indexes.push(index);
|
|
2690
|
+
indexesByKey.set(key, indexes);
|
|
2691
|
+
pending.add(key);
|
|
2271
2692
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const
|
|
2276
|
-
if (
|
|
2277
|
-
|
|
2693
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2694
|
+
const layer = this.layers[layerIndex];
|
|
2695
|
+
if (!layer) continue;
|
|
2696
|
+
const keys = [...pending];
|
|
2697
|
+
if (keys.length === 0) {
|
|
2698
|
+
break;
|
|
2278
2699
|
}
|
|
2279
|
-
const
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2700
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2701
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2702
|
+
const key = keys[offset];
|
|
2703
|
+
const stored = values[offset];
|
|
2704
|
+
if (!key || stored === null) {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
const resolved = resolveStoredValue(stored);
|
|
2708
|
+
if (resolved.state === "expired") {
|
|
2709
|
+
await layer.delete(key);
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
await this.tagIndex.touch(key);
|
|
2713
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2714
|
+
resultsByKey.set(key, resolved.value);
|
|
2715
|
+
pending.delete(key);
|
|
2716
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2283
2717
|
}
|
|
2284
|
-
await this.tagIndex.touch(key);
|
|
2285
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2286
|
-
resultsByKey.set(key, resolved.value);
|
|
2287
|
-
pending.delete(key);
|
|
2288
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2289
2718
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2719
|
+
if (pending.size > 0) {
|
|
2720
|
+
for (const key of pending) {
|
|
2721
|
+
await this.tagIndex.remove(key);
|
|
2722
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2723
|
+
}
|
|
2295
2724
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2725
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2726
|
+
});
|
|
2298
2727
|
}
|
|
2299
2728
|
async mset(entries) {
|
|
2300
|
-
this.
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2729
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2730
|
+
this.assertActive("mset");
|
|
2731
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2732
|
+
...entry,
|
|
2733
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2734
|
+
}));
|
|
2735
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2736
|
+
await this.awaitStartup("mset");
|
|
2737
|
+
await this.writeBatch(normalizedEntries);
|
|
2738
|
+
});
|
|
2308
2739
|
}
|
|
2309
2740
|
async warm(entries, options = {}) {
|
|
2310
2741
|
this.assertActive("warm");
|
|
@@ -2357,40 +2788,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2357
2788
|
return new CacheNamespace(this, prefix);
|
|
2358
2789
|
}
|
|
2359
2790
|
async invalidateByTag(tag) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2791
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2792
|
+
validateTag(tag);
|
|
2793
|
+
await this.awaitStartup("invalidateByTag");
|
|
2794
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2795
|
+
await this.deleteKeys(keys);
|
|
2796
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2797
|
+
});
|
|
2365
2798
|
}
|
|
2366
2799
|
async invalidateByTags(tags, mode = "any") {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2800
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2801
|
+
if (tags.length === 0) {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
validateTags(tags);
|
|
2805
|
+
await this.awaitStartup("invalidateByTags");
|
|
2806
|
+
const keysByTag = await Promise.all(
|
|
2807
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2808
|
+
);
|
|
2809
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2810
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2811
|
+
await this.deleteKeys(keys);
|
|
2812
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2813
|
+
});
|
|
2377
2814
|
}
|
|
2378
2815
|
async invalidateByPattern(pattern) {
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
this.
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2816
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2817
|
+
validatePattern(pattern);
|
|
2818
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2819
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2820
|
+
this.qualifyPattern(pattern),
|
|
2821
|
+
this.invalidationMaxKeys()
|
|
2822
|
+
);
|
|
2823
|
+
await this.deleteKeys(keys);
|
|
2824
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2825
|
+
});
|
|
2387
2826
|
}
|
|
2388
2827
|
async invalidateByPrefix(prefix) {
|
|
2389
|
-
await this.
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2828
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2829
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2830
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2831
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2832
|
+
await this.deleteKeys(keys);
|
|
2833
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2834
|
+
});
|
|
2394
2835
|
}
|
|
2395
2836
|
getMetrics() {
|
|
2396
2837
|
return this.metricsCollector.snapshot;
|
|
@@ -2492,104 +2933,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2492
2933
|
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
2493
2934
|
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
2494
2935
|
}
|
|
2495
|
-
}
|
|
2496
|
-
if (foundInLayers.length === 0) {
|
|
2497
|
-
return null;
|
|
2498
|
-
}
|
|
2499
|
-
const tags = await this.getTagsForKey(normalizedKey);
|
|
2500
|
-
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
2501
|
-
}
|
|
2502
|
-
async exportState() {
|
|
2503
|
-
await this.awaitStartup("exportState");
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
}
|
|
2510
|
-
async
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2518
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2519
|
-
await Promise.all(
|
|
2520
|
-
batch.map(async (entry) => {
|
|
2521
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2522
|
-
await this.tagIndex.touch(entry.key);
|
|
2523
|
-
})
|
|
2524
|
-
);
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
async persistToFile(filePath) {
|
|
2528
|
-
this.assertActive("persistToFile");
|
|
2529
|
-
const { promises: fs2 } = await import("fs");
|
|
2530
|
-
const path = await import("path");
|
|
2531
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2532
|
-
const tempPath = path.join(
|
|
2533
|
-
path.dirname(targetPath),
|
|
2534
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2535
|
-
);
|
|
2536
|
-
let handle;
|
|
2537
|
-
try {
|
|
2538
|
-
handle = await fs2.open(tempPath, "wx");
|
|
2539
|
-
const openedHandle = handle;
|
|
2540
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2541
|
-
let wroteAny = false;
|
|
2542
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2543
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2544
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2545
|
-
wroteAny = true;
|
|
2546
|
-
});
|
|
2547
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2548
|
-
await openedHandle.close();
|
|
2549
|
-
handle = void 0;
|
|
2550
|
-
await fs2.rename(tempPath, targetPath);
|
|
2551
|
-
} catch (error) {
|
|
2552
|
-
await handle?.close().catch(() => void 0);
|
|
2553
|
-
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2554
|
-
throw error;
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
async restoreFromFile(filePath) {
|
|
2558
|
-
this.assertActive("restoreFromFile");
|
|
2559
|
-
const { promises: fs2, constants } = await import("fs");
|
|
2560
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2561
|
-
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2562
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2563
|
-
let raw;
|
|
2564
|
-
try {
|
|
2565
|
-
if (snapshotMaxBytes !== false) {
|
|
2566
|
-
const stat = await handle.stat();
|
|
2567
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2568
|
-
throw new Error(
|
|
2569
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2570
|
-
);
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2574
|
-
} finally {
|
|
2575
|
-
await handle.close();
|
|
2576
|
-
}
|
|
2577
|
-
let parsed;
|
|
2578
|
-
try {
|
|
2579
|
-
parsed = JSON.parse(raw);
|
|
2580
|
-
} catch (cause) {
|
|
2581
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2582
|
-
}
|
|
2583
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2584
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2585
|
-
}
|
|
2586
|
-
await this.importState(
|
|
2587
|
-
parsed.map((entry) => ({
|
|
2588
|
-
key: entry.key,
|
|
2589
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2590
|
-
ttl: entry.ttl
|
|
2591
|
-
}))
|
|
2592
|
-
);
|
|
2936
|
+
}
|
|
2937
|
+
if (foundInLayers.length === 0) {
|
|
2938
|
+
return null;
|
|
2939
|
+
}
|
|
2940
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
2941
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
2942
|
+
}
|
|
2943
|
+
async exportState() {
|
|
2944
|
+
await this.awaitStartup("exportState");
|
|
2945
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2946
|
+
}
|
|
2947
|
+
async importState(entries) {
|
|
2948
|
+
await this.awaitStartup("importState");
|
|
2949
|
+
await this.snapshots.importState(entries);
|
|
2950
|
+
}
|
|
2951
|
+
async persistToFile(filePath) {
|
|
2952
|
+
this.assertActive("persistToFile");
|
|
2953
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2954
|
+
}
|
|
2955
|
+
async restoreFromFile(filePath) {
|
|
2956
|
+
this.assertActive("restoreFromFile");
|
|
2957
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2593
2958
|
}
|
|
2594
2959
|
async disconnect() {
|
|
2595
2960
|
if (!this.disconnectPromise) {
|
|
@@ -2601,6 +2966,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2601
2966
|
await this.maintenance.waitForGenerationCleanup();
|
|
2602
2967
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2603
2968
|
this.maintenance.disposeWriteBehindTimer();
|
|
2969
|
+
this.fetchRateLimiter.dispose();
|
|
2604
2970
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2605
2971
|
})();
|
|
2606
2972
|
}
|
|
@@ -2714,7 +3080,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2714
3080
|
async storeEntry(key, kind, value, options) {
|
|
2715
3081
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2716
3082
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2717
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
3083
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2718
3084
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2719
3085
|
return;
|
|
2720
3086
|
}
|
|
@@ -2731,52 +3097,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2731
3097
|
}
|
|
2732
3098
|
}
|
|
2733
3099
|
async writeBatch(entries) {
|
|
2734
|
-
const
|
|
2735
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2736
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2737
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2738
|
-
const immediateOperations = [];
|
|
2739
|
-
const deferredOperations = [];
|
|
2740
|
-
for (const entry of entries) {
|
|
2741
|
-
for (const layer of this.layers) {
|
|
2742
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2743
|
-
continue;
|
|
2744
|
-
}
|
|
2745
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2746
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2747
|
-
bucket.push(layerEntry);
|
|
2748
|
-
entriesByLayer.set(layer, bucket);
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2752
|
-
const operation = async () => {
|
|
2753
|
-
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2754
|
-
return;
|
|
2755
|
-
}
|
|
2756
|
-
const activeEntries = layerEntries.filter(
|
|
2757
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2758
|
-
);
|
|
2759
|
-
if (activeEntries.length === 0) {
|
|
2760
|
-
return;
|
|
2761
|
-
}
|
|
2762
|
-
try {
|
|
2763
|
-
if (layer.setMany) {
|
|
2764
|
-
await layer.setMany(activeEntries);
|
|
2765
|
-
return;
|
|
2766
|
-
}
|
|
2767
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2768
|
-
} catch (error) {
|
|
2769
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2770
|
-
}
|
|
2771
|
-
};
|
|
2772
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2773
|
-
deferredOperations.push(operation);
|
|
2774
|
-
} else {
|
|
2775
|
-
immediateOperations.push(operation);
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2779
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
3100
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2780
3101
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2781
3102
|
return;
|
|
2782
3103
|
}
|
|
@@ -2883,58 +3204,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2883
3204
|
this.emit("backfill", { key, layer: layer.name });
|
|
2884
3205
|
}
|
|
2885
3206
|
}
|
|
2886
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2887
|
-
const now = Date.now();
|
|
2888
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2889
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2890
|
-
const immediateOperations = [];
|
|
2891
|
-
const deferredOperations = [];
|
|
2892
|
-
for (const layer of this.layers) {
|
|
2893
|
-
const operation = async () => {
|
|
2894
|
-
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2901
|
-
try {
|
|
2902
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2903
|
-
} catch (error) {
|
|
2904
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2905
|
-
}
|
|
2906
|
-
};
|
|
2907
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2908
|
-
deferredOperations.push(operation);
|
|
2909
|
-
} else {
|
|
2910
|
-
immediateOperations.push(operation);
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2914
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2915
|
-
}
|
|
2916
|
-
async executeLayerOperations(operations, context) {
|
|
2917
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2918
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
2922
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2923
|
-
if (failures.length === 0) {
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2927
|
-
this.logger.debug?.("write-failure", {
|
|
2928
|
-
...context,
|
|
2929
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
2930
|
-
});
|
|
2931
|
-
if (failures.length === operations.length) {
|
|
2932
|
-
throw new AggregateError(
|
|
2933
|
-
failures.map((failure) => failure.reason),
|
|
2934
|
-
`${context.action} failed for every cache layer`
|
|
2935
|
-
);
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
3207
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2939
3208
|
return this.ttlResolver.resolveFreshTtl(
|
|
2940
3209
|
key,
|
|
@@ -3000,7 +3269,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3000
3269
|
return;
|
|
3001
3270
|
}
|
|
3002
3271
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3003
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3272
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
3004
3273
|
for (const key of keys) {
|
|
3005
3274
|
await this.tagIndex.remove(key);
|
|
3006
3275
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -3032,7 +3301,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3032
3301
|
}
|
|
3033
3302
|
const keys = message.keys ?? [];
|
|
3034
3303
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3035
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3304
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3036
3305
|
if (message.operation !== "write") {
|
|
3037
3306
|
for (const key of keys) {
|
|
3038
3307
|
await this.tagIndex.remove(key);
|
|
@@ -3089,6 +3358,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3089
3358
|
shouldBroadcastL1Invalidation() {
|
|
3090
3359
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3091
3360
|
}
|
|
3361
|
+
async observeOperation(name, attributes, execute) {
|
|
3362
|
+
const id = this.nextOperationId;
|
|
3363
|
+
this.nextOperationId += 1;
|
|
3364
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3365
|
+
try {
|
|
3366
|
+
const result = await execute();
|
|
3367
|
+
this.emit("operation-end", {
|
|
3368
|
+
id,
|
|
3369
|
+
name,
|
|
3370
|
+
attributes,
|
|
3371
|
+
success: true,
|
|
3372
|
+
result: result === null ? "null" : void 0
|
|
3373
|
+
});
|
|
3374
|
+
return result;
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
this.emit("operation-end", {
|
|
3377
|
+
id,
|
|
3378
|
+
name,
|
|
3379
|
+
attributes,
|
|
3380
|
+
success: false,
|
|
3381
|
+
error
|
|
3382
|
+
});
|
|
3383
|
+
throw error;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3092
3386
|
scheduleGenerationCleanup(generation) {
|
|
3093
3387
|
this.maintenance.scheduleGenerationCleanup(
|
|
3094
3388
|
generation,
|
|
@@ -3144,37 +3438,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3144
3438
|
});
|
|
3145
3439
|
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3146
3440
|
}
|
|
3147
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3148
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3149
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3150
|
-
layer.name,
|
|
3151
|
-
options?.staleWhileRevalidate,
|
|
3152
|
-
this.options.staleWhileRevalidate
|
|
3153
|
-
);
|
|
3154
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3155
|
-
const payload = createStoredValueEnvelope({
|
|
3156
|
-
kind,
|
|
3157
|
-
value,
|
|
3158
|
-
freshTtlSeconds: freshTtl,
|
|
3159
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3160
|
-
staleIfErrorSeconds: staleIfError,
|
|
3161
|
-
now
|
|
3162
|
-
});
|
|
3163
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
3164
|
-
return {
|
|
3165
|
-
key,
|
|
3166
|
-
value: payload,
|
|
3167
|
-
ttl
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
|
-
intersectKeys(groups) {
|
|
3171
|
-
if (groups.length === 0) {
|
|
3172
|
-
return [];
|
|
3173
|
-
}
|
|
3174
|
-
const [firstGroup, ...rest] = groups;
|
|
3175
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3176
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3177
|
-
}
|
|
3178
3441
|
qualifyKey(key) {
|
|
3179
3442
|
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3180
3443
|
}
|
|
@@ -3184,32 +3447,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3184
3447
|
stripQualifiedKey(key) {
|
|
3185
3448
|
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3186
3449
|
}
|
|
3187
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3188
|
-
await Promise.all(
|
|
3189
|
-
layers.map(async (layer) => {
|
|
3190
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3191
|
-
return;
|
|
3192
|
-
}
|
|
3193
|
-
if (layer.deleteMany) {
|
|
3194
|
-
try {
|
|
3195
|
-
await layer.deleteMany(keys);
|
|
3196
|
-
} catch (error) {
|
|
3197
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3198
|
-
}
|
|
3199
|
-
return;
|
|
3200
|
-
}
|
|
3201
|
-
await Promise.all(
|
|
3202
|
-
keys.map(async (key) => {
|
|
3203
|
-
try {
|
|
3204
|
-
await layer.delete(key);
|
|
3205
|
-
} catch (error) {
|
|
3206
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3207
|
-
}
|
|
3208
|
-
})
|
|
3209
|
-
);
|
|
3210
|
-
})
|
|
3211
|
-
);
|
|
3212
|
-
}
|
|
3213
3450
|
validateConfiguration() {
|
|
3214
3451
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
3215
3452
|
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
@@ -3340,18 +3577,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3340
3577
|
this.emit("error", { operation, ...context });
|
|
3341
3578
|
}
|
|
3342
3579
|
}
|
|
3343
|
-
isCacheSnapshotEntries(value) {
|
|
3344
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3345
|
-
if (!entry || typeof entry !== "object") {
|
|
3346
|
-
return false;
|
|
3347
|
-
}
|
|
3348
|
-
const candidate = entry;
|
|
3349
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3350
|
-
});
|
|
3351
|
-
}
|
|
3352
|
-
sanitizeSnapshotValue(value) {
|
|
3353
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3354
|
-
}
|
|
3355
3580
|
snapshotMaxBytes() {
|
|
3356
3581
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3357
3582
|
}
|
|
@@ -3361,62 +3586,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3361
3586
|
invalidationMaxKeys() {
|
|
3362
3587
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3363
3588
|
}
|
|
3364
|
-
async collectKeysForTag(tag) {
|
|
3365
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3366
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3367
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3368
|
-
keys.add(key);
|
|
3369
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3370
|
-
});
|
|
3371
|
-
return [...keys];
|
|
3372
|
-
}
|
|
3373
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3374
|
-
keys.add(key);
|
|
3375
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3376
|
-
}
|
|
3377
|
-
return [...keys];
|
|
3378
|
-
}
|
|
3379
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3380
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3381
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3382
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3383
|
-
}
|
|
3384
|
-
}
|
|
3385
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3386
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3387
|
-
for (const layer of this.layers) {
|
|
3388
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3389
|
-
continue;
|
|
3390
|
-
}
|
|
3391
|
-
const visitKey = async (key) => {
|
|
3392
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3393
|
-
if (exported.has(exportedKey)) {
|
|
3394
|
-
return;
|
|
3395
|
-
}
|
|
3396
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3397
|
-
if (stored === null) {
|
|
3398
|
-
return;
|
|
3399
|
-
}
|
|
3400
|
-
exported.add(exportedKey);
|
|
3401
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3402
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3403
|
-
}
|
|
3404
|
-
await visitor({
|
|
3405
|
-
key: exportedKey,
|
|
3406
|
-
value: stored,
|
|
3407
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3408
|
-
});
|
|
3409
|
-
};
|
|
3410
|
-
if (layer.forEachKey) {
|
|
3411
|
-
await layer.forEachKey(visitKey);
|
|
3412
|
-
continue;
|
|
3413
|
-
}
|
|
3414
|
-
const keys = await layer.keys?.();
|
|
3415
|
-
for (const key of keys ?? []) {
|
|
3416
|
-
await visitKey(key);
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
3589
|
};
|
|
3421
3590
|
|
|
3422
3591
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -3458,7 +3627,12 @@ var RedisInvalidationBus = class {
|
|
|
3458
3627
|
async dispatchToHandlers(payload) {
|
|
3459
3628
|
let message;
|
|
3460
3629
|
try {
|
|
3461
|
-
const parsed =
|
|
3630
|
+
const parsed = sanitizeStructuredData(JSON.parse(payload), {
|
|
3631
|
+
label: "Invalidation payload",
|
|
3632
|
+
maxDepth: 64,
|
|
3633
|
+
maxNodes: 1e4,
|
|
3634
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3635
|
+
});
|
|
3462
3636
|
if (!this.isInvalidationMessage(parsed)) {
|
|
3463
3637
|
throw new Error("Invalid invalidation payload shape.");
|
|
3464
3638
|
}
|
|
@@ -3495,31 +3669,6 @@ var RedisInvalidationBus = class {
|
|
|
3495
3669
|
console.error(`[layercache] ${message}`, error);
|
|
3496
3670
|
}
|
|
3497
3671
|
};
|
|
3498
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3499
|
-
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3500
|
-
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3501
|
-
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3502
|
-
state.count += 1;
|
|
3503
|
-
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3504
|
-
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3505
|
-
}
|
|
3506
|
-
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3507
|
-
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3508
|
-
}
|
|
3509
|
-
if (Array.isArray(value)) {
|
|
3510
|
-
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
3511
|
-
}
|
|
3512
|
-
if (value && typeof value === "object") {
|
|
3513
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
3514
|
-
for (const key of Object.keys(value)) {
|
|
3515
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
3516
|
-
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
return result;
|
|
3520
|
-
}
|
|
3521
|
-
return value;
|
|
3522
|
-
}
|
|
3523
3672
|
|
|
3524
3673
|
// src/invalidation/RedisTagIndex.ts
|
|
3525
3674
|
var RedisTagIndex = class {
|
|
@@ -3889,64 +4038,37 @@ function normalizeUrl2(url) {
|
|
|
3889
4038
|
|
|
3890
4039
|
// src/integrations/opentelemetry.ts
|
|
3891
4040
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3892
|
-
const
|
|
3893
|
-
|
|
3894
|
-
set:
|
|
3895
|
-
delete: cache.delete.bind(cache),
|
|
3896
|
-
mget: cache.mget.bind(cache),
|
|
3897
|
-
mset: cache.mset.bind(cache),
|
|
3898
|
-
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
3899
|
-
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
3900
|
-
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
3901
|
-
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
4041
|
+
const spans = /* @__PURE__ */ new Map();
|
|
4042
|
+
const onStart = (event) => {
|
|
4043
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
3902
4044
|
};
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
3913
|
-
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
3914
|
-
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
3915
|
-
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
3916
|
-
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
3917
|
-
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
3918
|
-
return {
|
|
3919
|
-
uninstall() {
|
|
3920
|
-
cache.get = originals.get;
|
|
3921
|
-
cache.set = originals.set;
|
|
3922
|
-
cache.delete = originals.delete;
|
|
3923
|
-
cache.mget = originals.mget;
|
|
3924
|
-
cache.mset = originals.mset;
|
|
3925
|
-
cache.invalidateByTag = originals.invalidateByTag;
|
|
3926
|
-
cache.invalidateByTags = originals.invalidateByTags;
|
|
3927
|
-
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
3928
|
-
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
4045
|
+
const onEnd = (event) => {
|
|
4046
|
+
const span = spans.get(event.id);
|
|
4047
|
+
if (!span) {
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
spans.delete(event.id);
|
|
4051
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
4052
|
+
if (event.result) {
|
|
4053
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3929
4054
|
}
|
|
4055
|
+
if (event.error !== void 0) {
|
|
4056
|
+
span.recordException?.(event.error);
|
|
4057
|
+
}
|
|
4058
|
+
span.end();
|
|
3930
4059
|
};
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
return
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
span.
|
|
3938
|
-
|
|
3939
|
-
span.setAttribute?.("layercache.result", "null");
|
|
4060
|
+
cache.on("operation-start", onStart);
|
|
4061
|
+
cache.on("operation-end", onEnd);
|
|
4062
|
+
return {
|
|
4063
|
+
uninstall() {
|
|
4064
|
+
cache.off("operation-start", onStart);
|
|
4065
|
+
cache.off("operation-end", onEnd);
|
|
4066
|
+
for (const span of spans.values()) {
|
|
4067
|
+
span.end();
|
|
3940
4068
|
}
|
|
3941
|
-
|
|
3942
|
-
} catch (error) {
|
|
3943
|
-
span.setAttribute?.("layercache.success", false);
|
|
3944
|
-
span.recordException?.(error);
|
|
3945
|
-
throw error;
|
|
3946
|
-
} finally {
|
|
3947
|
-
span.end();
|
|
4069
|
+
spans.clear();
|
|
3948
4070
|
}
|
|
3949
|
-
}
|
|
4071
|
+
};
|
|
3950
4072
|
}
|
|
3951
4073
|
|
|
3952
4074
|
// src/integrations/trpc.ts
|
|
@@ -4498,8 +4620,8 @@ var RedisLayer = class {
|
|
|
4498
4620
|
|
|
4499
4621
|
// src/layers/DiskLayer.ts
|
|
4500
4622
|
var import_node_crypto = require("crypto");
|
|
4501
|
-
var
|
|
4502
|
-
var
|
|
4623
|
+
var import_node_fs2 = require("fs");
|
|
4624
|
+
var import_node_path2 = require("path");
|
|
4503
4625
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
4504
4626
|
var DiskLayer = class {
|
|
4505
4627
|
name;
|
|
@@ -4542,7 +4664,7 @@ var DiskLayer = class {
|
|
|
4542
4664
|
}
|
|
4543
4665
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4544
4666
|
await this.enqueueWrite(async () => {
|
|
4545
|
-
await
|
|
4667
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4546
4668
|
const entry = {
|
|
4547
4669
|
key,
|
|
4548
4670
|
value,
|
|
@@ -4552,8 +4674,8 @@ var DiskLayer = class {
|
|
|
4552
4674
|
const targetPath = this.keyToPath(key);
|
|
4553
4675
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4554
4676
|
try {
|
|
4555
|
-
await
|
|
4556
|
-
await
|
|
4677
|
+
await import_node_fs2.promises.writeFile(tempPath, payload);
|
|
4678
|
+
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
4557
4679
|
} catch (error) {
|
|
4558
4680
|
await this.safeDelete(tempPath);
|
|
4559
4681
|
throw error;
|
|
@@ -4607,12 +4729,12 @@ var DiskLayer = class {
|
|
|
4607
4729
|
await this.enqueueWrite(async () => {
|
|
4608
4730
|
let entries;
|
|
4609
4731
|
try {
|
|
4610
|
-
entries = await
|
|
4732
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4611
4733
|
} catch {
|
|
4612
4734
|
return;
|
|
4613
4735
|
}
|
|
4614
4736
|
await this.deletePathsWithConcurrency(
|
|
4615
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) => (0,
|
|
4737
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
|
|
4616
4738
|
);
|
|
4617
4739
|
});
|
|
4618
4740
|
}
|
|
@@ -4641,7 +4763,7 @@ var DiskLayer = class {
|
|
|
4641
4763
|
}
|
|
4642
4764
|
async ping() {
|
|
4643
4765
|
try {
|
|
4644
|
-
await
|
|
4766
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4645
4767
|
return true;
|
|
4646
4768
|
} catch {
|
|
4647
4769
|
return false;
|
|
@@ -4651,7 +4773,7 @@ var DiskLayer = class {
|
|
|
4651
4773
|
}
|
|
4652
4774
|
keyToPath(key) {
|
|
4653
4775
|
const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
|
|
4654
|
-
return (0,
|
|
4776
|
+
return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
|
|
4655
4777
|
}
|
|
4656
4778
|
resolveDirectory(directory) {
|
|
4657
4779
|
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
@@ -4660,7 +4782,7 @@ var DiskLayer = class {
|
|
|
4660
4782
|
if (directory.includes("\0")) {
|
|
4661
4783
|
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
4662
4784
|
}
|
|
4663
|
-
return (0,
|
|
4785
|
+
return (0, import_node_path2.resolve)(directory);
|
|
4664
4786
|
}
|
|
4665
4787
|
normalizeMaxFiles(maxFiles) {
|
|
4666
4788
|
if (maxFiles === void 0) {
|
|
@@ -4684,7 +4806,7 @@ var DiskLayer = class {
|
|
|
4684
4806
|
async readEntryFile(filePath) {
|
|
4685
4807
|
let handle;
|
|
4686
4808
|
try {
|
|
4687
|
-
handle = await
|
|
4809
|
+
handle = await import_node_fs2.promises.open(filePath, "r");
|
|
4688
4810
|
return await this.readHandleWithLimit(handle);
|
|
4689
4811
|
} catch {
|
|
4690
4812
|
await this.safeDelete(filePath);
|
|
@@ -4724,7 +4846,7 @@ var DiskLayer = class {
|
|
|
4724
4846
|
async scanEntries(visitor) {
|
|
4725
4847
|
let entries;
|
|
4726
4848
|
try {
|
|
4727
|
-
entries = await
|
|
4849
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4728
4850
|
} catch {
|
|
4729
4851
|
return;
|
|
4730
4852
|
}
|
|
@@ -4740,7 +4862,7 @@ var DiskLayer = class {
|
|
|
4740
4862
|
if (name === void 0) {
|
|
4741
4863
|
return;
|
|
4742
4864
|
}
|
|
4743
|
-
const filePath = (0,
|
|
4865
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4744
4866
|
const raw = await this.readEntryFile(filePath);
|
|
4745
4867
|
if (raw === null) {
|
|
4746
4868
|
continue;
|
|
@@ -4787,7 +4909,7 @@ var DiskLayer = class {
|
|
|
4787
4909
|
}
|
|
4788
4910
|
async safeDelete(filePath) {
|
|
4789
4911
|
try {
|
|
4790
|
-
await
|
|
4912
|
+
await import_node_fs2.promises.unlink(filePath);
|
|
4791
4913
|
} catch {
|
|
4792
4914
|
}
|
|
4793
4915
|
}
|
|
@@ -4805,7 +4927,7 @@ var DiskLayer = class {
|
|
|
4805
4927
|
}
|
|
4806
4928
|
let entries;
|
|
4807
4929
|
try {
|
|
4808
|
-
entries = await
|
|
4930
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4809
4931
|
} catch {
|
|
4810
4932
|
return;
|
|
4811
4933
|
}
|
|
@@ -4815,9 +4937,9 @@ var DiskLayer = class {
|
|
|
4815
4937
|
}
|
|
4816
4938
|
const withStats = await Promise.all(
|
|
4817
4939
|
lcFiles.map(async (name) => {
|
|
4818
|
-
const filePath = (0,
|
|
4940
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4819
4941
|
try {
|
|
4820
|
-
const stat = await
|
|
4942
|
+
const stat = await import_node_fs2.promises.stat(filePath);
|
|
4821
4943
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
4822
4944
|
} catch {
|
|
4823
4945
|
return { filePath, mtimeMs: 0 };
|
|
@@ -4913,44 +5035,19 @@ var MemcachedLayer = class {
|
|
|
4913
5035
|
|
|
4914
5036
|
// src/serialization/MsgpackSerializer.ts
|
|
4915
5037
|
var import_msgpack = require("@msgpack/msgpack");
|
|
4916
|
-
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4917
|
-
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4918
|
-
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4919
5038
|
var MsgpackSerializer = class {
|
|
4920
5039
|
serialize(value) {
|
|
4921
5040
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4922
5041
|
}
|
|
4923
5042
|
deserialize(payload) {
|
|
4924
5043
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4925
|
-
return
|
|
5044
|
+
return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
|
|
5045
|
+
label: "MessagePack payload",
|
|
5046
|
+
maxDepth: 64,
|
|
5047
|
+
maxNodes: 1e4
|
|
5048
|
+
});
|
|
4926
5049
|
}
|
|
4927
5050
|
};
|
|
4928
|
-
function sanitizeMsgpackValue(value, depth, state) {
|
|
4929
|
-
state.count += 1;
|
|
4930
|
-
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4931
|
-
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4932
|
-
}
|
|
4933
|
-
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4934
|
-
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4935
|
-
}
|
|
4936
|
-
if (Array.isArray(value)) {
|
|
4937
|
-
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4938
|
-
}
|
|
4939
|
-
if (!isPlainObject2(value)) {
|
|
4940
|
-
return value;
|
|
4941
|
-
}
|
|
4942
|
-
const sanitized = {};
|
|
4943
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
4944
|
-
if (DANGEROUS_KEYS2.has(key)) {
|
|
4945
|
-
continue;
|
|
4946
|
-
}
|
|
4947
|
-
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4948
|
-
}
|
|
4949
|
-
return sanitized;
|
|
4950
|
-
}
|
|
4951
|
-
function isPlainObject2(value) {
|
|
4952
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
4953
|
-
}
|
|
4954
5051
|
|
|
4955
5052
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
4956
5053
|
var import_node_crypto2 = require("crypto");
|