English | 中文
Zero-dependency atomic file replacement shared by file-backed stores that must never leave partial, symlink-hijacked, or wider-than-intended content on disk — the user-settings document (dsh-settings-file) and the credentials store (dsh-credentials-local).
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
declare const text: string
declare const render: (previous: string) => string
await writeFileAtomic('/home/u/.dsh/settings.yaml', text, { mode: 0o600 })
// Read-modify-write against the same file from several processes.
await withFileLock('/home/u/.dsh/settings.yaml', async () => {
await writeFileAtomic('/home/u/.dsh/settings.yaml', render(text), { mode: 0o600 })
})
writeFileAtomic commits one already-rendered string. The contract, in the order failures would exploit it:
wx, random suffix): the open refuses to follow a symlink planted at a guessable temp path.mode through the rename: replacing a wider-permission file narrows it without a chmod race. mode is required so the permission decision stays visible at every call site (subject to the process umask, like every fresh inode).rename replaces a symlinked target itself, never writing through to its referent.withFileLock serializes the writers of one file across processes, for the read-render-commit cycles a bare atomic commit cannot make safe on its own. The lock is a wx-created <filename>.lock sibling, so readers never contend; waiters back off exponentially and fail with a timeout rather than block forever. EEXIST identifies contention directly; EPERM does so only when a fresh lstat confirms that the lock path exists, covering Windows exclusive-create behavior without hiding an unrelated permission failure. A contender never removes the existing lock: age cannot distinguish a crashed owner from a paused live writer.
How long a contender waits is a property of the operation the holder runs, so it is stated per call through waitMs. The default is sized for file work alone; a holder whose cycle includes a network round trip — a credential mutation that refreshes an expired token — states a longer one, because leaving the default would fail every other writer of that file for the duration. The retry cadence stays fixed: it governs how often a contender asks, which no caller has a reason to vary.
None, as this is a pure filesystem primitive; nothing here reaches a model request.
None; nothing here enters a request prefix.
fsync of the file or its directory, so after a crash the rename may be observed unwound. The file-backed stores here re-read and republish on boot, keeping durability the caller's policy.Buffer or stream form until a consumer needs one.