From 10be308ab6797189f7af1f1b924d87875e01f2ae Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Wed, 23 Sep 2026 20:23:25 +0300 Subject: [PATCH] =?UTF-8?q?writePositionMap:=20tmp=20+=20rename=20?= =?UTF-8?q?=E2=80=94=20the=20same=20crash-safety=20rewriteLedgers=20uses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/store/graph.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/store/graph.ts b/src/store/graph.ts index 2f84c01..f4f89b8 100644 --- a/src/store/graph.ts +++ b/src/store/graph.ts @@ -304,11 +304,21 @@ function readPositionMap(): Record { function writePositionMap(positions: Record): 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 =>