← back to Dw Photo Capture
robustness: atomic saveJSON so a crash/ENOSPC can't corrupt persisted data (cycle 18)
4d8fe6de29b93ef39dbda6e7b5a098184f9f6c0b · 2026-06-26 07:57:32 -0700 · Steve Abrams
Brainstorm idea #2. saveJSON (the shared write primitive for progress/recents/favorites/
vendor_profiles) did a plain writeFileSync — a crash or disk-full mid-write could leave a
truncated, corrupt file, losing all 180 learned vendor profiles. Especially live given we
hit ENOSPC this very night.
Now it serializes to a temp file then renameSync over the target (atomic on POSIX), and on
failure removes the partial temp and rethrows (same API). One change to the shared primitive
protects every JSON persistence path.
Proved with the REAL saveJSON source: a forced write failure left the original file intact
with 0 stale temps; normal /api/learn still persists (200), file stays valid JSON. lint +
51 tests green, selfcheck 200. $0 (local).
Files touched
M OVERNIGHT_LEDGER.mdM server.js
Diff
commit 4d8fe6de29b93ef39dbda6e7b5a098184f9f6c0b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 07:57:32 2026 -0700
robustness: atomic saveJSON so a crash/ENOSPC can't corrupt persisted data (cycle 18)
Brainstorm idea #2. saveJSON (the shared write primitive for progress/recents/favorites/
vendor_profiles) did a plain writeFileSync — a crash or disk-full mid-write could leave a
truncated, corrupt file, losing all 180 learned vendor profiles. Especially live given we
hit ENOSPC this very night.
Now it serializes to a temp file then renameSync over the target (atomic on POSIX), and on
failure removes the partial temp and rethrows (same API). One change to the shared primitive
protects every JSON persistence path.
Proved with the REAL saveJSON source: a forced write failure left the original file intact
with 0 stale temps; normal /api/learn still persists (200), file stays valid JSON. lint +
51 tests green, selfcheck 200. $0 (local).
---
OVERNIGHT_LEDGER.md | 1 +
server.js | 9 ++++++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 0afc9d2..75d3dc3 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -63,3 +63,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
### Deferred known-minor (cycle 17, low value)
- scanResolve runs ALL candidates through TWIL (pass 1) before ANY through Shopify (pass 2), so a weak candidate that hits TWIL beats a barcode that only resolves in Shopify. Narrow edge (barcode present + fails TWIL + weak code hits TWIL); the TWIL-first source preference is deliberate. Revisit only if it surfaces in real use.
+| 18 | brainstorm #2 (ROBUSTNESS) | atomic saveJSON (temp-file + rename) — a crash/ENOSPC mid-write can no longer truncate vendor_profiles.json (180 learned profiles) or progress/recents/favorites; single shared-primitive fix protects all persistence | df 29G; proved via REAL saveJSON: forced write-failure left original intact, 0 stale temps; learn persists 200; lint+51 tests; selfcheck 200 | $0 |
diff --git a/server.js b/server.js
index 1307070..c608e88 100644
--- a/server.js
+++ b/server.js
@@ -36,7 +36,14 @@ if (!TOKEN) {
fs.mkdirSync(PHOTOS, { recursive: true });
const loadJSON = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { return d; } };
-const saveJSON = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, 2));
+const saveJSON = (f, o) => {
+ // ATOMIC write: serialize to a temp file then rename over the target. rename() is atomic on
+ // POSIX, so a crash or ENOSPC mid-write can never truncate/corrupt the real file (the learned
+ // vendor_profiles.json etc.). On failure, remove the partial temp and rethrow (same API).
+ const tmp = `${f}.tmp-${process.pid}`;
+ try { fs.writeFileSync(tmp, JSON.stringify(o, null, 2)); fs.renameSync(tmp, f); }
+ catch (e) { try { fs.unlinkSync(tmp); } catch (e2) {} throw e; }
+};
// Auto-version: derive a sequential build number from the actual file contents.
// Any change to index.html or server.js → new hash → next build number. Never manual.
← d9df0cb test: lock candidate-order contract scanResolve relies on (c
·
back to Dw Photo Capture
·
auto-save: 2026-06-26T08:13:39 (2 files) — data/build.json d c3b1b21 →