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 { expect, test } from "bun:test"
import type { PathLike } from "node:fs" import type { Mode, OpenMode, PathLike } from "node:fs"
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises" import { mkdtemp, open, readdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
import { join } from "node:path" 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 }) 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 () => { test("detects and reaps stale lock entries", async () => {
// given // given
const { detectStaleLock, reapStaleLock } = await import("./locks") const { detectStaleLock, reapStaleLock } = await import("./locks")
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto" 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" import { tolerantFsync } from "../../../shared/tolerant-fsync"
@@ -8,6 +8,12 @@ type LockOptions = {
ownerTag?: string ownerTag?: string
} }
type AtomicWriteDeps = {
open?: typeof open
rename?: typeof rename
rm?: typeof rm
}
const LOCK_RETRY_MS = 50 const LOCK_RETRY_MS = 50
const LOCK_WAIT_TIMEOUT_MS = 4_000 const LOCK_WAIT_TIMEOUT_MS = 4_000
@@ -110,21 +116,24 @@ export async function reapStaleLock(lockPath: string): Promise<void> {
export async function atomicWrite( export async function atomicWrite(
filePath: string, filePath: string,
content: string | Buffer, content: string | Buffer,
deps: { rename: typeof rename } = { rename }, deps: AtomicWriteDeps = {},
): Promise<void> { ): Promise<void> {
const tmpPath = `${filePath}.tmp.${randomUUID()}` const tmpPath = `${filePath}.tmp.${randomUUID()}`
const openFile = deps.open ?? open
const renameFile = deps.rename ?? rename
const removeFile = deps.rm ?? rm
try { try {
await writeFile(tmpPath, content) const fileHandle = await openFile(tmpPath, "wx")
const fileHandle = await open(tmpPath, "r")
try { try {
await fileHandle.writeFile(content)
await tolerantFsync(fileHandle, `atomicWrite:${filePath}`) await tolerantFsync(fileHandle, `atomicWrite:${filePath}`)
} finally { } finally {
await fileHandle.close() await fileHandle.close()
} }
await deps.rename(tmpPath, filePath) await renameFile(tmpPath, filePath)
} catch (error) { } catch (error) {
await rm(tmpPath, { force: true }) await removeFile(tmpPath, { force: true })
throw error throw error
} }
} }