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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user