/** * One cross-process file lock, shared by producer ledgers and the approvals store. * * **Extracted rather than copied (R-49).** The ledger has had this lock since fan-out made a second writer * possible; the approvals store had an unlocked read-modify-write, so session 1 could load, session 2 could * revoke, and session 1's next save would **restore the revoked entry** — falsifying a property * `approval-store.ts` documents in so many words. The mitigation already existed twenty lines away. A * second implementation is how a fix comes to contain a smaller copy of the bug it fixed, which has happened * twice in this package (R-38's preview, ADR-0022's republish path), so there is exactly one of these. * * **The two callers want opposite failure behaviour, and that is the caller's decision, not this module's.** * A ledger write that cannot take the lock must fail the delegation closed — a child running with granted * capabilities and no audit line is the thing the ledger exists to prevent. An approvals write that cannot * take the lock must NOT fail the work: the human already said yes, and the store is a convenience cache * (ADR-0020). So this throws `LockTimeoutError`, distinguishable from every other failure, and each caller * decides what that means. */ /** How long to wait for another writer to finish before giving up. Short: failing closed beats hanging. */ export declare const LOCK_TIMEOUT_MS = 2000; /** A lock older than this is treated as abandoned by a killed process and broken. */ export declare const STALE_LOCK_MS = 10000; /** * Raised only when the wait ran out. Its own type so a caller can tell "somebody else is writing" from * "this filesystem rejected the write", which want different messages and, for the approvals store, * different outcomes. */ export declare class LockTimeoutError extends Error { constructor(label: string); } export interface FileLockOptions { readonly staleRecovery?: "age" | "disabled"; } /** * Run `work` while holding an exclusive lock beside `path`. * Explicit disabled recovery never reclaims by age/liveness. V4 selects it internally; an orphan * requires separately authorized quiescent recovery, not a timer or automatic operator action. * The age-based behavior described below remains the default for legacy callers. * * **Why a lock at all.** `O_APPEND` is atomic for one write to a regular file on a POSIX filesystem, and the * guarantee does **not** hold on drvfs (`/mnt/c` under WSL2) or NFS — which is exactly where this project * runs. The approvals store never had the guarantee anyway: read-modify-write is not one write. * * **A lock introduces its own failure mode and it is handled deliberately.** A process killed while holding * the lock would otherwise block every future write forever, so a lock older than `STALE_LOCK_MS` is broken. * Every delete proves ownership first (`removeIfOurs`) — see the token comment in the loop for the two * mutual-exclusion breaks that came from not doing so, both reproduced across real OS processes. * * **What staleness actually measures, stated because it is not what it sounds like.** `STALE_LOCK_MS` * compares the lock's mtime to now; it never checks whether the owner is alive. So *any* 10s stall of the * holder hands the lock on — a `SIGSTOP`, a laptop suspend, swap thrash, a debugger breakpoint, a long GC * pause. Measured on this project's own filesystems, no realistic `work()` comes near it: one ledger append * is 0.1ms on ext4 and 21ms on drvfs, and a 10,000-entry `saveApproval` is 30ms / 97ms. Sixteen-way * contention raises *waiters'* time, never the holder's — max hold measured at 49ms. So the threshold is * two orders of magnitude clear of normal operation and is not guarded against abnormal suspension. * * The timeout is short *on purpose*: work refused because a file was busy is recoverable and loud, while * work that hangs waiting for a lock is neither. */ export declare function withFileLock(path: string, label: string, work: () => Promise, options?: FileLockOptions): Promise; //# sourceMappingURL=file-lock.d.ts.map