diff --git a/packages/server-core/src/write-coalescer.js b/packages/server-core/src/write-coalescer.js index 86341b3..7f43a76 100644 --- a/packages/server-core/src/write-coalescer.js +++ b/packages/server-core/src/write-coalescer.js @@ -31,9 +31,19 @@ async function writeToDisk(absPath, data, encoding) { ); lastWriteTime.set(absPath, Date.now()); - const stat = await fs.promises.stat(absPath); - return { mtime: stat.mtimeMs, size: stat.size }; + // A concurrent delete can remove the file between the write and the stat (a rapid write-then-delete on the same path). + // The write itself succeeds, so report synthetic metadata rather than failing the request on the now-missing file. + try { + const stat = await fs.promises.stat(absPath); + return { mtime: stat.mtimeMs, size: stat.size }; + } catch (e) { + if (e.code === "ENOENT") { + return { mtime: Date.now(), size: estimateSize(data, encoding) }; + } + + throw e; + } } function flushEntry(absPath) { diff --git a/packages/server-core/src/write-coalescer.test.mjs b/packages/server-core/src/write-coalescer.test.mjs index 052501b..67c518c 100644 --- a/packages/server-core/src/write-coalescer.test.mjs +++ b/packages/server-core/src/write-coalescer.test.mjs @@ -99,6 +99,18 @@ describe("writeCoalesced", () => { expect(elapsed).toBeLessThan(20); }); + + it("returns synthetic metadata when the file is deleted before the post-write stat", async () => { + const filePath = path.join(tmpDir, "race.txt"); + vi.spyOn(fs.promises, "stat").mockRejectedValueOnce( + Object.assign(new Error("ENOENT"), { code: "ENOENT" }), + ); + + const result = await coalescer.writeCoalesced(filePath, "hello", "utf-8"); + + expect(result.size).toBe(5); + expect(result.mtime).toBeGreaterThan(0); + }); }); describe("getPending", () => {