layercache 1.2.7 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.cjs +12 -1
- package/dist/cli.js +12 -1
- package/dist/{edge-BMmPVqaD.d.cts → edge-DBs8Ko5W.d.cts} +20 -10
- package/dist/{edge-BMmPVqaD.d.ts → edge-DBs8Ko5W.d.ts} +20 -10
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +924 -827
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.js +831 -734
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +877 -708
- package/packages/nestjs/dist/index.d.cts +20 -10
- package/packages/nestjs/dist/index.d.ts +20 -10
- package/packages/nestjs/dist/index.js +872 -703
package/dist/index.js
CHANGED
|
@@ -468,102 +468,6 @@ function createInstanceId() {
|
|
|
468
468
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
// src/internal/CacheSnapshotFile.ts
|
|
472
|
-
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
473
|
-
const relative = path.relative(realBaseDir, candidatePath);
|
|
474
|
-
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
475
|
-
}
|
|
476
|
-
async function findExistingAncestor(directory, fs2, path) {
|
|
477
|
-
let current = directory;
|
|
478
|
-
while (true) {
|
|
479
|
-
try {
|
|
480
|
-
await fs2.lstat(current);
|
|
481
|
-
return current;
|
|
482
|
-
} catch (error) {
|
|
483
|
-
if (error.code !== "ENOENT") {
|
|
484
|
-
throw error;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const parent = path.dirname(current);
|
|
488
|
-
if (parent === current) {
|
|
489
|
-
return current;
|
|
490
|
-
}
|
|
491
|
-
current = parent;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
495
|
-
if (filePath.length === 0) {
|
|
496
|
-
throw new Error("filePath must not be empty.");
|
|
497
|
-
}
|
|
498
|
-
if (filePath.includes("\0")) {
|
|
499
|
-
throw new Error("filePath must not contain null bytes.");
|
|
500
|
-
}
|
|
501
|
-
const { promises: fs2 } = await import("fs");
|
|
502
|
-
const path = await import("path");
|
|
503
|
-
const resolved = path.resolve(filePath);
|
|
504
|
-
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
505
|
-
if (baseDir === false) {
|
|
506
|
-
return resolved;
|
|
507
|
-
}
|
|
508
|
-
await fs2.mkdir(baseDir, { recursive: true });
|
|
509
|
-
const realBaseDir = await fs2.realpath(baseDir);
|
|
510
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
511
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
512
|
-
}
|
|
513
|
-
if (mode === "read") {
|
|
514
|
-
const realTarget = await fs2.realpath(resolved);
|
|
515
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
516
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
517
|
-
}
|
|
518
|
-
return realTarget;
|
|
519
|
-
}
|
|
520
|
-
const parentDir = path.dirname(resolved);
|
|
521
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
|
|
522
|
-
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
523
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
524
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
525
|
-
}
|
|
526
|
-
await fs2.mkdir(parentDir, { recursive: true });
|
|
527
|
-
const realParentDir = await fs2.realpath(parentDir);
|
|
528
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
529
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
530
|
-
}
|
|
531
|
-
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
532
|
-
try {
|
|
533
|
-
const existing = await fs2.lstat(targetPath);
|
|
534
|
-
if (existing.isSymbolicLink()) {
|
|
535
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
536
|
-
}
|
|
537
|
-
} catch (error) {
|
|
538
|
-
if (error.code !== "ENOENT") {
|
|
539
|
-
throw error;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return targetPath;
|
|
543
|
-
}
|
|
544
|
-
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
545
|
-
if (byteLimit === false) {
|
|
546
|
-
return handle.readFile({ encoding: "utf8" });
|
|
547
|
-
}
|
|
548
|
-
const chunks = [];
|
|
549
|
-
let totalBytes = 0;
|
|
550
|
-
let position = 0;
|
|
551
|
-
while (true) {
|
|
552
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
553
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
554
|
-
if (bytesRead === 0) {
|
|
555
|
-
break;
|
|
556
|
-
}
|
|
557
|
-
totalBytes += bytesRead;
|
|
558
|
-
if (totalBytes > byteLimit) {
|
|
559
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
560
|
-
}
|
|
561
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
562
|
-
position += bytesRead;
|
|
563
|
-
}
|
|
564
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
565
|
-
}
|
|
566
|
-
|
|
567
471
|
// src/internal/CacheStackGeneration.ts
|
|
568
472
|
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
569
473
|
function generationPrefix(generation) {
|
|
@@ -611,6 +515,205 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
|
611
515
|
return batches;
|
|
612
516
|
}
|
|
613
517
|
|
|
518
|
+
// src/internal/CacheStackInvalidationSupport.ts
|
|
519
|
+
var CacheStackInvalidationSupport = class {
|
|
520
|
+
constructor(options) {
|
|
521
|
+
this.options = options;
|
|
522
|
+
}
|
|
523
|
+
options;
|
|
524
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
525
|
+
const keys = /* @__PURE__ */ new Set();
|
|
526
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
527
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
528
|
+
keys.add(key);
|
|
529
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
530
|
+
});
|
|
531
|
+
return [...keys];
|
|
532
|
+
}
|
|
533
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
534
|
+
keys.add(key);
|
|
535
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
536
|
+
}
|
|
537
|
+
return [...keys];
|
|
538
|
+
}
|
|
539
|
+
intersectKeys(groups) {
|
|
540
|
+
if (groups.length === 0) {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
const [firstGroup, ...rest] = groups;
|
|
544
|
+
const restSets = rest.map((group) => new Set(group));
|
|
545
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
546
|
+
}
|
|
547
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
548
|
+
await Promise.all(
|
|
549
|
+
layers.map(async (layer) => {
|
|
550
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (layer.deleteMany) {
|
|
554
|
+
try {
|
|
555
|
+
await layer.deleteMany(keys);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await Promise.all(
|
|
562
|
+
keys.map(async (key) => {
|
|
563
|
+
try {
|
|
564
|
+
await layer.delete(key);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
574
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
575
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/internal/CacheStackLayerWriter.ts
|
|
581
|
+
var CacheStackLayerWriter = class {
|
|
582
|
+
constructor(options) {
|
|
583
|
+
this.options = options;
|
|
584
|
+
}
|
|
585
|
+
options;
|
|
586
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
589
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
590
|
+
const immediateOperations = [];
|
|
591
|
+
const deferredOperations = [];
|
|
592
|
+
for (const layer of this.options.layers) {
|
|
593
|
+
const operation = async () => {
|
|
594
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
601
|
+
try {
|
|
602
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
608
|
+
deferredOperations.push(operation);
|
|
609
|
+
} else {
|
|
610
|
+
immediateOperations.push(operation);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
614
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
615
|
+
}
|
|
616
|
+
async writeBatch(entries) {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
619
|
+
const entryEpochs = new Map(
|
|
620
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
621
|
+
);
|
|
622
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
623
|
+
const immediateOperations = [];
|
|
624
|
+
const deferredOperations = [];
|
|
625
|
+
for (const entry of entries) {
|
|
626
|
+
for (const layer of this.options.layers) {
|
|
627
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
631
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
632
|
+
bucket.push(layerEntry);
|
|
633
|
+
entriesByLayer.set(layer, bucket);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
637
|
+
const operation = async () => {
|
|
638
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const activeEntries = layerEntries.filter(
|
|
642
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
643
|
+
);
|
|
644
|
+
if (activeEntries.length === 0) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
if (layer.setMany) {
|
|
649
|
+
await layer.setMany(activeEntries);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
653
|
+
} catch (error) {
|
|
654
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
658
|
+
deferredOperations.push(operation);
|
|
659
|
+
} else {
|
|
660
|
+
immediateOperations.push(operation);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
664
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
665
|
+
return { clearEpoch, entryEpochs };
|
|
666
|
+
}
|
|
667
|
+
async executeLayerOperations(operations, context) {
|
|
668
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
669
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
673
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
674
|
+
if (failures.length === 0) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.options.onWriteFailures(
|
|
678
|
+
context,
|
|
679
|
+
failures.map((failure) => failure.reason)
|
|
680
|
+
);
|
|
681
|
+
if (failures.length === operations.length) {
|
|
682
|
+
throw new AggregateError(
|
|
683
|
+
failures.map((failure) => failure.reason),
|
|
684
|
+
`${context.action} failed for every cache layer`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
689
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
690
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
691
|
+
layer.name,
|
|
692
|
+
writeOptions?.staleWhileRevalidate,
|
|
693
|
+
this.options.globalStaleWhileRevalidate
|
|
694
|
+
);
|
|
695
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
696
|
+
layer.name,
|
|
697
|
+
writeOptions?.staleIfError,
|
|
698
|
+
this.options.globalStaleIfError
|
|
699
|
+
);
|
|
700
|
+
const payload = createStoredValueEnvelope({
|
|
701
|
+
kind,
|
|
702
|
+
value,
|
|
703
|
+
freshTtlSeconds: freshTtl,
|
|
704
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
705
|
+
staleIfErrorSeconds: staleIfError,
|
|
706
|
+
now
|
|
707
|
+
});
|
|
708
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
709
|
+
return {
|
|
710
|
+
key,
|
|
711
|
+
value: payload,
|
|
712
|
+
ttl
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
614
717
|
// src/internal/CacheStackMaintenance.ts
|
|
615
718
|
var CacheStackMaintenance = class {
|
|
616
719
|
keyEpochs = /* @__PURE__ */ new Map();
|
|
@@ -694,57 +797,295 @@ var CacheStackMaintenance = class {
|
|
|
694
797
|
await this.flushWriteBehindQueue(options, flushBatch);
|
|
695
798
|
}
|
|
696
799
|
}
|
|
697
|
-
scheduleGenerationCleanup(generation, task, onError) {
|
|
698
|
-
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
699
|
-
onError(generation, error);
|
|
700
|
-
});
|
|
701
|
-
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
702
|
-
if (this.generationCleanupPromise === scheduledTask) {
|
|
703
|
-
this.generationCleanupPromise = void 0;
|
|
800
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
801
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
802
|
+
onError(generation, error);
|
|
803
|
+
});
|
|
804
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
805
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
806
|
+
this.generationCleanupPromise = void 0;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
async waitForGenerationCleanup() {
|
|
811
|
+
await this.generationCleanupPromise;
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
816
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
817
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
818
|
+
}
|
|
819
|
+
function shouldStartBackgroundRefresh({
|
|
820
|
+
isDisconnecting,
|
|
821
|
+
hasRefreshInFlight
|
|
822
|
+
}) {
|
|
823
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
824
|
+
}
|
|
825
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
826
|
+
if (!gracefulDegradation) {
|
|
827
|
+
return { degrade: false };
|
|
828
|
+
}
|
|
829
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
830
|
+
return {
|
|
831
|
+
degrade: true,
|
|
832
|
+
degradedUntil: now + retryAfterMs
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function planFreshReadPolicies({
|
|
836
|
+
stored,
|
|
837
|
+
hasFetcher,
|
|
838
|
+
slidingTtl,
|
|
839
|
+
refreshAheadSeconds
|
|
840
|
+
}) {
|
|
841
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
842
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
843
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
844
|
+
return {
|
|
845
|
+
refreshedStored,
|
|
846
|
+
refreshedStoredTtl,
|
|
847
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
852
|
+
import { constants, promises as fs } from "fs";
|
|
853
|
+
import path from "path";
|
|
854
|
+
|
|
855
|
+
// src/internal/CacheSnapshotFile.ts
|
|
856
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
857
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
858
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
859
|
+
}
|
|
860
|
+
async function findExistingAncestor(directory, fs3, path2) {
|
|
861
|
+
let current = directory;
|
|
862
|
+
while (true) {
|
|
863
|
+
try {
|
|
864
|
+
await fs3.lstat(current);
|
|
865
|
+
return current;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error.code !== "ENOENT") {
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const parent = path2.dirname(current);
|
|
872
|
+
if (parent === current) {
|
|
873
|
+
return current;
|
|
874
|
+
}
|
|
875
|
+
current = parent;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
879
|
+
if (filePath.length === 0) {
|
|
880
|
+
throw new Error("filePath must not be empty.");
|
|
881
|
+
}
|
|
882
|
+
if (filePath.includes("\0")) {
|
|
883
|
+
throw new Error("filePath must not contain null bytes.");
|
|
884
|
+
}
|
|
885
|
+
const { promises: fs3 } = await import("fs");
|
|
886
|
+
const path2 = await import("path");
|
|
887
|
+
const resolved = path2.resolve(filePath);
|
|
888
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
889
|
+
if (baseDir === false) {
|
|
890
|
+
return resolved;
|
|
891
|
+
}
|
|
892
|
+
await fs3.mkdir(baseDir, { recursive: true });
|
|
893
|
+
const realBaseDir = await fs3.realpath(baseDir);
|
|
894
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
895
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
896
|
+
}
|
|
897
|
+
if (mode === "read") {
|
|
898
|
+
const realTarget = await fs3.realpath(resolved);
|
|
899
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
900
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
901
|
+
}
|
|
902
|
+
return realTarget;
|
|
903
|
+
}
|
|
904
|
+
const parentDir = path2.dirname(resolved);
|
|
905
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
|
|
906
|
+
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
907
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
908
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
909
|
+
}
|
|
910
|
+
await fs3.mkdir(parentDir, { recursive: true });
|
|
911
|
+
const realParentDir = await fs3.realpath(parentDir);
|
|
912
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
913
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
914
|
+
}
|
|
915
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
916
|
+
try {
|
|
917
|
+
const existing = await fs3.lstat(targetPath);
|
|
918
|
+
if (existing.isSymbolicLink()) {
|
|
919
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (error.code !== "ENOENT") {
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return targetPath;
|
|
927
|
+
}
|
|
928
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
929
|
+
if (byteLimit === false) {
|
|
930
|
+
return handle.readFile({ encoding: "utf8" });
|
|
931
|
+
}
|
|
932
|
+
const chunks = [];
|
|
933
|
+
let totalBytes = 0;
|
|
934
|
+
let position = 0;
|
|
935
|
+
while (true) {
|
|
936
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
937
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
938
|
+
if (bytesRead === 0) {
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
totalBytes += bytesRead;
|
|
942
|
+
if (totalBytes > byteLimit) {
|
|
943
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
944
|
+
}
|
|
945
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
946
|
+
position += bytesRead;
|
|
947
|
+
}
|
|
948
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
952
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
953
|
+
var CacheStackSnapshotManager = class {
|
|
954
|
+
constructor(options) {
|
|
955
|
+
this.options = options;
|
|
956
|
+
}
|
|
957
|
+
options;
|
|
958
|
+
async exportState(maxEntries) {
|
|
959
|
+
const entries = [];
|
|
960
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
961
|
+
entries.push(entry);
|
|
962
|
+
});
|
|
963
|
+
return entries;
|
|
964
|
+
}
|
|
965
|
+
async importState(entries) {
|
|
966
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
967
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
968
|
+
value: entry.value,
|
|
969
|
+
ttl: entry.ttl
|
|
970
|
+
}));
|
|
971
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
972
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
973
|
+
await Promise.all(
|
|
974
|
+
batch.map(async (entry) => {
|
|
975
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
976
|
+
await this.options.tagIndex.touch(entry.key);
|
|
977
|
+
})
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
982
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
983
|
+
const tempPath = path.join(
|
|
984
|
+
path.dirname(targetPath),
|
|
985
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
986
|
+
);
|
|
987
|
+
let handle;
|
|
988
|
+
try {
|
|
989
|
+
handle = await fs.open(tempPath, "wx");
|
|
990
|
+
const openedHandle = handle;
|
|
991
|
+
await openedHandle.writeFile("[", "utf8");
|
|
992
|
+
let wroteAny = false;
|
|
993
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
994
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
995
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
996
|
+
wroteAny = true;
|
|
997
|
+
});
|
|
998
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
999
|
+
await openedHandle.close();
|
|
1000
|
+
handle = void 0;
|
|
1001
|
+
await fs.rename(tempPath, targetPath);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
await handle?.close().catch(() => void 0);
|
|
1004
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1009
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1010
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1011
|
+
let raw;
|
|
1012
|
+
try {
|
|
1013
|
+
if (maxBytes !== false) {
|
|
1014
|
+
const stat = await handle.stat();
|
|
1015
|
+
if (stat.size > maxBytes) {
|
|
1016
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1020
|
+
} finally {
|
|
1021
|
+
await handle.close();
|
|
1022
|
+
}
|
|
1023
|
+
let parsed;
|
|
1024
|
+
try {
|
|
1025
|
+
parsed = JSON.parse(raw);
|
|
1026
|
+
} catch (cause) {
|
|
1027
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1028
|
+
}
|
|
1029
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1030
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1031
|
+
}
|
|
1032
|
+
await this.importState(
|
|
1033
|
+
parsed.map((entry) => ({
|
|
1034
|
+
key: entry.key,
|
|
1035
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1036
|
+
ttl: entry.ttl
|
|
1037
|
+
}))
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1041
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1042
|
+
for (const layer of this.options.layers) {
|
|
1043
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const visitKey = async (key) => {
|
|
1047
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1048
|
+
if (exported.has(exportedKey)) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1052
|
+
if (stored === null) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
exported.add(exportedKey);
|
|
1056
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1057
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1058
|
+
}
|
|
1059
|
+
await visitor({
|
|
1060
|
+
key: exportedKey,
|
|
1061
|
+
value: stored,
|
|
1062
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
if (layer.forEachKey) {
|
|
1066
|
+
await layer.forEachKey(visitKey);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const keys = await layer.keys?.();
|
|
1070
|
+
for (const key of keys ?? []) {
|
|
1071
|
+
await visitKey(key);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
isCacheSnapshotEntries(value) {
|
|
1076
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1077
|
+
if (!entry || typeof entry !== "object") {
|
|
1078
|
+
return false;
|
|
704
1079
|
}
|
|
1080
|
+
const candidate = entry;
|
|
1081
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
705
1082
|
});
|
|
706
1083
|
}
|
|
707
|
-
|
|
708
|
-
|
|
1084
|
+
sanitizeSnapshotValue(value) {
|
|
1085
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
709
1086
|
}
|
|
710
1087
|
};
|
|
711
1088
|
|
|
712
|
-
// src/internal/CacheStackRuntimePolicy.ts
|
|
713
|
-
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
714
|
-
return degradedUntil !== void 0 && degradedUntil > now;
|
|
715
|
-
}
|
|
716
|
-
function shouldStartBackgroundRefresh({
|
|
717
|
-
isDisconnecting,
|
|
718
|
-
hasRefreshInFlight
|
|
719
|
-
}) {
|
|
720
|
-
return !isDisconnecting && !hasRefreshInFlight;
|
|
721
|
-
}
|
|
722
|
-
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
723
|
-
if (!gracefulDegradation) {
|
|
724
|
-
return { degrade: false };
|
|
725
|
-
}
|
|
726
|
-
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
727
|
-
return {
|
|
728
|
-
degrade: true,
|
|
729
|
-
degradedUntil: now + retryAfterMs
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
function planFreshReadPolicies({
|
|
733
|
-
stored,
|
|
734
|
-
hasFetcher,
|
|
735
|
-
slidingTtl,
|
|
736
|
-
refreshAheadSeconds
|
|
737
|
-
}) {
|
|
738
|
-
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
739
|
-
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
740
|
-
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
741
|
-
return {
|
|
742
|
-
refreshedStored,
|
|
743
|
-
refreshedStoredTtl,
|
|
744
|
-
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
1089
|
// src/internal/CacheStackValidation.ts
|
|
749
1090
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
750
1091
|
var MAX_PATTERN_LENGTH = 1024;
|
|
@@ -972,7 +1313,11 @@ var FetchRateLimiter = class {
|
|
|
972
1313
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
973
1314
|
nextFetcherBucketId = 0;
|
|
974
1315
|
drainTimer;
|
|
1316
|
+
isDisposed = false;
|
|
975
1317
|
async schedule(options, context, task) {
|
|
1318
|
+
if (this.isDisposed) {
|
|
1319
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1320
|
+
}
|
|
976
1321
|
if (!options) {
|
|
977
1322
|
return task();
|
|
978
1323
|
}
|
|
@@ -995,6 +1340,27 @@ var FetchRateLimiter = class {
|
|
|
995
1340
|
this.drain();
|
|
996
1341
|
});
|
|
997
1342
|
}
|
|
1343
|
+
dispose() {
|
|
1344
|
+
this.isDisposed = true;
|
|
1345
|
+
if (this.drainTimer) {
|
|
1346
|
+
clearTimeout(this.drainTimer);
|
|
1347
|
+
this.drainTimer = void 0;
|
|
1348
|
+
}
|
|
1349
|
+
for (const bucket of this.buckets.values()) {
|
|
1350
|
+
if (bucket.cleanupTimer) {
|
|
1351
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1352
|
+
bucket.cleanupTimer = void 0;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1356
|
+
for (const item of queue) {
|
|
1357
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
this.queuesByBucket.clear();
|
|
1361
|
+
this.pendingBuckets.clear();
|
|
1362
|
+
this.buckets.clear();
|
|
1363
|
+
}
|
|
998
1364
|
normalize(options) {
|
|
999
1365
|
const maxConcurrent = options.maxConcurrent;
|
|
1000
1366
|
const intervalMs = options.intervalMs;
|
|
@@ -1030,6 +1396,9 @@ var FetchRateLimiter = class {
|
|
|
1030
1396
|
return "global";
|
|
1031
1397
|
}
|
|
1032
1398
|
drain() {
|
|
1399
|
+
if (this.isDisposed) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1033
1402
|
if (this.drainTimer) {
|
|
1034
1403
|
clearTimeout(this.drainTimer);
|
|
1035
1404
|
this.drainTimer = void 0;
|
|
@@ -1126,6 +1495,9 @@ var FetchRateLimiter = class {
|
|
|
1126
1495
|
}
|
|
1127
1496
|
}
|
|
1128
1497
|
bucketState(bucketKey) {
|
|
1498
|
+
if (this.isDisposed) {
|
|
1499
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1500
|
+
}
|
|
1129
1501
|
const existing = this.buckets.get(bucketKey);
|
|
1130
1502
|
if (existing) {
|
|
1131
1503
|
return existing;
|
|
@@ -1361,39 +1733,31 @@ var TtlResolver = class {
|
|
|
1361
1733
|
}
|
|
1362
1734
|
};
|
|
1363
1735
|
|
|
1364
|
-
// src/
|
|
1365
|
-
var
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
}
|
|
1371
|
-
deserialize(payload) {
|
|
1372
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1373
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1377
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
1736
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
1737
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1738
|
+
function sanitizeStructuredData(value, options) {
|
|
1739
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1740
|
+
}
|
|
1741
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1378
1742
|
state.count += 1;
|
|
1379
|
-
if (state.count >
|
|
1380
|
-
throw new Error(
|
|
1743
|
+
if (state.count > options.maxNodes) {
|
|
1744
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1381
1745
|
}
|
|
1382
|
-
if (depth >
|
|
1383
|
-
throw new Error(
|
|
1746
|
+
if (depth > options.maxDepth) {
|
|
1747
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1384
1748
|
}
|
|
1385
1749
|
if (Array.isArray(value)) {
|
|
1386
|
-
return value.map((entry) =>
|
|
1750
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1387
1751
|
}
|
|
1388
1752
|
if (!isPlainObject(value)) {
|
|
1389
1753
|
return value;
|
|
1390
1754
|
}
|
|
1391
|
-
const sanitized = {};
|
|
1755
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1392
1756
|
for (const [key, entry] of Object.entries(value)) {
|
|
1393
|
-
if (
|
|
1757
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1394
1758
|
continue;
|
|
1395
1759
|
}
|
|
1396
|
-
sanitized[key] =
|
|
1760
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1397
1761
|
}
|
|
1398
1762
|
return sanitized;
|
|
1399
1763
|
}
|
|
@@ -1401,6 +1765,21 @@ function isPlainObject(value) {
|
|
|
1401
1765
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1402
1766
|
}
|
|
1403
1767
|
|
|
1768
|
+
// src/serialization/JsonSerializer.ts
|
|
1769
|
+
var JsonSerializer = class {
|
|
1770
|
+
serialize(value) {
|
|
1771
|
+
return JSON.stringify(value);
|
|
1772
|
+
}
|
|
1773
|
+
deserialize(payload) {
|
|
1774
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1775
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
1776
|
+
label: "JSON payload",
|
|
1777
|
+
maxDepth: 200,
|
|
1778
|
+
maxNodes: 1e4
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1404
1783
|
// src/stampede/StampedeGuard.ts
|
|
1405
1784
|
import { Mutex as Mutex2 } from "async-mutex";
|
|
1406
1785
|
var StampedeGuard = class {
|
|
@@ -1445,7 +1824,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1445
1824
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1446
1825
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1447
1826
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1448
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1449
1827
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1450
1828
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1451
1829
|
var DebugLogger = class {
|
|
@@ -1502,6 +1880,35 @@ var CacheStack = class extends EventEmitter {
|
|
|
1502
1880
|
await this.handleLayerFailure(layer, operation, error);
|
|
1503
1881
|
}
|
|
1504
1882
|
});
|
|
1883
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
1884
|
+
tagIndex: this.tagIndex,
|
|
1885
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1886
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1887
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
1891
|
+
layers: this.layers,
|
|
1892
|
+
maintenance: this.maintenance,
|
|
1893
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1894
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
1895
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1896
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1897
|
+
},
|
|
1898
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
1899
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
1900
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
1901
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
1902
|
+
globalStaleIfError: this.options.staleIfError,
|
|
1903
|
+
writePolicy: this.options.writePolicy,
|
|
1904
|
+
onWriteFailures: (context, failures) => {
|
|
1905
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1906
|
+
this.logger.debug?.("write-failure", {
|
|
1907
|
+
...context,
|
|
1908
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1505
1912
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1506
1913
|
this.logger.warn?.(
|
|
1507
1914
|
"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."
|
|
@@ -1517,6 +1924,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1517
1924
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1518
1925
|
);
|
|
1519
1926
|
}
|
|
1927
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
1928
|
+
layers: this.layers,
|
|
1929
|
+
tagIndex: this.tagIndex,
|
|
1930
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
1931
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
1932
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
1933
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
1934
|
+
validateCacheKey,
|
|
1935
|
+
formatError: this.formatError.bind(this)
|
|
1936
|
+
});
|
|
1520
1937
|
this.initializeWriteBehind(options.writeBehind);
|
|
1521
1938
|
this.startup = this.initialize();
|
|
1522
1939
|
}
|
|
@@ -1532,11 +1949,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1532
1949
|
keyDiscovery;
|
|
1533
1950
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1534
1951
|
snapshotSerializer = new JsonSerializer();
|
|
1952
|
+
invalidation;
|
|
1953
|
+
layerWriter;
|
|
1954
|
+
snapshots;
|
|
1535
1955
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1536
1956
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1537
1957
|
maintenance = new CacheStackMaintenance();
|
|
1538
1958
|
ttlResolver;
|
|
1539
1959
|
circuitBreakerManager;
|
|
1960
|
+
nextOperationId = 0;
|
|
1540
1961
|
currentGeneration;
|
|
1541
1962
|
isDisconnecting = false;
|
|
1542
1963
|
disconnectPromise;
|
|
@@ -1547,10 +1968,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
1547
1968
|
* and no `fetcher` is provided.
|
|
1548
1969
|
*/
|
|
1549
1970
|
async get(key, fetcher, options) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1971
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
1972
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1973
|
+
this.validateWriteOptions(options);
|
|
1974
|
+
await this.awaitStartup("get");
|
|
1975
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1976
|
+
});
|
|
1554
1977
|
}
|
|
1555
1978
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
1556
1979
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -1672,23 +2095,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
1672
2095
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1673
2096
|
*/
|
|
1674
2097
|
async set(key, value, options) {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2098
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2099
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2100
|
+
this.validateWriteOptions(options);
|
|
2101
|
+
await this.awaitStartup("set");
|
|
2102
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2103
|
+
});
|
|
1679
2104
|
}
|
|
1680
2105
|
/**
|
|
1681
2106
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1682
2107
|
*/
|
|
1683
2108
|
async delete(key) {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2109
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2110
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2111
|
+
await this.awaitStartup("delete");
|
|
2112
|
+
await this.deleteKeys([normalizedKey]);
|
|
2113
|
+
await this.publishInvalidation({
|
|
2114
|
+
scope: "key",
|
|
2115
|
+
keys: [normalizedKey],
|
|
2116
|
+
sourceId: this.instanceId,
|
|
2117
|
+
operation: "delete"
|
|
2118
|
+
});
|
|
1692
2119
|
});
|
|
1693
2120
|
}
|
|
1694
2121
|
async clear() {
|
|
@@ -1721,95 +2148,99 @@ var CacheStack = class extends EventEmitter {
|
|
|
1721
2148
|
});
|
|
1722
2149
|
}
|
|
1723
2150
|
async mget(entries) {
|
|
1724
|
-
this.
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2151
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2152
|
+
this.assertActive("mget");
|
|
2153
|
+
if (entries.length === 0) {
|
|
2154
|
+
return [];
|
|
2155
|
+
}
|
|
2156
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2157
|
+
...entry,
|
|
2158
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2159
|
+
}));
|
|
2160
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2161
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2162
|
+
if (!canFastPath) {
|
|
2163
|
+
await this.awaitStartup("mget");
|
|
2164
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2165
|
+
return Promise.all(
|
|
2166
|
+
normalizedEntries.map((entry) => {
|
|
2167
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2168
|
+
const existing = pendingReads.get(entry.key);
|
|
2169
|
+
if (!existing) {
|
|
2170
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2171
|
+
pendingReads.set(entry.key, {
|
|
2172
|
+
promise,
|
|
2173
|
+
fetch: entry.fetch,
|
|
2174
|
+
optionsSignature
|
|
2175
|
+
});
|
|
2176
|
+
return promise;
|
|
2177
|
+
}
|
|
2178
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2179
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2180
|
+
}
|
|
2181
|
+
return existing.promise;
|
|
2182
|
+
})
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
1735
2185
|
await this.awaitStartup("mget");
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
});
|
|
1748
|
-
return promise;
|
|
1749
|
-
}
|
|
1750
|
-
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
1751
|
-
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
1752
|
-
}
|
|
1753
|
-
return existing.promise;
|
|
1754
|
-
})
|
|
1755
|
-
);
|
|
1756
|
-
}
|
|
1757
|
-
await this.awaitStartup("mget");
|
|
1758
|
-
const pending = /* @__PURE__ */ new Set();
|
|
1759
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1760
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
1761
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
1762
|
-
const entry = normalizedEntries[index];
|
|
1763
|
-
if (!entry) continue;
|
|
1764
|
-
const key = entry.key;
|
|
1765
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
1766
|
-
indexes.push(index);
|
|
1767
|
-
indexesByKey.set(key, indexes);
|
|
1768
|
-
pending.add(key);
|
|
1769
|
-
}
|
|
1770
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
1771
|
-
const layer = this.layers[layerIndex];
|
|
1772
|
-
if (!layer) continue;
|
|
1773
|
-
const keys = [...pending];
|
|
1774
|
-
if (keys.length === 0) {
|
|
1775
|
-
break;
|
|
2186
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2187
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2188
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2189
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2190
|
+
const entry = normalizedEntries[index];
|
|
2191
|
+
if (!entry) continue;
|
|
2192
|
+
const key = entry.key;
|
|
2193
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2194
|
+
indexes.push(index);
|
|
2195
|
+
indexesByKey.set(key, indexes);
|
|
2196
|
+
pending.add(key);
|
|
1776
2197
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
const
|
|
1781
|
-
if (
|
|
1782
|
-
|
|
2198
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2199
|
+
const layer = this.layers[layerIndex];
|
|
2200
|
+
if (!layer) continue;
|
|
2201
|
+
const keys = [...pending];
|
|
2202
|
+
if (keys.length === 0) {
|
|
2203
|
+
break;
|
|
1783
2204
|
}
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2205
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2206
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2207
|
+
const key = keys[offset];
|
|
2208
|
+
const stored = values[offset];
|
|
2209
|
+
if (!key || stored === null) {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
const resolved = resolveStoredValue(stored);
|
|
2213
|
+
if (resolved.state === "expired") {
|
|
2214
|
+
await layer.delete(key);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
await this.tagIndex.touch(key);
|
|
2218
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2219
|
+
resultsByKey.set(key, resolved.value);
|
|
2220
|
+
pending.delete(key);
|
|
2221
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
1788
2222
|
}
|
|
1789
|
-
await this.tagIndex.touch(key);
|
|
1790
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
1791
|
-
resultsByKey.set(key, resolved.value);
|
|
1792
|
-
pending.delete(key);
|
|
1793
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
1794
2223
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2224
|
+
if (pending.size > 0) {
|
|
2225
|
+
for (const key of pending) {
|
|
2226
|
+
await this.tagIndex.remove(key);
|
|
2227
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2228
|
+
}
|
|
1800
2229
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
2230
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2231
|
+
});
|
|
1803
2232
|
}
|
|
1804
2233
|
async mset(entries) {
|
|
1805
|
-
this.
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2234
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2235
|
+
this.assertActive("mset");
|
|
2236
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2237
|
+
...entry,
|
|
2238
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2239
|
+
}));
|
|
2240
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2241
|
+
await this.awaitStartup("mset");
|
|
2242
|
+
await this.writeBatch(normalizedEntries);
|
|
2243
|
+
});
|
|
1813
2244
|
}
|
|
1814
2245
|
async warm(entries, options = {}) {
|
|
1815
2246
|
this.assertActive("warm");
|
|
@@ -1862,40 +2293,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
1862
2293
|
return new CacheNamespace(this, prefix);
|
|
1863
2294
|
}
|
|
1864
2295
|
async invalidateByTag(tag) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2296
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2297
|
+
validateTag(tag);
|
|
2298
|
+
await this.awaitStartup("invalidateByTag");
|
|
2299
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2300
|
+
await this.deleteKeys(keys);
|
|
2301
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2302
|
+
});
|
|
1870
2303
|
}
|
|
1871
2304
|
async invalidateByTags(tags, mode = "any") {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2305
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2306
|
+
if (tags.length === 0) {
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
validateTags(tags);
|
|
2310
|
+
await this.awaitStartup("invalidateByTags");
|
|
2311
|
+
const keysByTag = await Promise.all(
|
|
2312
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2313
|
+
);
|
|
2314
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2315
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2316
|
+
await this.deleteKeys(keys);
|
|
2317
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2318
|
+
});
|
|
1882
2319
|
}
|
|
1883
2320
|
async invalidateByPattern(pattern) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
this.
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2321
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2322
|
+
validatePattern(pattern);
|
|
2323
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2324
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2325
|
+
this.qualifyPattern(pattern),
|
|
2326
|
+
this.invalidationMaxKeys()
|
|
2327
|
+
);
|
|
2328
|
+
await this.deleteKeys(keys);
|
|
2329
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2330
|
+
});
|
|
1892
2331
|
}
|
|
1893
2332
|
async invalidateByPrefix(prefix) {
|
|
1894
|
-
await this.
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2333
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2334
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2335
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2336
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2337
|
+
await this.deleteKeys(keys);
|
|
2338
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2339
|
+
});
|
|
1899
2340
|
}
|
|
1900
2341
|
getMetrics() {
|
|
1901
2342
|
return this.metricsCollector.snapshot;
|
|
@@ -1996,105 +2437,29 @@ var CacheStack = class extends EventEmitter {
|
|
|
1996
2437
|
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1997
2438
|
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1998
2439
|
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
if (foundInLayers.length === 0) {
|
|
2002
|
-
return null;
|
|
2003
|
-
}
|
|
2004
|
-
const tags = await this.getTagsForKey(normalizedKey);
|
|
2005
|
-
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
2006
|
-
}
|
|
2007
|
-
async exportState() {
|
|
2008
|
-
await this.awaitStartup("exportState");
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
}
|
|
2015
|
-
async
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2023
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2024
|
-
await Promise.all(
|
|
2025
|
-
batch.map(async (entry) => {
|
|
2026
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2027
|
-
await this.tagIndex.touch(entry.key);
|
|
2028
|
-
})
|
|
2029
|
-
);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
async persistToFile(filePath) {
|
|
2033
|
-
this.assertActive("persistToFile");
|
|
2034
|
-
const { promises: fs2 } = await import("fs");
|
|
2035
|
-
const path = await import("path");
|
|
2036
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2037
|
-
const tempPath = path.join(
|
|
2038
|
-
path.dirname(targetPath),
|
|
2039
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2040
|
-
);
|
|
2041
|
-
let handle;
|
|
2042
|
-
try {
|
|
2043
|
-
handle = await fs2.open(tempPath, "wx");
|
|
2044
|
-
const openedHandle = handle;
|
|
2045
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2046
|
-
let wroteAny = false;
|
|
2047
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2048
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2049
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2050
|
-
wroteAny = true;
|
|
2051
|
-
});
|
|
2052
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2053
|
-
await openedHandle.close();
|
|
2054
|
-
handle = void 0;
|
|
2055
|
-
await fs2.rename(tempPath, targetPath);
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
await handle?.close().catch(() => void 0);
|
|
2058
|
-
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2059
|
-
throw error;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
async restoreFromFile(filePath) {
|
|
2063
|
-
this.assertActive("restoreFromFile");
|
|
2064
|
-
const { promises: fs2, constants } = await import("fs");
|
|
2065
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2066
|
-
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2067
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2068
|
-
let raw;
|
|
2069
|
-
try {
|
|
2070
|
-
if (snapshotMaxBytes !== false) {
|
|
2071
|
-
const stat = await handle.stat();
|
|
2072
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2073
|
-
throw new Error(
|
|
2074
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2075
|
-
);
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2079
|
-
} finally {
|
|
2080
|
-
await handle.close();
|
|
2081
|
-
}
|
|
2082
|
-
let parsed;
|
|
2083
|
-
try {
|
|
2084
|
-
parsed = JSON.parse(raw);
|
|
2085
|
-
} catch (cause) {
|
|
2086
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2087
|
-
}
|
|
2088
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2089
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2090
|
-
}
|
|
2091
|
-
await this.importState(
|
|
2092
|
-
parsed.map((entry) => ({
|
|
2093
|
-
key: entry.key,
|
|
2094
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2095
|
-
ttl: entry.ttl
|
|
2096
|
-
}))
|
|
2097
|
-
);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (foundInLayers.length === 0) {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
2446
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
2447
|
+
}
|
|
2448
|
+
async exportState() {
|
|
2449
|
+
await this.awaitStartup("exportState");
|
|
2450
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2451
|
+
}
|
|
2452
|
+
async importState(entries) {
|
|
2453
|
+
await this.awaitStartup("importState");
|
|
2454
|
+
await this.snapshots.importState(entries);
|
|
2455
|
+
}
|
|
2456
|
+
async persistToFile(filePath) {
|
|
2457
|
+
this.assertActive("persistToFile");
|
|
2458
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2459
|
+
}
|
|
2460
|
+
async restoreFromFile(filePath) {
|
|
2461
|
+
this.assertActive("restoreFromFile");
|
|
2462
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2098
2463
|
}
|
|
2099
2464
|
async disconnect() {
|
|
2100
2465
|
if (!this.disconnectPromise) {
|
|
@@ -2106,6 +2471,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2106
2471
|
await this.maintenance.waitForGenerationCleanup();
|
|
2107
2472
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2108
2473
|
this.maintenance.disposeWriteBehindTimer();
|
|
2474
|
+
this.fetchRateLimiter.dispose();
|
|
2109
2475
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2110
2476
|
})();
|
|
2111
2477
|
}
|
|
@@ -2219,7 +2585,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2219
2585
|
async storeEntry(key, kind, value, options) {
|
|
2220
2586
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2221
2587
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2222
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
2588
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2223
2589
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2224
2590
|
return;
|
|
2225
2591
|
}
|
|
@@ -2236,52 +2602,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2236
2602
|
}
|
|
2237
2603
|
}
|
|
2238
2604
|
async writeBatch(entries) {
|
|
2239
|
-
const
|
|
2240
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2241
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2242
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2243
|
-
const immediateOperations = [];
|
|
2244
|
-
const deferredOperations = [];
|
|
2245
|
-
for (const entry of entries) {
|
|
2246
|
-
for (const layer of this.layers) {
|
|
2247
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2248
|
-
continue;
|
|
2249
|
-
}
|
|
2250
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2251
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2252
|
-
bucket.push(layerEntry);
|
|
2253
|
-
entriesByLayer.set(layer, bucket);
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2257
|
-
const operation = async () => {
|
|
2258
|
-
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
const activeEntries = layerEntries.filter(
|
|
2262
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2263
|
-
);
|
|
2264
|
-
if (activeEntries.length === 0) {
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
try {
|
|
2268
|
-
if (layer.setMany) {
|
|
2269
|
-
await layer.setMany(activeEntries);
|
|
2270
|
-
return;
|
|
2271
|
-
}
|
|
2272
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2273
|
-
} catch (error) {
|
|
2274
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2275
|
-
}
|
|
2276
|
-
};
|
|
2277
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2278
|
-
deferredOperations.push(operation);
|
|
2279
|
-
} else {
|
|
2280
|
-
immediateOperations.push(operation);
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2284
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2605
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2285
2606
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2286
2607
|
return;
|
|
2287
2608
|
}
|
|
@@ -2388,58 +2709,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2388
2709
|
this.emit("backfill", { key, layer: layer.name });
|
|
2389
2710
|
}
|
|
2390
2711
|
}
|
|
2391
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2392
|
-
const now = Date.now();
|
|
2393
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2394
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2395
|
-
const immediateOperations = [];
|
|
2396
|
-
const deferredOperations = [];
|
|
2397
|
-
for (const layer of this.layers) {
|
|
2398
|
-
const operation = async () => {
|
|
2399
|
-
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2406
|
-
try {
|
|
2407
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2408
|
-
} catch (error) {
|
|
2409
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2410
|
-
}
|
|
2411
|
-
};
|
|
2412
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2413
|
-
deferredOperations.push(operation);
|
|
2414
|
-
} else {
|
|
2415
|
-
immediateOperations.push(operation);
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2419
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2420
|
-
}
|
|
2421
|
-
async executeLayerOperations(operations, context) {
|
|
2422
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2423
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2424
|
-
return;
|
|
2425
|
-
}
|
|
2426
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
2427
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2428
|
-
if (failures.length === 0) {
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2432
|
-
this.logger.debug?.("write-failure", {
|
|
2433
|
-
...context,
|
|
2434
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
2435
|
-
});
|
|
2436
|
-
if (failures.length === operations.length) {
|
|
2437
|
-
throw new AggregateError(
|
|
2438
|
-
failures.map((failure) => failure.reason),
|
|
2439
|
-
`${context.action} failed for every cache layer`
|
|
2440
|
-
);
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
2712
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2444
2713
|
return this.ttlResolver.resolveFreshTtl(
|
|
2445
2714
|
key,
|
|
@@ -2505,7 +2774,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2505
2774
|
return;
|
|
2506
2775
|
}
|
|
2507
2776
|
this.maintenance.bumpKeyEpochs(keys);
|
|
2508
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2777
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
2509
2778
|
for (const key of keys) {
|
|
2510
2779
|
await this.tagIndex.remove(key);
|
|
2511
2780
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -2537,7 +2806,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2537
2806
|
}
|
|
2538
2807
|
const keys = message.keys ?? [];
|
|
2539
2808
|
this.maintenance.bumpKeyEpochs(keys);
|
|
2540
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2809
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
2541
2810
|
if (message.operation !== "write") {
|
|
2542
2811
|
for (const key of keys) {
|
|
2543
2812
|
await this.tagIndex.remove(key);
|
|
@@ -2594,6 +2863,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
2594
2863
|
shouldBroadcastL1Invalidation() {
|
|
2595
2864
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2596
2865
|
}
|
|
2866
|
+
async observeOperation(name, attributes, execute) {
|
|
2867
|
+
const id = this.nextOperationId;
|
|
2868
|
+
this.nextOperationId += 1;
|
|
2869
|
+
this.emit("operation-start", { id, name, attributes });
|
|
2870
|
+
try {
|
|
2871
|
+
const result = await execute();
|
|
2872
|
+
this.emit("operation-end", {
|
|
2873
|
+
id,
|
|
2874
|
+
name,
|
|
2875
|
+
attributes,
|
|
2876
|
+
success: true,
|
|
2877
|
+
result: result === null ? "null" : void 0
|
|
2878
|
+
});
|
|
2879
|
+
return result;
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
this.emit("operation-end", {
|
|
2882
|
+
id,
|
|
2883
|
+
name,
|
|
2884
|
+
attributes,
|
|
2885
|
+
success: false,
|
|
2886
|
+
error
|
|
2887
|
+
});
|
|
2888
|
+
throw error;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2597
2891
|
scheduleGenerationCleanup(generation) {
|
|
2598
2892
|
this.maintenance.scheduleGenerationCleanup(
|
|
2599
2893
|
generation,
|
|
@@ -2649,37 +2943,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2649
2943
|
});
|
|
2650
2944
|
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2651
2945
|
}
|
|
2652
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2653
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2654
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2655
|
-
layer.name,
|
|
2656
|
-
options?.staleWhileRevalidate,
|
|
2657
|
-
this.options.staleWhileRevalidate
|
|
2658
|
-
);
|
|
2659
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2660
|
-
const payload = createStoredValueEnvelope({
|
|
2661
|
-
kind,
|
|
2662
|
-
value,
|
|
2663
|
-
freshTtlSeconds: freshTtl,
|
|
2664
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2665
|
-
staleIfErrorSeconds: staleIfError,
|
|
2666
|
-
now
|
|
2667
|
-
});
|
|
2668
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
2669
|
-
return {
|
|
2670
|
-
key,
|
|
2671
|
-
value: payload,
|
|
2672
|
-
ttl
|
|
2673
|
-
};
|
|
2674
|
-
}
|
|
2675
|
-
intersectKeys(groups) {
|
|
2676
|
-
if (groups.length === 0) {
|
|
2677
|
-
return [];
|
|
2678
|
-
}
|
|
2679
|
-
const [firstGroup, ...rest] = groups;
|
|
2680
|
-
const restSets = rest.map((group) => new Set(group));
|
|
2681
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2682
|
-
}
|
|
2683
2946
|
qualifyKey(key) {
|
|
2684
2947
|
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2685
2948
|
}
|
|
@@ -2689,32 +2952,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2689
2952
|
stripQualifiedKey(key) {
|
|
2690
2953
|
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2691
2954
|
}
|
|
2692
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
2693
|
-
await Promise.all(
|
|
2694
|
-
layers.map(async (layer) => {
|
|
2695
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2696
|
-
return;
|
|
2697
|
-
}
|
|
2698
|
-
if (layer.deleteMany) {
|
|
2699
|
-
try {
|
|
2700
|
-
await layer.deleteMany(keys);
|
|
2701
|
-
} catch (error) {
|
|
2702
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
2703
|
-
}
|
|
2704
|
-
return;
|
|
2705
|
-
}
|
|
2706
|
-
await Promise.all(
|
|
2707
|
-
keys.map(async (key) => {
|
|
2708
|
-
try {
|
|
2709
|
-
await layer.delete(key);
|
|
2710
|
-
} catch (error) {
|
|
2711
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
2712
|
-
}
|
|
2713
|
-
})
|
|
2714
|
-
);
|
|
2715
|
-
})
|
|
2716
|
-
);
|
|
2717
|
-
}
|
|
2718
2955
|
validateConfiguration() {
|
|
2719
2956
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
2720
2957
|
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
@@ -2845,18 +3082,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2845
3082
|
this.emit("error", { operation, ...context });
|
|
2846
3083
|
}
|
|
2847
3084
|
}
|
|
2848
|
-
isCacheSnapshotEntries(value) {
|
|
2849
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
2850
|
-
if (!entry || typeof entry !== "object") {
|
|
2851
|
-
return false;
|
|
2852
|
-
}
|
|
2853
|
-
const candidate = entry;
|
|
2854
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
sanitizeSnapshotValue(value) {
|
|
2858
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2859
|
-
}
|
|
2860
3085
|
snapshotMaxBytes() {
|
|
2861
3086
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
2862
3087
|
}
|
|
@@ -2866,62 +3091,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2866
3091
|
invalidationMaxKeys() {
|
|
2867
3092
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
2868
3093
|
}
|
|
2869
|
-
async collectKeysForTag(tag) {
|
|
2870
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2871
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
2872
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
2873
|
-
keys.add(key);
|
|
2874
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2875
|
-
});
|
|
2876
|
-
return [...keys];
|
|
2877
|
-
}
|
|
2878
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
2879
|
-
keys.add(key);
|
|
2880
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2881
|
-
}
|
|
2882
|
-
return [...keys];
|
|
2883
|
-
}
|
|
2884
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
2885
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
2886
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
2887
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
2891
|
-
const exported = /* @__PURE__ */ new Set();
|
|
2892
|
-
for (const layer of this.layers) {
|
|
2893
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
2894
|
-
continue;
|
|
2895
|
-
}
|
|
2896
|
-
const visitKey = async (key) => {
|
|
2897
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2898
|
-
if (exported.has(exportedKey)) {
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2902
|
-
if (stored === null) {
|
|
2903
|
-
return;
|
|
2904
|
-
}
|
|
2905
|
-
exported.add(exportedKey);
|
|
2906
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
2907
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
2908
|
-
}
|
|
2909
|
-
await visitor({
|
|
2910
|
-
key: exportedKey,
|
|
2911
|
-
value: stored,
|
|
2912
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2913
|
-
});
|
|
2914
|
-
};
|
|
2915
|
-
if (layer.forEachKey) {
|
|
2916
|
-
await layer.forEachKey(visitKey);
|
|
2917
|
-
continue;
|
|
2918
|
-
}
|
|
2919
|
-
const keys = await layer.keys?.();
|
|
2920
|
-
for (const key of keys ?? []) {
|
|
2921
|
-
await visitKey(key);
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
3094
|
};
|
|
2926
3095
|
|
|
2927
3096
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -2963,7 +3132,12 @@ var RedisInvalidationBus = class {
|
|
|
2963
3132
|
async dispatchToHandlers(payload) {
|
|
2964
3133
|
let message;
|
|
2965
3134
|
try {
|
|
2966
|
-
const parsed =
|
|
3135
|
+
const parsed = sanitizeStructuredData(JSON.parse(payload), {
|
|
3136
|
+
label: "Invalidation payload",
|
|
3137
|
+
maxDepth: 64,
|
|
3138
|
+
maxNodes: 1e4,
|
|
3139
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3140
|
+
});
|
|
2967
3141
|
if (!this.isInvalidationMessage(parsed)) {
|
|
2968
3142
|
throw new Error("Invalid invalidation payload shape.");
|
|
2969
3143
|
}
|
|
@@ -3000,31 +3174,6 @@ var RedisInvalidationBus = class {
|
|
|
3000
3174
|
console.error(`[layercache] ${message}`, error);
|
|
3001
3175
|
}
|
|
3002
3176
|
};
|
|
3003
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3004
|
-
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3005
|
-
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3006
|
-
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3007
|
-
state.count += 1;
|
|
3008
|
-
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3009
|
-
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3010
|
-
}
|
|
3011
|
-
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3012
|
-
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3013
|
-
}
|
|
3014
|
-
if (Array.isArray(value)) {
|
|
3015
|
-
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
3016
|
-
}
|
|
3017
|
-
if (value && typeof value === "object") {
|
|
3018
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
3019
|
-
for (const key of Object.keys(value)) {
|
|
3020
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
3021
|
-
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
return result;
|
|
3025
|
-
}
|
|
3026
|
-
return value;
|
|
3027
|
-
}
|
|
3028
3177
|
|
|
3029
3178
|
// src/http/createCacheStatsHandler.ts
|
|
3030
3179
|
function createCacheStatsHandler(cache, options = {}) {
|
|
@@ -3169,64 +3318,37 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
3169
3318
|
|
|
3170
3319
|
// src/integrations/opentelemetry.ts
|
|
3171
3320
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
set:
|
|
3175
|
-
delete: cache.delete.bind(cache),
|
|
3176
|
-
mget: cache.mget.bind(cache),
|
|
3177
|
-
mset: cache.mset.bind(cache),
|
|
3178
|
-
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
3179
|
-
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
3180
|
-
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
3181
|
-
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
3321
|
+
const spans = /* @__PURE__ */ new Map();
|
|
3322
|
+
const onStart = (event) => {
|
|
3323
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
3182
3324
|
};
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
"layercache.key": String(args[0] ?? "")
|
|
3188
|
-
}));
|
|
3189
|
-
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
3190
|
-
"layercache.key": String(args[0] ?? "")
|
|
3191
|
-
}));
|
|
3192
|
-
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
3193
|
-
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
3194
|
-
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
3195
|
-
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
3196
|
-
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
3197
|
-
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
3198
|
-
return {
|
|
3199
|
-
uninstall() {
|
|
3200
|
-
cache.get = originals.get;
|
|
3201
|
-
cache.set = originals.set;
|
|
3202
|
-
cache.delete = originals.delete;
|
|
3203
|
-
cache.mget = originals.mget;
|
|
3204
|
-
cache.mset = originals.mset;
|
|
3205
|
-
cache.invalidateByTag = originals.invalidateByTag;
|
|
3206
|
-
cache.invalidateByTags = originals.invalidateByTags;
|
|
3207
|
-
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
3208
|
-
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
3325
|
+
const onEnd = (event) => {
|
|
3326
|
+
const span = spans.get(event.id);
|
|
3327
|
+
if (!span) {
|
|
3328
|
+
return;
|
|
3209
3329
|
}
|
|
3330
|
+
spans.delete(event.id);
|
|
3331
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
3332
|
+
if (event.result) {
|
|
3333
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3334
|
+
}
|
|
3335
|
+
if (event.error !== void 0) {
|
|
3336
|
+
span.recordException?.(event.error);
|
|
3337
|
+
}
|
|
3338
|
+
span.end();
|
|
3210
3339
|
};
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
return
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
span.
|
|
3218
|
-
|
|
3219
|
-
span.setAttribute?.("layercache.result", "null");
|
|
3340
|
+
cache.on("operation-start", onStart);
|
|
3341
|
+
cache.on("operation-end", onEnd);
|
|
3342
|
+
return {
|
|
3343
|
+
uninstall() {
|
|
3344
|
+
cache.off("operation-start", onStart);
|
|
3345
|
+
cache.off("operation-end", onEnd);
|
|
3346
|
+
for (const span of spans.values()) {
|
|
3347
|
+
span.end();
|
|
3220
3348
|
}
|
|
3221
|
-
|
|
3222
|
-
} catch (error) {
|
|
3223
|
-
span.setAttribute?.("layercache.success", false);
|
|
3224
|
-
span.recordException?.(error);
|
|
3225
|
-
throw error;
|
|
3226
|
-
} finally {
|
|
3227
|
-
span.end();
|
|
3349
|
+
spans.clear();
|
|
3228
3350
|
}
|
|
3229
|
-
}
|
|
3351
|
+
};
|
|
3230
3352
|
}
|
|
3231
3353
|
|
|
3232
3354
|
// src/integrations/trpc.ts
|
|
@@ -3590,7 +3712,7 @@ var RedisLayer = class {
|
|
|
3590
3712
|
|
|
3591
3713
|
// src/layers/DiskLayer.ts
|
|
3592
3714
|
import { createHash } from "crypto";
|
|
3593
|
-
import { promises as
|
|
3715
|
+
import { promises as fs2 } from "fs";
|
|
3594
3716
|
import { join, resolve } from "path";
|
|
3595
3717
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
3596
3718
|
var DiskLayer = class {
|
|
@@ -3634,7 +3756,7 @@ var DiskLayer = class {
|
|
|
3634
3756
|
}
|
|
3635
3757
|
async set(key, value, ttl = this.defaultTtl) {
|
|
3636
3758
|
await this.enqueueWrite(async () => {
|
|
3637
|
-
await
|
|
3759
|
+
await fs2.mkdir(this.directory, { recursive: true });
|
|
3638
3760
|
const entry = {
|
|
3639
3761
|
key,
|
|
3640
3762
|
value,
|
|
@@ -3644,8 +3766,8 @@ var DiskLayer = class {
|
|
|
3644
3766
|
const targetPath = this.keyToPath(key);
|
|
3645
3767
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3646
3768
|
try {
|
|
3647
|
-
await
|
|
3648
|
-
await
|
|
3769
|
+
await fs2.writeFile(tempPath, payload);
|
|
3770
|
+
await fs2.rename(tempPath, targetPath);
|
|
3649
3771
|
} catch (error) {
|
|
3650
3772
|
await this.safeDelete(tempPath);
|
|
3651
3773
|
throw error;
|
|
@@ -3699,7 +3821,7 @@ var DiskLayer = class {
|
|
|
3699
3821
|
await this.enqueueWrite(async () => {
|
|
3700
3822
|
let entries;
|
|
3701
3823
|
try {
|
|
3702
|
-
entries = await
|
|
3824
|
+
entries = await fs2.readdir(this.directory);
|
|
3703
3825
|
} catch {
|
|
3704
3826
|
return;
|
|
3705
3827
|
}
|
|
@@ -3733,7 +3855,7 @@ var DiskLayer = class {
|
|
|
3733
3855
|
}
|
|
3734
3856
|
async ping() {
|
|
3735
3857
|
try {
|
|
3736
|
-
await
|
|
3858
|
+
await fs2.mkdir(this.directory, { recursive: true });
|
|
3737
3859
|
return true;
|
|
3738
3860
|
} catch {
|
|
3739
3861
|
return false;
|
|
@@ -3776,7 +3898,7 @@ var DiskLayer = class {
|
|
|
3776
3898
|
async readEntryFile(filePath) {
|
|
3777
3899
|
let handle;
|
|
3778
3900
|
try {
|
|
3779
|
-
handle = await
|
|
3901
|
+
handle = await fs2.open(filePath, "r");
|
|
3780
3902
|
return await this.readHandleWithLimit(handle);
|
|
3781
3903
|
} catch {
|
|
3782
3904
|
await this.safeDelete(filePath);
|
|
@@ -3816,7 +3938,7 @@ var DiskLayer = class {
|
|
|
3816
3938
|
async scanEntries(visitor) {
|
|
3817
3939
|
let entries;
|
|
3818
3940
|
try {
|
|
3819
|
-
entries = await
|
|
3941
|
+
entries = await fs2.readdir(this.directory);
|
|
3820
3942
|
} catch {
|
|
3821
3943
|
return;
|
|
3822
3944
|
}
|
|
@@ -3879,7 +4001,7 @@ var DiskLayer = class {
|
|
|
3879
4001
|
}
|
|
3880
4002
|
async safeDelete(filePath) {
|
|
3881
4003
|
try {
|
|
3882
|
-
await
|
|
4004
|
+
await fs2.unlink(filePath);
|
|
3883
4005
|
} catch {
|
|
3884
4006
|
}
|
|
3885
4007
|
}
|
|
@@ -3897,7 +4019,7 @@ var DiskLayer = class {
|
|
|
3897
4019
|
}
|
|
3898
4020
|
let entries;
|
|
3899
4021
|
try {
|
|
3900
|
-
entries = await
|
|
4022
|
+
entries = await fs2.readdir(this.directory);
|
|
3901
4023
|
} catch {
|
|
3902
4024
|
return;
|
|
3903
4025
|
}
|
|
@@ -3909,7 +4031,7 @@ var DiskLayer = class {
|
|
|
3909
4031
|
lcFiles.map(async (name) => {
|
|
3910
4032
|
const filePath = join(this.directory, name);
|
|
3911
4033
|
try {
|
|
3912
|
-
const stat = await
|
|
4034
|
+
const stat = await fs2.stat(filePath);
|
|
3913
4035
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
3914
4036
|
} catch {
|
|
3915
4037
|
return { filePath, mtimeMs: 0 };
|
|
@@ -4005,44 +4127,19 @@ var MemcachedLayer = class {
|
|
|
4005
4127
|
|
|
4006
4128
|
// src/serialization/MsgpackSerializer.ts
|
|
4007
4129
|
import { decode, encode } from "@msgpack/msgpack";
|
|
4008
|
-
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4009
|
-
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4010
|
-
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4011
4130
|
var MsgpackSerializer = class {
|
|
4012
4131
|
serialize(value) {
|
|
4013
4132
|
return Buffer.from(encode(value));
|
|
4014
4133
|
}
|
|
4015
4134
|
deserialize(payload) {
|
|
4016
4135
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4017
|
-
return
|
|
4136
|
+
return sanitizeStructuredData(decode(normalized), {
|
|
4137
|
+
label: "MessagePack payload",
|
|
4138
|
+
maxDepth: 64,
|
|
4139
|
+
maxNodes: 1e4
|
|
4140
|
+
});
|
|
4018
4141
|
}
|
|
4019
4142
|
};
|
|
4020
|
-
function sanitizeMsgpackValue(value, depth, state) {
|
|
4021
|
-
state.count += 1;
|
|
4022
|
-
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4023
|
-
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4024
|
-
}
|
|
4025
|
-
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4026
|
-
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4027
|
-
}
|
|
4028
|
-
if (Array.isArray(value)) {
|
|
4029
|
-
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4030
|
-
}
|
|
4031
|
-
if (!isPlainObject2(value)) {
|
|
4032
|
-
return value;
|
|
4033
|
-
}
|
|
4034
|
-
const sanitized = {};
|
|
4035
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
4036
|
-
if (DANGEROUS_KEYS2.has(key)) {
|
|
4037
|
-
continue;
|
|
4038
|
-
}
|
|
4039
|
-
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4040
|
-
}
|
|
4041
|
-
return sanitized;
|
|
4042
|
-
}
|
|
4043
|
-
function isPlainObject2(value) {
|
|
4044
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
4045
|
-
}
|
|
4046
4143
|
|
|
4047
4144
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
4048
4145
|
import { randomUUID } from "crypto";
|