mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Record one probe for two reads of a file that saw the same content
A read within IDENTITY_SETTLE_MS of a file's last write records no identity, and a later read of the same bytes in the same frontend does. FrontendInputTracker.record compared the two probes as JSON and marked the snapshot unstable. A build or coverage run that read its entry before and after that point then published no cache entry: the exact repeat of a build lowered again, and a coverage verdict was not replayed. Two file probes with the same digest now merge into one that keeps the settled identity; any other difference still marks the snapshot unstable.
This commit is contained in:
@@ -316,3 +316,41 @@ test("a file changed shortly before the read is hashed on every check", async ()
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
expect(reads.mock.calls.filter(([path]) => path === source)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("a read before and a read after a file settles record one probe", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const tracker = new FrontendInputTracker();
|
||||
// The first read comes right after the write, the second once the file
|
||||
// has been unchanged long enough for its identity to stand for it.
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
vi.setSystemTime(Date.now() + 5000);
|
||||
try {
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
const snapshot = tracker.snapshot();
|
||||
expect(snapshot.stable).toBe(true);
|
||||
const probe = snapshot.probes.find((entry) => entry.op === "file");
|
||||
expect(probe?.op === "file" && probe.identity !== undefined).toBe(true);
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(true);
|
||||
// The identity still invalidates the snapshot on any write.
|
||||
await writeFile(source, "export const answer = 2;\n");
|
||||
expect(frontendInputsStillMatch(snapshot)).toBe(false);
|
||||
});
|
||||
|
||||
test("two reads of a file that saw different content prevent cache publication", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
|
||||
scratch.push(dir);
|
||||
const source = join(dir, "entry.ts");
|
||||
await writeFile(source, "export const answer = 1;\n");
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
await writeFile(source, "export const answer = 2;\n");
|
||||
tracker.run(() => trackedReadFile(source));
|
||||
expect(tracker.snapshot().stable).toBe(false);
|
||||
});
|
||||
|
||||
@@ -84,6 +84,22 @@ function readWithIdentity(path: string): { text: string; identity: string | null
|
||||
return { text, identity: settled && after === identity ? identity : null };
|
||||
}
|
||||
|
||||
/** Two reads of one file that saw the same content, as one probe: a read
|
||||
* before the file had been unchanged for IDENTITY_SETTLE_MS records no
|
||||
* identity, and a later read of the same bytes does. The probe keeps the
|
||||
* identity, which the settled read proved for that content. Null when the
|
||||
* reads differ in anything else. */
|
||||
function sameFileContent(
|
||||
previous: FrontendInputProbe,
|
||||
probe: FrontendInputProbe,
|
||||
): FrontendInputProbe | null {
|
||||
if (previous.op !== "file" || probe.op !== "file" || previous.digest !== probe.digest)
|
||||
return null;
|
||||
if (previous.identity === undefined) return probe;
|
||||
if (probe.identity === undefined || probe.identity === previous.identity) return previous;
|
||||
return null;
|
||||
}
|
||||
|
||||
function pathKind(path: string): Extract<FrontendInputProbe, { op: "kind" }>["kind"] {
|
||||
try {
|
||||
const info = statSync(path);
|
||||
@@ -142,12 +158,17 @@ export class FrontendInputTracker {
|
||||
const key = `${probe.op}\0${probe.path}`;
|
||||
if (probe.op === "text" && this.probes.has(key)) return;
|
||||
const previous = this.probes.get(key);
|
||||
if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) {
|
||||
const merged = previous === undefined ? null : sameFileContent(previous, probe);
|
||||
if (
|
||||
merged === null &&
|
||||
previous !== undefined &&
|
||||
JSON.stringify(previous) !== JSON.stringify(probe)
|
||||
) {
|
||||
// An input changed while this frontend was running. The build may
|
||||
// still finish normally, but it is not safe to publish as a cache key.
|
||||
this.stable = false;
|
||||
}
|
||||
this.probes.set(key, probe);
|
||||
this.probes.set(key, merged ?? probe);
|
||||
if (probe.op === "file") {
|
||||
// A successful content read supersedes earlier existence probes for the
|
||||
// same path only when they also observed a file. A missing/non-file
|
||||
|
||||
Reference in New Issue
Block a user