ktg-plugin-marketplace/plugins/ms-ai-architect/scripts/kb-update/lib/atomic-write.mjs
Kjell Tore Guttormsen 4a615b10ce feat(ms-ai-architect): add lib/atomic-write for crash-safe state files [skip-docs]
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>
2026-05-05 10:32:13 +02:00

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');
}