Foundation lib for status-fil + lock-fil writes in v1.12.0 cron rewrite. Pattern: writeFileSync to <path>.tmp.<pid>.<random> then renameSync to target. Defends against half-written files; readers either see the previous version or the new one, never a partial. - atomicWriteSync(path, content) — string or Buffer - atomicWriteJson(path, obj) — 2-space indent, trailing newline - Windows EEXIST/EPERM defensive fallback (unlink target + rename) - Best-effort tmp cleanup on writeFileSync failure - crypto.randomInt(0, 2**32) two-arg form (unambiguous across Node) 9/9 tests pass including 50-way concurrent-write fuzzer (async-aware withTmp helper). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
53 lines
1.5 KiB
JavaScript
53 lines
1.5 KiB
JavaScript
// atomic-write.mjs — Crash-safe writes via tmp+rename.
|
|
// Zero dependencies. Defends against half-written files; readers either see
|
|
// the previous version or the new one, never a partial.
|
|
|
|
import { writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
import { randomInt } from 'node:crypto';
|
|
|
|
/**
|
|
* Atomically write content to filePath via a tmp file + rename.
|
|
* @param {string} filePath — target absolute path
|
|
* @param {string|Buffer} content — bytes to write
|
|
*/
|
|
export function atomicWriteSync(filePath, content) {
|
|
if (!filePath || typeof filePath !== 'string') {
|
|
throw new Error('atomicWriteSync: filePath required');
|
|
}
|
|
const tmp = `${filePath}.tmp.${process.pid}.${randomInt(0, 2 ** 32)}`;
|
|
try {
|
|
writeFileSync(tmp, content);
|
|
try {
|
|
renameSync(tmp, filePath);
|
|
} catch (err) {
|
|
// Windows ERROR_ALREADY_EXISTS / EPERM defensive fallback.
|
|
if (err && (err.code === 'EEXIST' || err.code === 'EPERM')) {
|
|
try {
|
|
unlinkSync(filePath);
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
renameSync(tmp, filePath);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Best-effort cleanup of the tmp file on failure.
|
|
try {
|
|
unlinkSync(tmp);
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atomically write a JSON-serialized object with 2-space indent.
|
|
* @param {string} filePath
|
|
* @param {unknown} obj
|
|
*/
|
|
export function atomicWriteJson(filePath, obj) {
|
|
atomicWriteSync(filePath, JSON.stringify(obj, null, 2) + '\n');
|
|
}
|