How nodeterm Ensures Atomic Writes to Its Stores: A Deep Dive into the Filesystem Layer
nodeterm guarantees data integrity by writing to unique temporary files and atomically renaming them into place, with automatic retry logic for Windows compatibility and comprehensive cleanup safeguards.
nodeterm is a terminal emulator that requires rock-solid persistence for user data ranging from workspace layouts to credential files. To prevent data corruption during crashes or concurrent access, the project implements a robust atomic write strategy centralized in src/core/fs-atomic.ts, ensuring readers always see either the previous complete state or the new complete state—never a partial write.
The Three-Stage Atomic Commit Process
Stage 1: Isolated Temporary File Creation
Every write operation begins by generating a unique temporary filename via tempNameFor. This function encodes the target path, current process PID, a sequence counter, and a UUID into the filename (e.g., settings.json.12345.1.550e8400-e29b-41d4-a716-446655440000.tmp). This naming convention eliminates collision risks when multiple processes write simultaneously.
Stage 2: Atomic Publication with Cross-Platform Retries
After writing data to the temporary file with exclusive mode flags (wx), the system attempts to publish the content using renameAtomic. On POSIX systems, fs.rename is atomic by design. However, Windows can throw transient sharing-violation errors (EPERM, EACCES, EBUSY). The renameAtomic implementation automatically retries the operation with exponential backoff delays of 10 ms, 25 ms, 75 ms, and 200 ms before failing definitively.
Stage 3: Defensive Cleanup on Failure
If any step in the write pipeline fails, writeFileAtomic immediately removes the temporary file using fs.rm(..., { force: true }). This ensures incomplete writes never persist, leaving the original target file untouched and valid.
Handling Deletions and Maintenance
Atomic Removal with removeAtomic
The companion function removeAtomic applies the same retry logic to file deletions, treating ENOENT (file not found) as success. This prevents race conditions where a file is deleted between existence checks and removal attempts.
The Stale Temp File Sweeper
To prevent disk pollution from abandoned temporary files (e.g., after process crashes), nodeterm runs sweepStaleTempFiles, which removes temporary files older than 24 hours. This maintenance routine keeps data directories clean without risking active writes.
Enforcing Atomicity Across the Codebase
The Guard Test Pattern
A specialized test file, src/core/fs-atomic.guard.test.ts, scans the entire codebase to verify that all stores use writeFileAtomic or renameAtomic rather than raw fs.rename calls. This automated check prevents regression and ensures consistent atomicity guarantees across settings, workspace layouts, credential files, and scrollback stores.
Integration in Core Stores
The implementation enforces usage across critical components. For example, src/core/workspace-store.ts persists layout data via writeFileAtomic, while src/core/scrollback-store.ts uses the same pathway for terminal history snapshots. SSH project management in src/main/remote-ssh/ssh-project.ts combines removeAtomic and renameAtomic for safe file operations.
Practical Implementation Examples
Use writeFileAtomic for standard JSON persistence:
import { writeFileAtomic } from '@/core/fs-atomic'
// `targetPath` is the final location of the file
await writeFileAtomic(targetPath, JSON.stringify(workspaceData, null, 2))
For scenarios where you have already prepared a temporary file, use explicit atomic renaming:
import { renameAtomic } from '@/core/fs-atomic'
await renameAtomic(tempPath, targetPath)
Handle deletions safely with Windows-compatible retry logic:
import { removeAtomic } from '@/core/fs-atomic'
const deleted = await removeAtomic(targetPath)
if (!deleted) {
// handle the case where the file could not be removed
}
Generate unique temporary names for custom workflows:
import { tempNameFor } from '@/core/fs-atomic'
const tmp = tempNameFor(targetPath) // e.g. "/data/settings.json.12345.1.550e8400-e29b-41d4-a716-446655440000.tmp"
Summary
- Unique temporary files prevent write collisions by encoding PID, sequence counters, and UUIDs into filenames via
tempNameFor. - Atomic publication uses
renameAtomicwith platform-specific retry logic (handlingEPERM,EACCES,EBUSYon Windows) to ensure readers never see partial data. - Automatic cleanup removes temporary files on write failure and sweeps stale temps older than 24 hours via
sweepStaleTempFiles. - Enforcement testing via
src/core/fs-atomic.guard.test.tsensures all stores use atomic helpers instead of raw filesystem operations.
Frequently Asked Questions
What happens if nodeterm crashes during an atomic write?
If the process terminates after creating the temporary file but before the rename completes, the original target file remains intact and readable. The orphaned temporary file will be cleaned up by sweepStaleTempFiles after 24 hours. Readers never encounter corrupted or incomplete data because the new content is only visible after the atomic rename succeeds.
Does this atomic write strategy work reliably on Windows?
Yes. While POSIX systems rely on native atomic fs.rename semantics, the renameAtomic implementation specifically handles Windows-specific sharing violations (EPERM, EACCES, EBUSY) through automatic retries with exponential backoff delays (10 ms, 25 ms, 75 ms, 200 ms). This ensures cross-platform consistency without platform-specific code in the stores themselves.
How does nodeterm prevent temporary file accumulation?
The sweepStaleTempFiles utility automatically deletes temporary files older than 24 hours. Additionally, the writeFileAtomic function immediately cleans up temporary files using fs.rm(..., { force: true }) if any step fails, preventing accumulation from interrupted writes.
Which nodeterm components use atomic writes?
All persistent stores in the application are required to use these helpers, including workspace layouts (src/core/workspace-store.ts), terminal scrollback history (src/core/scrollback-store.ts), GitHub control caches (src/main/github-control.ts), and SSH project files (src/main/remote-ssh/ssh-project.ts). The guard test ensures no store bypasses these atomic primitives.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →