Atomic whole-file replacement (same-dir temp + fsync + rename + parent fsync); the in-memory unit state is authoritative and the file is always the current net state, pretty-printed. Missing files open as empty units and materialize on first write; foreign or unparsable files reject with malformed-medium, stored-version drift with version-mismatch.
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
/**
|
|
* Atomic whole-file replacement for the JSON backend.
|
|
*
|
|
* Publish protocol: write a same-directory temp file, fsync it, then
|
|
* `rename()` over the target. Rename is an atomic replace on POSIX and on
|
|
* Windows (libuv maps it to `MoveFileExW(..., MOVEFILE_REPLACE_EXISTING)`),
|
|
* and replacement is the intended semantic here — unlike the session-log
|
|
* backend's link()+unlink() no-clobber protocol, a unit file has exactly one
|
|
* writer per process and last-write-wins is correct. After the rename the
|
|
* parent directory is fsynced on POSIX so the new entry is crash-durable.
|
|
* @module @deepseek-ai/dsh-storage-json/src/atomic
|
|
*/
|
|
|
|
import { open, rename, rm } from 'node:fs/promises'
|
|
import { dirname, join } from 'node:path'
|
|
import { randomUUID } from 'node:crypto'
|
|
|
|
/**
|
|
* Durably replace `path` with `data`.
|
|
* @param path - Absolute target file path.
|
|
* @param data - Full new file content.
|
|
* @returns resolution after the replacement is crash-durable.
|
|
*/
|
|
export async function writeAtomic(path: string, data: string): Promise<void> {
|
|
const tmp = join(dirname(path), `.${randomUUID()}.tmp`)
|
|
try {
|
|
const handle = await open(tmp, 'wx', 0o600)
|
|
try {
|
|
await handle.writeFile(data, 'utf8')
|
|
await handle.sync()
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
await rename(tmp, path)
|
|
await fsyncDirectory(dirname(path))
|
|
} catch (error) {
|
|
await rm(tmp, { force: true })
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/** fsync a POSIX directory so a just-renamed entry is crash-durable. */
|
|
/* v8 ignore start -- Windows rejects O_RDONLY directory opens; POSIX coverage exercises this. */
|
|
async function fsyncDirectory(path: string): Promise<void> {
|
|
if (process.platform === 'win32') return
|
|
const handle = await open(path, 'r')
|
|
try {
|
|
await handle.sync()
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|
|
/* v8 ignore stop */
|