writePositionMap: tmp + rename — the same crash-safety rewriteLedgers uses

Flag routed from the UI stream's rename-shuffle fix (a9339b3): the
positions ledger was the one remaining plain writeFileSync among the
rewritable ledgers — a torn write (crash mid-write) could replace it
with a half-written file, and readPositionMap parses a failure to {}
(honest empty, but the resting layout would be silently lost). The
tmp+rename pattern from rewriteLedgers, ~4 lines up the file: write a
uniquely-named temp, rename over the ledger — a crash leaves either the
old ledger intact or the new one complete, plus an orphan .tmp-*.

879/879 tests, typecheck/lint/build green; live-verified via the node
deletion's positions prune (no temp residue, valid JSON).
This commit is contained in:
amosblomqvist
2026-09-23 20:23:25 +03:00
parent e826088cc8
commit 10be308ab6
+15 -5
View File
@@ -304,11 +304,21 @@ function readPositionMap(): Record<string, Position> {
function writePositionMap(positions: Record<string, Position>): void {
fs.mkdirSync(path.dirname(POSITIONS_FILE), { recursive: true });
fs.writeFileSync(
POSITIONS_FILE,
JSON.stringify({ positions, updatedAt: new Date().toISOString() }, null, "\t"),
"utf-8",
);
// Tmp + rename — the same crash-safety rewriteLedgers uses two blocks
// up (a9339b3 UI flag): a torn write must never replace the ledger
// with a half-written file the reader parses as empty.
const tmp = `${POSITIONS_FILE}.tmp-${crypto.randomUUID().slice(0, 8)}`;
try {
fs.writeFileSync(tmp, JSON.stringify({ positions, updatedAt: new Date().toISOString() }, null, "\t"), "utf-8");
fs.renameSync(tmp, POSITIONS_FILE);
} catch (err) {
try {
fs.unlinkSync(tmp);
} catch {
/* the rename either already happened or the tmp never existed */
}
throw err;
}
}
const isPosition = (v: unknown): v is Position =>