layercache 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.cjs +26 -2
- package/dist/cli.js +26 -2
- package/dist/{edge-BMmPVqaD.d.cts → edge-BXWTKlI1.d.cts} +21 -10
- package/dist/{edge-BMmPVqaD.d.ts → edge-BXWTKlI1.d.ts} +21 -10
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +1016 -834
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +919 -737
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +942 -715
- package/packages/nestjs/dist/index.d.cts +21 -10
- package/packages/nestjs/dist/index.d.ts +21 -10
- package/packages/nestjs/dist/index.js +942 -715
package/dist/index.cjs
CHANGED
|
@@ -528,7 +528,7 @@ function normalizeForSerialization(value) {
|
|
|
528
528
|
}
|
|
529
529
|
function serializeKeyPart(value) {
|
|
530
530
|
if (typeof value === "string") {
|
|
531
|
-
return `s:${value}`;
|
|
531
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
532
532
|
}
|
|
533
533
|
if (typeof value === "number") {
|
|
534
534
|
return `n:${value}`;
|
|
@@ -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,576 @@ 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
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
921
|
+
if (failures.length === 0) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
this.options.onWriteFailures(
|
|
925
|
+
context,
|
|
926
|
+
failures.map((failure) => failure.reason)
|
|
927
|
+
);
|
|
928
|
+
if (failures.length === operations.length) {
|
|
929
|
+
throw new AggregateError(
|
|
930
|
+
failures.map((failure) => failure.reason),
|
|
931
|
+
`${context.action} failed for every cache layer`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
936
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
937
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
938
|
+
layer.name,
|
|
939
|
+
writeOptions?.staleWhileRevalidate,
|
|
940
|
+
this.options.globalStaleWhileRevalidate
|
|
941
|
+
);
|
|
942
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
943
|
+
layer.name,
|
|
944
|
+
writeOptions?.staleIfError,
|
|
945
|
+
this.options.globalStaleIfError
|
|
946
|
+
);
|
|
947
|
+
const payload = createStoredValueEnvelope({
|
|
948
|
+
kind,
|
|
949
|
+
value,
|
|
950
|
+
freshTtlSeconds: freshTtl,
|
|
951
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
952
|
+
staleIfErrorSeconds: staleIfError,
|
|
953
|
+
now
|
|
954
|
+
});
|
|
955
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
956
|
+
return {
|
|
957
|
+
key,
|
|
958
|
+
value: payload,
|
|
959
|
+
ttl
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/internal/CacheStackMaintenance.ts
|
|
965
|
+
var CacheStackMaintenance = class {
|
|
966
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
967
|
+
writeBehindQueue = [];
|
|
968
|
+
writeBehindTimer;
|
|
969
|
+
writeBehindFlushPromise;
|
|
970
|
+
generationCleanupPromise;
|
|
971
|
+
clearEpoch = 0;
|
|
972
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
973
|
+
if (writeStrategy !== "write-behind") {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
977
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this.disposeWriteBehindTimer();
|
|
981
|
+
this.writeBehindTimer = setInterval(() => {
|
|
982
|
+
void flush();
|
|
983
|
+
}, flushIntervalMs);
|
|
984
|
+
this.writeBehindTimer.unref?.();
|
|
985
|
+
}
|
|
986
|
+
disposeWriteBehindTimer() {
|
|
987
|
+
if (!this.writeBehindTimer) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
clearInterval(this.writeBehindTimer);
|
|
991
|
+
this.writeBehindTimer = void 0;
|
|
992
|
+
}
|
|
993
|
+
beginClearEpoch() {
|
|
994
|
+
this.clearEpoch += 1;
|
|
995
|
+
this.keyEpochs.clear();
|
|
996
|
+
this.writeBehindQueue.length = 0;
|
|
997
|
+
}
|
|
998
|
+
currentClearEpoch() {
|
|
999
|
+
return this.clearEpoch;
|
|
1000
|
+
}
|
|
1001
|
+
currentKeyEpoch(key) {
|
|
1002
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1003
|
+
}
|
|
1004
|
+
bumpKeyEpochs(keys) {
|
|
1005
|
+
for (const key of keys) {
|
|
1006
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1010
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1019
|
+
this.writeBehindQueue.push(operation);
|
|
1020
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1021
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1022
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1023
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1027
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1031
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1032
|
+
await this.writeBehindFlushPromise;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1036
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1037
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1038
|
+
try {
|
|
1039
|
+
await this.writeBehindFlushPromise;
|
|
1040
|
+
} finally {
|
|
1041
|
+
this.writeBehindFlushPromise = void 0;
|
|
1042
|
+
}
|
|
1043
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1044
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1048
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1049
|
+
onError(generation, error);
|
|
1050
|
+
});
|
|
1051
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1052
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1053
|
+
this.generationCleanupPromise = void 0;
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async waitForGenerationCleanup() {
|
|
1058
|
+
await this.generationCleanupPromise;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
1063
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1064
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1065
|
+
}
|
|
1066
|
+
function shouldStartBackgroundRefresh({
|
|
1067
|
+
isDisconnecting,
|
|
1068
|
+
hasRefreshInFlight
|
|
1069
|
+
}) {
|
|
1070
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1071
|
+
}
|
|
1072
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1073
|
+
if (!gracefulDegradation) {
|
|
1074
|
+
return { degrade: false };
|
|
1075
|
+
}
|
|
1076
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1077
|
+
return {
|
|
1078
|
+
degrade: true,
|
|
1079
|
+
degradedUntil: now + retryAfterMs
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function planFreshReadPolicies({
|
|
1083
|
+
stored,
|
|
1084
|
+
hasFetcher,
|
|
1085
|
+
slidingTtl,
|
|
1086
|
+
refreshAheadSeconds
|
|
1087
|
+
}) {
|
|
1088
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1089
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1090
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1091
|
+
return {
|
|
1092
|
+
refreshedStored,
|
|
1093
|
+
refreshedStoredTtl,
|
|
1094
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1099
|
+
var import_node_crypto = require("crypto");
|
|
1100
|
+
var import_node_fs = require("fs");
|
|
1101
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1102
|
+
|
|
1103
|
+
// src/internal/CacheSnapshotFile.ts
|
|
1104
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1105
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1106
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1107
|
+
}
|
|
1108
|
+
async function findExistingAncestor(directory, fs3, path2) {
|
|
1109
|
+
let current = directory;
|
|
1110
|
+
while (true) {
|
|
1111
|
+
try {
|
|
1112
|
+
await fs3.lstat(current);
|
|
1113
|
+
return current;
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
if (error.code !== "ENOENT") {
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const parent = path2.dirname(current);
|
|
1120
|
+
if (parent === current) {
|
|
1121
|
+
return current;
|
|
1122
|
+
}
|
|
1123
|
+
current = parent;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1127
|
+
if (filePath.length === 0) {
|
|
1128
|
+
throw new Error("filePath must not be empty.");
|
|
1129
|
+
}
|
|
1130
|
+
if (filePath.includes("\0")) {
|
|
1131
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1132
|
+
}
|
|
1133
|
+
const { promises: fs3 } = await import("fs");
|
|
1134
|
+
const path2 = await import("path");
|
|
1135
|
+
const resolved = path2.resolve(filePath);
|
|
1136
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1137
|
+
if (baseDir === false) {
|
|
1138
|
+
return resolved;
|
|
1139
|
+
}
|
|
1140
|
+
await fs3.mkdir(baseDir, { recursive: true });
|
|
1141
|
+
const realBaseDir = await fs3.realpath(baseDir);
|
|
1142
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1143
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (mode === "read") {
|
|
1146
|
+
const realTarget = await fs3.realpath(resolved);
|
|
1147
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1148
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1149
|
+
}
|
|
1150
|
+
return realTarget;
|
|
1151
|
+
}
|
|
1152
|
+
const parentDir = path2.dirname(resolved);
|
|
1153
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
|
|
1154
|
+
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
1155
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1156
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1157
|
+
}
|
|
1158
|
+
await fs3.mkdir(parentDir, { recursive: true });
|
|
1159
|
+
const realParentDir = await fs3.realpath(parentDir);
|
|
1160
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1161
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1162
|
+
}
|
|
1163
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1164
|
+
try {
|
|
1165
|
+
const existing = await fs3.lstat(targetPath);
|
|
1166
|
+
if (existing.isSymbolicLink()) {
|
|
1167
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (error.code !== "ENOENT") {
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return targetPath;
|
|
1175
|
+
}
|
|
1176
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1177
|
+
if (byteLimit === false) {
|
|
1178
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1179
|
+
}
|
|
1180
|
+
const chunks = [];
|
|
1181
|
+
let totalBytes = 0;
|
|
1182
|
+
let position = 0;
|
|
1183
|
+
while (true) {
|
|
1184
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1185
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1186
|
+
if (bytesRead === 0) {
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
totalBytes += bytesRead;
|
|
1190
|
+
if (totalBytes > byteLimit) {
|
|
1191
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1192
|
+
}
|
|
1193
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1194
|
+
position += bytesRead;
|
|
1195
|
+
}
|
|
1196
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
1200
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1201
|
+
function sanitizeStructuredData(value, options) {
|
|
1202
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1203
|
+
}
|
|
1204
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1205
|
+
state.count += 1;
|
|
1206
|
+
if (state.count > options.maxNodes) {
|
|
1207
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1208
|
+
}
|
|
1209
|
+
if (depth > options.maxDepth) {
|
|
1210
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1211
|
+
}
|
|
1212
|
+
if (Array.isArray(value)) {
|
|
1213
|
+
const sanitized2 = [];
|
|
1214
|
+
for (const entry of value) {
|
|
1215
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1216
|
+
}
|
|
1217
|
+
return sanitized2;
|
|
1218
|
+
}
|
|
1219
|
+
if (!isPlainObject(value)) {
|
|
1220
|
+
return value;
|
|
1221
|
+
}
|
|
1222
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1223
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1224
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1228
|
+
}
|
|
1229
|
+
return sanitized;
|
|
1230
|
+
}
|
|
1231
|
+
function isPlainObject(value) {
|
|
1232
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1236
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1237
|
+
var CacheStackSnapshotManager = class {
|
|
1238
|
+
constructor(options) {
|
|
1239
|
+
this.options = options;
|
|
1240
|
+
}
|
|
1241
|
+
options;
|
|
1242
|
+
async exportState(maxEntries) {
|
|
1243
|
+
const entries = [];
|
|
1244
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1245
|
+
entries.push(entry);
|
|
1246
|
+
});
|
|
1247
|
+
return entries;
|
|
1248
|
+
}
|
|
1249
|
+
async importState(entries) {
|
|
1250
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1251
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1252
|
+
value: entry.value,
|
|
1253
|
+
ttl: entry.ttl
|
|
1254
|
+
}));
|
|
1255
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1256
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1257
|
+
await Promise.all(
|
|
1258
|
+
batch.map(async (entry) => {
|
|
1259
|
+
await Promise.all(
|
|
1260
|
+
this.options.layers.map(async (layer) => {
|
|
1261
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1262
|
+
try {
|
|
1263
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1266
|
+
}
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1269
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1270
|
+
})
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1275
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1276
|
+
const tempPath = import_node_path.default.join(
|
|
1277
|
+
import_node_path.default.dirname(targetPath),
|
|
1278
|
+
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1279
|
+
);
|
|
1280
|
+
let handle;
|
|
1281
|
+
try {
|
|
1282
|
+
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
1283
|
+
const openedHandle = handle;
|
|
1284
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1285
|
+
let wroteAny = false;
|
|
1286
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1287
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1288
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1289
|
+
wroteAny = true;
|
|
1290
|
+
});
|
|
1291
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1292
|
+
await openedHandle.close();
|
|
1293
|
+
handle = void 0;
|
|
1294
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
await handle?.close().catch(() => void 0);
|
|
1297
|
+
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1302
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1303
|
+
const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1304
|
+
let raw;
|
|
1305
|
+
try {
|
|
1306
|
+
if (maxBytes !== false) {
|
|
1307
|
+
const stat = await handle.stat();
|
|
1308
|
+
if (stat.size > maxBytes) {
|
|
1309
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1313
|
+
} finally {
|
|
1314
|
+
await handle.close();
|
|
1315
|
+
}
|
|
1316
|
+
let parsed;
|
|
1317
|
+
try {
|
|
1318
|
+
parsed = JSON.parse(raw);
|
|
1319
|
+
} catch (cause) {
|
|
1320
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1321
|
+
}
|
|
1322
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1323
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1324
|
+
}
|
|
1325
|
+
await this.importState(
|
|
1326
|
+
parsed.map((entry) => ({
|
|
1327
|
+
key: entry.key,
|
|
1328
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1329
|
+
ttl: entry.ttl
|
|
1330
|
+
}))
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1334
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1335
|
+
for (const layer of this.options.layers) {
|
|
1336
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const visitKey = async (key) => {
|
|
1340
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1341
|
+
if (exported.has(exportedKey)) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1345
|
+
if (stored === null) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
exported.add(exportedKey);
|
|
1349
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1350
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1351
|
+
}
|
|
1352
|
+
await visitor({
|
|
1353
|
+
key: exportedKey,
|
|
1354
|
+
value: stored,
|
|
1355
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1356
|
+
});
|
|
1357
|
+
};
|
|
1358
|
+
if (layer.forEachKey) {
|
|
1359
|
+
await layer.forEachKey(visitKey);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const keys = await layer.keys?.();
|
|
1363
|
+
for (const key of keys ?? []) {
|
|
1364
|
+
await visitKey(key);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
isCacheSnapshotEntries(value) {
|
|
1369
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1370
|
+
if (!entry || typeof entry !== "object") {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
const candidate = entry;
|
|
1374
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1375
|
+
});
|
|
954
1376
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
isDisconnecting,
|
|
964
|
-
hasRefreshInFlight
|
|
965
|
-
}) {
|
|
966
|
-
return !isDisconnecting && !hasRefreshInFlight;
|
|
967
|
-
}
|
|
968
|
-
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
969
|
-
if (!gracefulDegradation) {
|
|
970
|
-
return { degrade: false };
|
|
1377
|
+
sanitizeSnapshotValue(value) {
|
|
1378
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1379
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1380
|
+
label: "Snapshot value",
|
|
1381
|
+
maxDepth: 64,
|
|
1382
|
+
maxNodes: 1e4,
|
|
1383
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1384
|
+
});
|
|
971
1385
|
}
|
|
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
|
-
}
|
|
1386
|
+
};
|
|
993
1387
|
|
|
994
1388
|
// src/internal/CacheStackValidation.ts
|
|
995
1389
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -1218,7 +1612,11 @@ var FetchRateLimiter = class {
|
|
|
1218
1612
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1219
1613
|
nextFetcherBucketId = 0;
|
|
1220
1614
|
drainTimer;
|
|
1615
|
+
isDisposed = false;
|
|
1221
1616
|
async schedule(options, context, task) {
|
|
1617
|
+
if (this.isDisposed) {
|
|
1618
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1619
|
+
}
|
|
1222
1620
|
if (!options) {
|
|
1223
1621
|
return task();
|
|
1224
1622
|
}
|
|
@@ -1241,6 +1639,27 @@ var FetchRateLimiter = class {
|
|
|
1241
1639
|
this.drain();
|
|
1242
1640
|
});
|
|
1243
1641
|
}
|
|
1642
|
+
dispose() {
|
|
1643
|
+
this.isDisposed = true;
|
|
1644
|
+
if (this.drainTimer) {
|
|
1645
|
+
clearTimeout(this.drainTimer);
|
|
1646
|
+
this.drainTimer = void 0;
|
|
1647
|
+
}
|
|
1648
|
+
for (const bucket of this.buckets.values()) {
|
|
1649
|
+
if (bucket.cleanupTimer) {
|
|
1650
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1651
|
+
bucket.cleanupTimer = void 0;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1655
|
+
for (const item of queue) {
|
|
1656
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
this.queuesByBucket.clear();
|
|
1660
|
+
this.pendingBuckets.clear();
|
|
1661
|
+
this.buckets.clear();
|
|
1662
|
+
}
|
|
1244
1663
|
normalize(options) {
|
|
1245
1664
|
const maxConcurrent = options.maxConcurrent;
|
|
1246
1665
|
const intervalMs = options.intervalMs;
|
|
@@ -1276,6 +1695,9 @@ var FetchRateLimiter = class {
|
|
|
1276
1695
|
return "global";
|
|
1277
1696
|
}
|
|
1278
1697
|
drain() {
|
|
1698
|
+
if (this.isDisposed) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1279
1701
|
if (this.drainTimer) {
|
|
1280
1702
|
clearTimeout(this.drainTimer);
|
|
1281
1703
|
this.drainTimer = void 0;
|
|
@@ -1339,7 +1761,13 @@ var FetchRateLimiter = class {
|
|
|
1339
1761
|
this.pendingBuckets.add(next.bucketKey);
|
|
1340
1762
|
}
|
|
1341
1763
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1342
|
-
this.
|
|
1764
|
+
if (!this.drainTimer) {
|
|
1765
|
+
this.drainTimer = setTimeout(() => {
|
|
1766
|
+
this.drainTimer = void 0;
|
|
1767
|
+
this.drain();
|
|
1768
|
+
}, 0);
|
|
1769
|
+
this.drainTimer.unref?.();
|
|
1770
|
+
}
|
|
1343
1771
|
});
|
|
1344
1772
|
}
|
|
1345
1773
|
}
|
|
@@ -1372,12 +1800,18 @@ var FetchRateLimiter = class {
|
|
|
1372
1800
|
}
|
|
1373
1801
|
}
|
|
1374
1802
|
bucketState(bucketKey) {
|
|
1803
|
+
if (this.isDisposed) {
|
|
1804
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1805
|
+
}
|
|
1375
1806
|
const existing = this.buckets.get(bucketKey);
|
|
1376
1807
|
if (existing) {
|
|
1377
1808
|
return existing;
|
|
1378
1809
|
}
|
|
1379
1810
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1380
1811
|
this.evictIdleBuckets();
|
|
1812
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1813
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1814
|
+
}
|
|
1381
1815
|
}
|
|
1382
1816
|
const bucket = { active: 0, startedAt: [] };
|
|
1383
1817
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -1830,19 +2264,19 @@ var TagIndex = class {
|
|
|
1830
2264
|
if (!this.knownKeys.delete(key)) {
|
|
1831
2265
|
return;
|
|
1832
2266
|
}
|
|
1833
|
-
const
|
|
2267
|
+
const path2 = [];
|
|
1834
2268
|
let node = this.root;
|
|
1835
2269
|
for (const character of key) {
|
|
1836
2270
|
const child = node.children.get(character);
|
|
1837
2271
|
if (!child) {
|
|
1838
2272
|
return;
|
|
1839
2273
|
}
|
|
1840
|
-
|
|
2274
|
+
path2.push([node, character]);
|
|
1841
2275
|
node = child;
|
|
1842
2276
|
}
|
|
1843
2277
|
node.terminal = false;
|
|
1844
|
-
for (let index =
|
|
1845
|
-
const entry =
|
|
2278
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2279
|
+
const entry = path2[index];
|
|
1846
2280
|
if (!entry) {
|
|
1847
2281
|
continue;
|
|
1848
2282
|
}
|
|
@@ -1857,44 +2291,19 @@ var TagIndex = class {
|
|
|
1857
2291
|
};
|
|
1858
2292
|
|
|
1859
2293
|
// src/serialization/JsonSerializer.ts
|
|
1860
|
-
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1861
|
-
var MAX_SANITIZE_NODES = 1e4;
|
|
1862
2294
|
var JsonSerializer = class {
|
|
1863
2295
|
serialize(value) {
|
|
1864
2296
|
return JSON.stringify(value);
|
|
1865
2297
|
}
|
|
1866
2298
|
deserialize(payload) {
|
|
1867
2299
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1868
|
-
return
|
|
2300
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2301
|
+
label: "JSON payload",
|
|
2302
|
+
maxDepth: 200,
|
|
2303
|
+
maxNodes: 1e4
|
|
2304
|
+
});
|
|
1869
2305
|
}
|
|
1870
2306
|
};
|
|
1871
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1872
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
1873
|
-
state.count += 1;
|
|
1874
|
-
if (state.count > MAX_SANITIZE_NODES) {
|
|
1875
|
-
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1876
|
-
}
|
|
1877
|
-
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1878
|
-
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1879
|
-
}
|
|
1880
|
-
if (Array.isArray(value)) {
|
|
1881
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1882
|
-
}
|
|
1883
|
-
if (!isPlainObject(value)) {
|
|
1884
|
-
return value;
|
|
1885
|
-
}
|
|
1886
|
-
const sanitized = {};
|
|
1887
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1888
|
-
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1889
|
-
continue;
|
|
1890
|
-
}
|
|
1891
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1892
|
-
}
|
|
1893
|
-
return sanitized;
|
|
1894
|
-
}
|
|
1895
|
-
function isPlainObject(value) {
|
|
1896
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1897
|
-
}
|
|
1898
2307
|
|
|
1899
2308
|
// src/stampede/StampedeGuard.ts
|
|
1900
2309
|
var import_async_mutex2 = require("async-mutex");
|
|
@@ -1940,7 +2349,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1940
2349
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1941
2350
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1942
2351
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1943
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1944
2352
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1945
2353
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1946
2354
|
var DebugLogger = class {
|
|
@@ -1997,6 +2405,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1997
2405
|
await this.handleLayerFailure(layer, operation, error);
|
|
1998
2406
|
}
|
|
1999
2407
|
});
|
|
2408
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2409
|
+
tagIndex: this.tagIndex,
|
|
2410
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2411
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2412
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2416
|
+
layers: this.layers,
|
|
2417
|
+
maintenance: this.maintenance,
|
|
2418
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2419
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2420
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2421
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2422
|
+
},
|
|
2423
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2424
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2425
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2426
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2427
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2428
|
+
writePolicy: this.options.writePolicy,
|
|
2429
|
+
onWriteFailures: (context, failures) => {
|
|
2430
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2431
|
+
this.logger.debug?.("write-failure", {
|
|
2432
|
+
...context,
|
|
2433
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2000
2437
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
2001
2438
|
this.logger.warn?.(
|
|
2002
2439
|
"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 +2449,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2012
2449
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
2013
2450
|
);
|
|
2014
2451
|
}
|
|
2452
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2453
|
+
layers: this.layers,
|
|
2454
|
+
tagIndex: this.tagIndex,
|
|
2455
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2456
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2457
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2458
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2459
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2460
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2461
|
+
validateCacheKey,
|
|
2462
|
+
formatError: this.formatError.bind(this)
|
|
2463
|
+
});
|
|
2015
2464
|
this.initializeWriteBehind(options.writeBehind);
|
|
2016
2465
|
this.startup = this.initialize();
|
|
2017
2466
|
}
|
|
@@ -2027,11 +2476,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2027
2476
|
keyDiscovery;
|
|
2028
2477
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2029
2478
|
snapshotSerializer = new JsonSerializer();
|
|
2479
|
+
invalidation;
|
|
2480
|
+
layerWriter;
|
|
2481
|
+
snapshots;
|
|
2030
2482
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2483
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2031
2484
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2032
2485
|
maintenance = new CacheStackMaintenance();
|
|
2033
2486
|
ttlResolver;
|
|
2034
2487
|
circuitBreakerManager;
|
|
2488
|
+
nextOperationId = 0;
|
|
2035
2489
|
currentGeneration;
|
|
2036
2490
|
isDisconnecting = false;
|
|
2037
2491
|
disconnectPromise;
|
|
@@ -2042,10 +2496,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2042
2496
|
* and no `fetcher` is provided.
|
|
2043
2497
|
*/
|
|
2044
2498
|
async get(key, fetcher, options) {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2499
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2500
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2501
|
+
this.validateWriteOptions(options);
|
|
2502
|
+
await this.awaitStartup("get");
|
|
2503
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2504
|
+
});
|
|
2049
2505
|
}
|
|
2050
2506
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2051
2507
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2167,23 +2623,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2167
2623
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2168
2624
|
*/
|
|
2169
2625
|
async set(key, value, options) {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2626
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2627
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2628
|
+
this.validateWriteOptions(options);
|
|
2629
|
+
await this.awaitStartup("set");
|
|
2630
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2631
|
+
});
|
|
2174
2632
|
}
|
|
2175
2633
|
/**
|
|
2176
2634
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2177
2635
|
*/
|
|
2178
2636
|
async delete(key) {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2637
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2638
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2639
|
+
await this.awaitStartup("delete");
|
|
2640
|
+
await this.deleteKeys([normalizedKey]);
|
|
2641
|
+
await this.publishInvalidation({
|
|
2642
|
+
scope: "key",
|
|
2643
|
+
keys: [normalizedKey],
|
|
2644
|
+
sourceId: this.instanceId,
|
|
2645
|
+
operation: "delete"
|
|
2646
|
+
});
|
|
2187
2647
|
});
|
|
2188
2648
|
}
|
|
2189
2649
|
async clear() {
|
|
@@ -2216,95 +2676,102 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2216
2676
|
});
|
|
2217
2677
|
}
|
|
2218
2678
|
async mget(entries) {
|
|
2219
|
-
this.
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2679
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2680
|
+
this.assertActive("mget");
|
|
2681
|
+
if (entries.length === 0) {
|
|
2682
|
+
return [];
|
|
2683
|
+
}
|
|
2684
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2685
|
+
...entry,
|
|
2686
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2687
|
+
}));
|
|
2688
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2689
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2690
|
+
if (!canFastPath) {
|
|
2691
|
+
await this.awaitStartup("mget");
|
|
2692
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2693
|
+
return Promise.all(
|
|
2694
|
+
normalizedEntries.map((entry) => {
|
|
2695
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2696
|
+
const existing = pendingReads.get(entry.key);
|
|
2697
|
+
if (!existing) {
|
|
2698
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2699
|
+
pendingReads.set(entry.key, {
|
|
2700
|
+
promise,
|
|
2701
|
+
fetch: entry.fetch,
|
|
2702
|
+
optionsSignature
|
|
2703
|
+
});
|
|
2704
|
+
return promise;
|
|
2705
|
+
}
|
|
2706
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2707
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2708
|
+
}
|
|
2709
|
+
return existing.promise;
|
|
2710
|
+
})
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2230
2713
|
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;
|
|
2714
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2715
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2716
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2717
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2718
|
+
const entry = normalizedEntries[index];
|
|
2719
|
+
if (!entry) continue;
|
|
2720
|
+
const key = entry.key;
|
|
2721
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2722
|
+
indexes.push(index);
|
|
2723
|
+
indexesByKey.set(key, indexes);
|
|
2724
|
+
pending.add(key);
|
|
2271
2725
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const
|
|
2276
|
-
if (
|
|
2277
|
-
|
|
2726
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2727
|
+
const layer = this.layers[layerIndex];
|
|
2728
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2729
|
+
const keys = [...pending];
|
|
2730
|
+
if (keys.length === 0) {
|
|
2731
|
+
break;
|
|
2278
2732
|
}
|
|
2279
|
-
const
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2733
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2734
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2735
|
+
const key = keys[offset];
|
|
2736
|
+
const stored = values[offset];
|
|
2737
|
+
if (!key || stored === null) {
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
const resolved = resolveStoredValue(stored);
|
|
2741
|
+
if (resolved.state === "expired") {
|
|
2742
|
+
await layer.delete(key);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2746
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2747
|
+
}
|
|
2748
|
+
await this.tagIndex.touch(key);
|
|
2749
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2750
|
+
resultsByKey.set(key, resolved.value);
|
|
2751
|
+
pending.delete(key);
|
|
2752
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2283
2753
|
}
|
|
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
2754
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2755
|
+
if (pending.size > 0) {
|
|
2756
|
+
for (const key of pending) {
|
|
2757
|
+
await this.tagIndex.remove(key);
|
|
2758
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2759
|
+
}
|
|
2295
2760
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2761
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2762
|
+
});
|
|
2298
2763
|
}
|
|
2299
2764
|
async mset(entries) {
|
|
2300
|
-
this.
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2765
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2766
|
+
this.assertActive("mset");
|
|
2767
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2768
|
+
...entry,
|
|
2769
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2770
|
+
}));
|
|
2771
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2772
|
+
await this.awaitStartup("mset");
|
|
2773
|
+
await this.writeBatch(normalizedEntries);
|
|
2774
|
+
});
|
|
2308
2775
|
}
|
|
2309
2776
|
async warm(entries, options = {}) {
|
|
2310
2777
|
this.assertActive("warm");
|
|
@@ -2357,40 +2824,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2357
2824
|
return new CacheNamespace(this, prefix);
|
|
2358
2825
|
}
|
|
2359
2826
|
async invalidateByTag(tag) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2827
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2828
|
+
validateTag(tag);
|
|
2829
|
+
await this.awaitStartup("invalidateByTag");
|
|
2830
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2831
|
+
await this.deleteKeys(keys);
|
|
2832
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2833
|
+
});
|
|
2365
2834
|
}
|
|
2366
2835
|
async invalidateByTags(tags, mode = "any") {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2836
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2837
|
+
if (tags.length === 0) {
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
validateTags(tags);
|
|
2841
|
+
await this.awaitStartup("invalidateByTags");
|
|
2842
|
+
const keysByTag = await Promise.all(
|
|
2843
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2844
|
+
);
|
|
2845
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2846
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2847
|
+
await this.deleteKeys(keys);
|
|
2848
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2849
|
+
});
|
|
2377
2850
|
}
|
|
2378
2851
|
async invalidateByPattern(pattern) {
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
this.
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2852
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2853
|
+
validatePattern(pattern);
|
|
2854
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2855
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2856
|
+
this.qualifyPattern(pattern),
|
|
2857
|
+
this.invalidationMaxKeys()
|
|
2858
|
+
);
|
|
2859
|
+
await this.deleteKeys(keys);
|
|
2860
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2861
|
+
});
|
|
2387
2862
|
}
|
|
2388
2863
|
async invalidateByPrefix(prefix) {
|
|
2389
|
-
await this.
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2864
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2865
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2866
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2867
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2868
|
+
await this.deleteKeys(keys);
|
|
2869
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2870
|
+
});
|
|
2394
2871
|
}
|
|
2395
2872
|
getMetrics() {
|
|
2396
2873
|
return this.metricsCollector.snapshot;
|
|
@@ -2501,95 +2978,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2501
2978
|
}
|
|
2502
2979
|
async exportState() {
|
|
2503
2980
|
await this.awaitStartup("exportState");
|
|
2504
|
-
|
|
2505
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2506
|
-
entries.push(entry);
|
|
2507
|
-
});
|
|
2508
|
-
return entries;
|
|
2981
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2509
2982
|
}
|
|
2510
2983
|
async importState(entries) {
|
|
2511
2984
|
await this.awaitStartup("importState");
|
|
2512
|
-
|
|
2513
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2514
|
-
value: entry.value,
|
|
2515
|
-
ttl: entry.ttl
|
|
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
|
-
}
|
|
2985
|
+
await this.snapshots.importState(entries);
|
|
2526
2986
|
}
|
|
2527
2987
|
async persistToFile(filePath) {
|
|
2528
2988
|
this.assertActive("persistToFile");
|
|
2529
|
-
|
|
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
|
-
}
|
|
2989
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2556
2990
|
}
|
|
2557
2991
|
async restoreFromFile(filePath) {
|
|
2558
2992
|
this.assertActive("restoreFromFile");
|
|
2559
|
-
|
|
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
|
-
);
|
|
2993
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2593
2994
|
}
|
|
2594
2995
|
async disconnect() {
|
|
2595
2996
|
if (!this.disconnectPromise) {
|
|
@@ -2599,8 +3000,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2599
3000
|
await this.unsubscribeInvalidation?.();
|
|
2600
3001
|
await this.flushWriteBehindQueue();
|
|
2601
3002
|
await this.maintenance.waitForGenerationCleanup();
|
|
2602
|
-
|
|
3003
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3004
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3005
|
+
}
|
|
3006
|
+
await Promise.allSettled(
|
|
3007
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3008
|
+
let timer;
|
|
3009
|
+
return Promise.race([
|
|
3010
|
+
promise,
|
|
3011
|
+
new Promise((resolve2) => {
|
|
3012
|
+
timer = setTimeout(resolve2, 5e3);
|
|
3013
|
+
timer.unref?.();
|
|
3014
|
+
})
|
|
3015
|
+
]).finally(() => {
|
|
3016
|
+
if (timer) clearTimeout(timer);
|
|
3017
|
+
});
|
|
3018
|
+
})
|
|
3019
|
+
);
|
|
3020
|
+
this.backgroundRefreshes.clear();
|
|
3021
|
+
this.backgroundRefreshAbort.clear();
|
|
2603
3022
|
this.maintenance.disposeWriteBehindTimer();
|
|
3023
|
+
this.fetchRateLimiter.dispose();
|
|
2604
3024
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2605
3025
|
})();
|
|
2606
3026
|
}
|
|
@@ -2714,7 +3134,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2714
3134
|
async storeEntry(key, kind, value, options) {
|
|
2715
3135
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2716
3136
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2717
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
3137
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2718
3138
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2719
3139
|
return;
|
|
2720
3140
|
}
|
|
@@ -2731,52 +3151,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2731
3151
|
}
|
|
2732
3152
|
}
|
|
2733
3153
|
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)));
|
|
3154
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2780
3155
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2781
3156
|
return;
|
|
2782
3157
|
}
|
|
@@ -2883,58 +3258,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2883
3258
|
this.emit("backfill", { key, layer: layer.name });
|
|
2884
3259
|
}
|
|
2885
3260
|
}
|
|
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
3261
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2939
3262
|
return this.ttlResolver.resolveFreshTtl(
|
|
2940
3263
|
key,
|
|
@@ -2962,15 +3285,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2962
3285
|
}
|
|
2963
3286
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2964
3287
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3288
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
2965
3289
|
const refresh = (async () => {
|
|
2966
3290
|
this.metricsCollector.increment("refreshes");
|
|
2967
3291
|
try {
|
|
3292
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2968
3293
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2969
3294
|
} catch (error) {
|
|
3295
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2970
3296
|
this.metricsCollector.increment("refreshErrors");
|
|
2971
3297
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
2972
3298
|
} finally {
|
|
2973
3299
|
this.backgroundRefreshes.delete(key);
|
|
3300
|
+
this.backgroundRefreshAbort.delete(key);
|
|
2974
3301
|
}
|
|
2975
3302
|
})();
|
|
2976
3303
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3000,7 +3327,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3000
3327
|
return;
|
|
3001
3328
|
}
|
|
3002
3329
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3003
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3330
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
3004
3331
|
for (const key of keys) {
|
|
3005
3332
|
await this.tagIndex.remove(key);
|
|
3006
3333
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -3032,7 +3359,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3032
3359
|
}
|
|
3033
3360
|
const keys = message.keys ?? [];
|
|
3034
3361
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3035
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3362
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3036
3363
|
if (message.operation !== "write") {
|
|
3037
3364
|
for (const key of keys) {
|
|
3038
3365
|
await this.tagIndex.remove(key);
|
|
@@ -3073,7 +3400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3073
3400
|
timer.unref?.();
|
|
3074
3401
|
})
|
|
3075
3402
|
]);
|
|
3076
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3403
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3077
3404
|
if (result.kind === "error") {
|
|
3078
3405
|
throw result.error;
|
|
3079
3406
|
}
|
|
@@ -3089,6 +3416,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3089
3416
|
shouldBroadcastL1Invalidation() {
|
|
3090
3417
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3091
3418
|
}
|
|
3419
|
+
async observeOperation(name, attributes, execute) {
|
|
3420
|
+
const id = this.nextOperationId;
|
|
3421
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3422
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3423
|
+
try {
|
|
3424
|
+
const result = await execute();
|
|
3425
|
+
this.emit("operation-end", {
|
|
3426
|
+
id,
|
|
3427
|
+
name,
|
|
3428
|
+
attributes,
|
|
3429
|
+
success: true,
|
|
3430
|
+
result: result === null ? "null" : void 0
|
|
3431
|
+
});
|
|
3432
|
+
return result;
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
this.emit("operation-end", {
|
|
3435
|
+
id,
|
|
3436
|
+
name,
|
|
3437
|
+
attributes,
|
|
3438
|
+
success: false,
|
|
3439
|
+
error
|
|
3440
|
+
});
|
|
3441
|
+
throw error;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3092
3444
|
scheduleGenerationCleanup(generation) {
|
|
3093
3445
|
this.maintenance.scheduleGenerationCleanup(
|
|
3094
3446
|
generation,
|
|
@@ -3144,37 +3496,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3144
3496
|
});
|
|
3145
3497
|
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3146
3498
|
}
|
|
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
3499
|
qualifyKey(key) {
|
|
3179
3500
|
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3180
3501
|
}
|
|
@@ -3184,32 +3505,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3184
3505
|
stripQualifiedKey(key) {
|
|
3185
3506
|
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3186
3507
|
}
|
|
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
3508
|
validateConfiguration() {
|
|
3214
3509
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
3215
3510
|
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
@@ -3340,18 +3635,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3340
3635
|
this.emit("error", { operation, ...context });
|
|
3341
3636
|
}
|
|
3342
3637
|
}
|
|
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
3638
|
snapshotMaxBytes() {
|
|
3356
3639
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3357
3640
|
}
|
|
@@ -3361,62 +3644,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3361
3644
|
invalidationMaxKeys() {
|
|
3362
3645
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3363
3646
|
}
|
|
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
3647
|
};
|
|
3421
3648
|
|
|
3422
3649
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -3427,6 +3654,7 @@ var RedisInvalidationBus = class {
|
|
|
3427
3654
|
logger;
|
|
3428
3655
|
handlers = /* @__PURE__ */ new Set();
|
|
3429
3656
|
sharedListener;
|
|
3657
|
+
subscribePromise;
|
|
3430
3658
|
constructor(options) {
|
|
3431
3659
|
this.publisher = options.publisher;
|
|
3432
3660
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
@@ -3434,15 +3662,27 @@ var RedisInvalidationBus = class {
|
|
|
3434
3662
|
this.logger = options.logger;
|
|
3435
3663
|
}
|
|
3436
3664
|
async subscribe(handler) {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
await
|
|
3665
|
+
const previousPromise = this.subscribePromise;
|
|
3666
|
+
let resolveThis;
|
|
3667
|
+
this.subscribePromise = new Promise((resolve2) => {
|
|
3668
|
+
resolveThis = resolve2;
|
|
3669
|
+
});
|
|
3670
|
+
if (previousPromise) {
|
|
3671
|
+
await previousPromise;
|
|
3672
|
+
}
|
|
3673
|
+
try {
|
|
3674
|
+
if (this.handlers.size === 0) {
|
|
3675
|
+
const listener = (_channel, payload) => {
|
|
3676
|
+
void this.dispatchToHandlers(payload);
|
|
3677
|
+
};
|
|
3678
|
+
this.sharedListener = listener;
|
|
3679
|
+
this.subscriber.on("message", listener);
|
|
3680
|
+
await this.subscriber.subscribe(this.channel);
|
|
3681
|
+
}
|
|
3682
|
+
this.handlers.add(handler);
|
|
3683
|
+
} finally {
|
|
3684
|
+
resolveThis();
|
|
3444
3685
|
}
|
|
3445
|
-
this.handlers.add(handler);
|
|
3446
3686
|
return async () => {
|
|
3447
3687
|
this.handlers.delete(handler);
|
|
3448
3688
|
if (this.handlers.size === 0 && this.sharedListener) {
|
|
@@ -3458,7 +3698,12 @@ var RedisInvalidationBus = class {
|
|
|
3458
3698
|
async dispatchToHandlers(payload) {
|
|
3459
3699
|
let message;
|
|
3460
3700
|
try {
|
|
3461
|
-
const parsed =
|
|
3701
|
+
const parsed = sanitizeStructuredData(JSON.parse(payload), {
|
|
3702
|
+
label: "Invalidation payload",
|
|
3703
|
+
maxDepth: 64,
|
|
3704
|
+
maxNodes: 1e4,
|
|
3705
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3706
|
+
});
|
|
3462
3707
|
if (!this.isInvalidationMessage(parsed)) {
|
|
3463
3708
|
throw new Error("Invalid invalidation payload shape.");
|
|
3464
3709
|
}
|
|
@@ -3495,31 +3740,6 @@ var RedisInvalidationBus = class {
|
|
|
3495
3740
|
console.error(`[layercache] ${message}`, error);
|
|
3496
3741
|
}
|
|
3497
3742
|
};
|
|
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
3743
|
|
|
3524
3744
|
// src/invalidation/RedisTagIndex.ts
|
|
3525
3745
|
var RedisTagIndex = class {
|
|
@@ -3888,65 +4108,52 @@ function normalizeUrl2(url) {
|
|
|
3888
4108
|
}
|
|
3889
4109
|
|
|
3890
4110
|
// src/integrations/opentelemetry.ts
|
|
4111
|
+
var MAX_SPANS = 1e4;
|
|
3891
4112
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3892
|
-
const
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
"layercache.key": String(args[0] ?? "")
|
|
3905
|
-
}));
|
|
3906
|
-
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
3907
|
-
"layercache.key": String(args[0] ?? "")
|
|
3908
|
-
}));
|
|
3909
|
-
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
3910
|
-
"layercache.key": String(args[0] ?? "")
|
|
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;
|
|
4113
|
+
const spans = /* @__PURE__ */ new Map();
|
|
4114
|
+
const onStart = (event) => {
|
|
4115
|
+
try {
|
|
4116
|
+
if (spans.size >= MAX_SPANS) {
|
|
4117
|
+
const oldest = spans.keys().next().value;
|
|
4118
|
+
if (oldest !== void 0) {
|
|
4119
|
+
spans.get(oldest)?.end();
|
|
4120
|
+
spans.delete(oldest);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
4124
|
+
} catch {
|
|
3929
4125
|
}
|
|
3930
4126
|
};
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
4127
|
+
const onEnd = (event) => {
|
|
4128
|
+
const span = spans.get(event.id);
|
|
4129
|
+
if (!span) {
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
spans.delete(event.id);
|
|
3935
4133
|
try {
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
span.setAttribute?.("layercache.result", "null");
|
|
4134
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
4135
|
+
if (event.result) {
|
|
4136
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3940
4137
|
}
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
throw error;
|
|
3946
|
-
} finally {
|
|
3947
|
-
span.end();
|
|
4138
|
+
if (event.error !== void 0) {
|
|
4139
|
+
span.recordException?.(event.error);
|
|
4140
|
+
}
|
|
4141
|
+
} catch {
|
|
3948
4142
|
}
|
|
3949
|
-
|
|
4143
|
+
span.end();
|
|
4144
|
+
};
|
|
4145
|
+
cache.on("operation-start", onStart);
|
|
4146
|
+
cache.on("operation-end", onEnd);
|
|
4147
|
+
return {
|
|
4148
|
+
uninstall() {
|
|
4149
|
+
cache.off("operation-start", onStart);
|
|
4150
|
+
cache.off("operation-end", onEnd);
|
|
4151
|
+
for (const span of spans.values()) {
|
|
4152
|
+
span.end();
|
|
4153
|
+
}
|
|
4154
|
+
spans.clear();
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
3950
4157
|
}
|
|
3951
4158
|
|
|
3952
4159
|
// src/integrations/trpc.ts
|
|
@@ -4497,9 +4704,9 @@ var RedisLayer = class {
|
|
|
4497
4704
|
};
|
|
4498
4705
|
|
|
4499
4706
|
// src/layers/DiskLayer.ts
|
|
4500
|
-
var
|
|
4501
|
-
var
|
|
4502
|
-
var
|
|
4707
|
+
var import_node_crypto2 = require("crypto");
|
|
4708
|
+
var import_node_fs2 = require("fs");
|
|
4709
|
+
var import_node_path2 = require("path");
|
|
4503
4710
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
4504
4711
|
var DiskLayer = class {
|
|
4505
4712
|
name;
|
|
@@ -4542,7 +4749,7 @@ var DiskLayer = class {
|
|
|
4542
4749
|
}
|
|
4543
4750
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4544
4751
|
await this.enqueueWrite(async () => {
|
|
4545
|
-
await
|
|
4752
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4546
4753
|
const entry = {
|
|
4547
4754
|
key,
|
|
4548
4755
|
value,
|
|
@@ -4550,10 +4757,10 @@ var DiskLayer = class {
|
|
|
4550
4757
|
};
|
|
4551
4758
|
const payload = this.serializer.serialize(entry);
|
|
4552
4759
|
const targetPath = this.keyToPath(key);
|
|
4553
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
4760
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
|
|
4554
4761
|
try {
|
|
4555
|
-
await
|
|
4556
|
-
await
|
|
4762
|
+
await import_node_fs2.promises.writeFile(tempPath, payload);
|
|
4763
|
+
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
4557
4764
|
} catch (error) {
|
|
4558
4765
|
await this.safeDelete(tempPath);
|
|
4559
4766
|
throw error;
|
|
@@ -4607,12 +4814,12 @@ var DiskLayer = class {
|
|
|
4607
4814
|
await this.enqueueWrite(async () => {
|
|
4608
4815
|
let entries;
|
|
4609
4816
|
try {
|
|
4610
|
-
entries = await
|
|
4817
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4611
4818
|
} catch {
|
|
4612
4819
|
return;
|
|
4613
4820
|
}
|
|
4614
4821
|
await this.deletePathsWithConcurrency(
|
|
4615
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) => (0,
|
|
4822
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
|
|
4616
4823
|
);
|
|
4617
4824
|
});
|
|
4618
4825
|
}
|
|
@@ -4641,7 +4848,7 @@ var DiskLayer = class {
|
|
|
4641
4848
|
}
|
|
4642
4849
|
async ping() {
|
|
4643
4850
|
try {
|
|
4644
|
-
await
|
|
4851
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4645
4852
|
return true;
|
|
4646
4853
|
} catch {
|
|
4647
4854
|
return false;
|
|
@@ -4650,8 +4857,8 @@ var DiskLayer = class {
|
|
|
4650
4857
|
async dispose() {
|
|
4651
4858
|
}
|
|
4652
4859
|
keyToPath(key) {
|
|
4653
|
-
const hash = (0,
|
|
4654
|
-
return (0,
|
|
4860
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
|
|
4861
|
+
return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
|
|
4655
4862
|
}
|
|
4656
4863
|
resolveDirectory(directory) {
|
|
4657
4864
|
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
@@ -4660,7 +4867,7 @@ var DiskLayer = class {
|
|
|
4660
4867
|
if (directory.includes("\0")) {
|
|
4661
4868
|
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
4662
4869
|
}
|
|
4663
|
-
return (0,
|
|
4870
|
+
return (0, import_node_path2.resolve)(directory);
|
|
4664
4871
|
}
|
|
4665
4872
|
normalizeMaxFiles(maxFiles) {
|
|
4666
4873
|
if (maxFiles === void 0) {
|
|
@@ -4684,7 +4891,7 @@ var DiskLayer = class {
|
|
|
4684
4891
|
async readEntryFile(filePath) {
|
|
4685
4892
|
let handle;
|
|
4686
4893
|
try {
|
|
4687
|
-
handle = await
|
|
4894
|
+
handle = await import_node_fs2.promises.open(filePath, "r");
|
|
4688
4895
|
return await this.readHandleWithLimit(handle);
|
|
4689
4896
|
} catch {
|
|
4690
4897
|
await this.safeDelete(filePath);
|
|
@@ -4724,7 +4931,7 @@ var DiskLayer = class {
|
|
|
4724
4931
|
async scanEntries(visitor) {
|
|
4725
4932
|
let entries;
|
|
4726
4933
|
try {
|
|
4727
|
-
entries = await
|
|
4934
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4728
4935
|
} catch {
|
|
4729
4936
|
return;
|
|
4730
4937
|
}
|
|
@@ -4740,7 +4947,7 @@ var DiskLayer = class {
|
|
|
4740
4947
|
if (name === void 0) {
|
|
4741
4948
|
return;
|
|
4742
4949
|
}
|
|
4743
|
-
const filePath = (0,
|
|
4950
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4744
4951
|
const raw = await this.readEntryFile(filePath);
|
|
4745
4952
|
if (raw === null) {
|
|
4746
4953
|
continue;
|
|
@@ -4787,7 +4994,7 @@ var DiskLayer = class {
|
|
|
4787
4994
|
}
|
|
4788
4995
|
async safeDelete(filePath) {
|
|
4789
4996
|
try {
|
|
4790
|
-
await
|
|
4997
|
+
await import_node_fs2.promises.unlink(filePath);
|
|
4791
4998
|
} catch {
|
|
4792
4999
|
}
|
|
4793
5000
|
}
|
|
@@ -4805,7 +5012,7 @@ var DiskLayer = class {
|
|
|
4805
5012
|
}
|
|
4806
5013
|
let entries;
|
|
4807
5014
|
try {
|
|
4808
|
-
entries = await
|
|
5015
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4809
5016
|
} catch {
|
|
4810
5017
|
return;
|
|
4811
5018
|
}
|
|
@@ -4815,9 +5022,9 @@ var DiskLayer = class {
|
|
|
4815
5022
|
}
|
|
4816
5023
|
const withStats = await Promise.all(
|
|
4817
5024
|
lcFiles.map(async (name) => {
|
|
4818
|
-
const filePath = (0,
|
|
5025
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4819
5026
|
try {
|
|
4820
|
-
const stat = await
|
|
5027
|
+
const stat = await import_node_fs2.promises.stat(filePath);
|
|
4821
5028
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
4822
5029
|
} catch {
|
|
4823
5030
|
return { filePath, mtimeMs: 0 };
|
|
@@ -4913,47 +5120,22 @@ var MemcachedLayer = class {
|
|
|
4913
5120
|
|
|
4914
5121
|
// src/serialization/MsgpackSerializer.ts
|
|
4915
5122
|
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
5123
|
var MsgpackSerializer = class {
|
|
4920
5124
|
serialize(value) {
|
|
4921
5125
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4922
5126
|
}
|
|
4923
5127
|
deserialize(payload) {
|
|
4924
5128
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4925
|
-
return
|
|
5129
|
+
return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
|
|
5130
|
+
label: "MessagePack payload",
|
|
5131
|
+
maxDepth: 64,
|
|
5132
|
+
maxNodes: 1e4
|
|
5133
|
+
});
|
|
4926
5134
|
}
|
|
4927
5135
|
};
|
|
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
5136
|
|
|
4955
5137
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
4956
|
-
var
|
|
5138
|
+
var import_node_crypto3 = require("crypto");
|
|
4957
5139
|
var RELEASE_SCRIPT = `
|
|
4958
5140
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
4959
5141
|
return redis.call("del", KEYS[1])
|
|
@@ -4975,7 +5157,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
4975
5157
|
}
|
|
4976
5158
|
async execute(key, options, worker, waiter) {
|
|
4977
5159
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
4978
|
-
const token = (0,
|
|
5160
|
+
const token = (0, import_node_crypto3.randomUUID)();
|
|
4979
5161
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
4980
5162
|
if (acquired === "OK") {
|
|
4981
5163
|
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|