/** * Shared atomic-file helper: a single lock+tmp+rename implementation reused by every * on-disk store that does a read-modify-write against a JSON/text file. Modeled on the pattern * already proven in `memory/providers/file-store.ts` (proper-lockfile advisory lock + write-tmp- * then-rename); before this helper existed the pattern was copy-pasted per store, and most copies * were missing either the lock, the atomic write, or both. * * Two call shapes: * - `withFileLock(Sync)` — hold an exclusive advisory lock across an arbitrary read-modify-write * callback. The lock spans BOTH the read and the write, closing the classic RMW race where two * writers each read the old content before either writes back. * - `writeFileAtomic(Sync)` — write-tmp-then-rename. Used INSIDE a `withFileLock` callback (or * standalone, for a pure overwrite that has no read step to race). * * Sync and async variants are both exported. Most existing stores expose a synchronous public API * called from hot, non-async paths (e.g. a per-token-stream perf sample), so the sync variant lets * them gain locking without forcing an async ripple through their callers; the async variant is for * call sites that are already async. */ import { type FaultableFs } from "./faultable-fs.ts"; export interface AtomicFileLockOptions { /** * Bounded retry attempts while waiting for a lock already held elsewhere. Both variants use a * SHORT, capped backoff (see {@link RETRY_MIN_TIMEOUT_MS}/{@link RETRY_MAX_TIMEOUT_MS}) — these * stores' critical sections are sub-millisecond reads+writes of small JSON/text files, so * contention should clear in milliseconds, not the multi-second-to-31-second worst case * proper-lockfile's OWN default backoff produces for a bare numeric `retries` (its default * `minTimeout` is 1000ms with factor 2 — see node_modules/retry/lib/retry.js). Passing a bare * number straight through would turn brief contention into a multi-second stall on a hot path * (e.g. a per-token-stream perf sample), so both variants instead build an explicit short-backoff * `retry` options object. * - Async (`withFileLock`): forwarded as `{retries, minTimeout, maxTimeout}` to proper-lockfile. * - Sync (`withFileLockSync`): proper-lockfile's sync API REJECTS `retries > 0` outright (it * requires the whole acquire flow to be synchronous — see proper-lockfile/lib/adapter.js * `toSyncOptions`, which throws `ESYNC`). So the sync path implements its own bounded retry * around single `lockfile.lockSync` attempts, blocking briefly between them (Atomics.wait) — * callers are already fully synchronous fs code, so a short blocking wait on contention matches * the existing execution model rather than introducing a new one. */ retries?: number; /** Initial delay between sync/async acquisition attempts; defaults to 25ms. */ minRetryDelayMs?: number; /** Maximum delay between sync/async acquisition attempts; defaults to 500ms. */ maxRetryDelayMs?: number; /** Retry-delay multiplier; defaults to 2. Set max=min for a fixed delay. */ retryFactor?: number; /** Resolve symlinks before locking (proper-lockfile `realpath`); false matches file-store.ts. */ realpath?: boolean; /** Lock staleness window in ms (proper-lockfile `stale`); omitted = proper-lockfile's own default. */ stale?: number; /** Explicit proper-lockfile directory path; defaults to `${filePath}.lock`. */ lockfilePath?: string; } export interface AtomicFileWriteOptions { /** POSIX permission bits applied to the temporary file and inherited by the renamed destination. */ mode?: number; /** * Injection seam for the mutating fs primitives this write issues (`mkdirSync`, `writeFileSync`, * `renameSync`). Defaults to real `node:fs` — omitting this option is a zero-behavior-change no-op. * Only the destructive-testing harness passes a fault-injecting implementation. */ fs?: FaultableFs; } /** Narrow an unknown Node filesystem failure without unsafe casts at every storage owner. */ export declare function isMissingFileError(error: unknown): boolean; /** * Existing auth/settings/trust paths historically used 10 total attempts separated by 20ms. Keep * that low-latency user-facing policy explicit while routing the mechanism through this module. */ export declare const LOW_LATENCY_FILE_LOCK_OPTIONS: Readonly; /** * Acquire a synchronous advisory file lock and return its release function. This is the shared * mechanism for coordinators that must hold several locks in a deterministic order before entering * one critical section; callers retain ownership of release ordering and release-error semantics. */ export declare function acquireFileLockSync(filePath: string, options?: AtomicFileLockOptions): () => void; /** * Hold an exclusive advisory lock on `filePath` for the duration of `fn` (sync). Always releases, * including when `fn` throws. */ export declare function withFileLockSync(filePath: string, fn: () => T, options?: AtomicFileLockOptions): T; /** Async counterpart of {@link withFileLockSync}. Always releases, including when `fn` throws/rejects. */ export declare function withFileLock(filePath: string, fn: () => Promise | T, options?: AtomicFileLockOptions): Promise; export declare function writeFileAtomicSync(filePath: string, content: string, options?: AtomicFileWriteOptions): void; /** Async counterpart of {@link writeFileAtomicSync}. */ export declare function writeFileAtomic(filePath: string, content: string, options?: AtomicFileWriteOptions): Promise; //# sourceMappingURL=atomic-file.d.ts.map