Merge pull request #3838 from NICxKMS/fix/team-mode-windows-atomic-write

fix(team-mode): sync atomic writes through writable handle
This commit is contained in:
YeonGyu-Kim
2026-05-21 00:46:24 +09:00
committed by GitHub
2 changed files with 40 additions and 8 deletions
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test"
import type { PathLike } from "node:fs"
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"
import type { Mode, OpenMode, PathLike } from "node:fs"
import { mkdtemp, open, readdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
@@ -72,6 +72,29 @@ test("atomicWrite leaves no partial file when rename fails", async () => {
await rm(rootDirectory, { recursive: true, force: true })
})
test("atomicWrite syncs temp files through a writable handle", async () => {
// given
const rootDirectory = await createTempDirectory("locks-atomic-writable-")
const targetPath = join(rootDirectory, "target.txt")
const openFlags: string[] = []
const { atomicWrite } = await import("./locks")
// when
await atomicWrite(targetPath, "new content", {
rename,
open: async (filePath: PathLike, flags?: OpenMode, mode?: Mode) => {
openFlags.push(String(flags))
return await open(filePath, flags, mode)
},
})
// then
expect(openFlags).toEqual(["wx"])
expect(await readFile(targetPath, "utf8")).toBe("new content")
await rm(rootDirectory, { recursive: true, force: true })
})
test("detects and reaps stale lock entries", async () => {
// given
const { detectStaleLock, reapStaleLock } = await import("./locks")
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto"
import { open, readFile, rename, rm, unlink, writeFile } from "node:fs/promises"
import { open, readFile, rename, rm, unlink } from "node:fs/promises"
import { tolerantFsync } from "../../../shared/tolerant-fsync"
@@ -8,6 +8,12 @@ type LockOptions = {
ownerTag?: string
}
type AtomicWriteDeps = {
open?: typeof open
rename?: typeof rename
rm?: typeof rm
}
const LOCK_RETRY_MS = 50
const LOCK_WAIT_TIMEOUT_MS = 4_000
@@ -110,21 +116,24 @@ export async function reapStaleLock(lockPath: string): Promise<void> {
export async function atomicWrite(
filePath: string,
content: string | Buffer,
deps: { rename: typeof rename } = { rename },
deps: AtomicWriteDeps = {},
): Promise<void> {
const tmpPath = `${filePath}.tmp.${randomUUID()}`
const openFile = deps.open ?? open
const renameFile = deps.rename ?? rename
const removeFile = deps.rm ?? rm
try {
await writeFile(tmpPath, content)
const fileHandle = await open(tmpPath, "r")
const fileHandle = await openFile(tmpPath, "wx")
try {
await fileHandle.writeFile(content)
await tolerantFsync(fileHandle, `atomicWrite:${filePath}`)
} finally {
await fileHandle.close()
}
await deps.rename(tmpPath, filePath)
await renameFile(tmpPath, filePath)
} catch (error) {
await rm(tmpPath, { force: true })
await removeFile(tmpPath, { force: true })
throw error
}
}