export interface AtomicWriteOptions { mode?: number; dirMode?: number; renameRetries?: number; renameRetryDelayMs?: number; fsync?: boolean; durabilityRoot?: string; } export declare const renameAtomic: (source: string, target: string, options?: AtomicWriteOptions) => void; export declare const unsupportedDirectoryFsyncCodes: (platform: NodeJS.Platform) => ReadonlySet; /** * Flushes a directory entry table so a create or rename in it is durable. * Platforms that cannot open or flush a directory (Windows, some network * filesystems) report an error code that is ignored here; on POSIX a * permission error is rethrown. */ export declare const fsyncDirectory: (directory: string) => void; /** * mkdir -p plus barriers for every ancestor link, including pre-existing ones. * By default the crash boundary is the filesystem root. With an explicit * inclusive durabilityRoot, its parent chain must already be durable; require * that parent to exist so mkdir cannot silently create above the boundary. * Ancestors must not be concurrently renamed/replaced. Concurrent creators * and retries after failed mkdir/fsync are safe: all barriers run again. */ export declare const mkdirDurable: (directory: string, mode: number, fsync: boolean, durabilityRoot?: string) => void; export declare const writeFileAtomic: (filePath: string, contents: string, options?: AtomicWriteOptions) => void; export interface AtomicJsonOptions extends AtomicWriteOptions { space?: number; newline?: boolean; } export declare const writeJsonAtomic: (filePath: string, value: unknown, options?: AtomicJsonOptions) => void; export declare const writeJsonAtomicAsync: (filePath: string, value: unknown, options?: AtomicJsonOptions) => Promise; /** Long-lived owners refresh `heartbeatAt` this often. */ export declare const OWNER_HEARTBEAT_INTERVAL_MS = 10000; /** A heartbeat older than this proves the owner dead. */ export declare const OWNER_HEARTBEAT_TTL_MS = 45000; /** * Short-lived locks carry no heartbeat. Across namespaces their creation time * stands in for one, so a crashed foreign holder cannot strand a lock forever. * * The ceiling has an inherent cost: a holder suspended past it (SIGSTOP, a * laptop sleep, a debugger) is reaped while it still believes it holds the * lock, and on resuming it can finish one write after another process took * the lock. Its release stays safe: releaseShortLock only removes a lock * whose owner file carries its own token, so it never frees the new holder. */ export declare const SHORT_LOCK_MAX_HOLD_MS: number; export type OwnerLiveness = "alive" | "dead" | "unknown"; export interface OwnerIdentity { pid: number; hostname: string; pidNamespace?: string; bootId?: string; startedAt: number; } /** A persisted owner. Identity fields are optional so old records still parse. */ export interface OwnerRecord { pid: number; hostname?: string; pidNamespace?: string; bootId?: string; startedAt?: number; } export interface LivenessProbes { self?: () => OwnerIdentity; /** `kill(pid, 0)`: ESRCH dead, EPERM alive, anything else unknown. */ signal?: (pid: number) => OwnerLiveness; /** Wall-clock start of `pid` when the platform reports it (Linux procfs). */ processStartedAt?: (pid: number) => number | undefined; now?: () => number; } export interface OwnerLivenessOptions { heartbeatAt?: number; heartbeatTtlMs?: number; /** The caller's exact pre-identity probe, used only for legacy records. */ legacyAlive?: (pid: number) => boolean; probes?: LivenessProbes; } /** Parses `/proc//stat` (field 22, starttime) into wall-clock ms. */ export declare const parseProcStartedAt: (stat: string, bootSeconds: number) => number | undefined; /** This process's identity, computed once on first use (never at import). */ export declare const currentOwnerIdentity: () => OwnerIdentity; /** Identity fields of a persisted record, or undefined for legacy/malformed ones. */ export declare const ownerIdentityFields: (value: unknown) => Omit | undefined; /** One JSON line appended to line-oriented lock owner files. */ export declare const encodeOwnerIdentityLine: () => string; /** The identity line of a lock owner file; absent or malformed is legacy. */ export declare const decodeOwnerIdentityLine: (line: string | undefined) => Omit | undefined; /** Additive fields a long-lived JSON owner record carries. */ export interface OwnerHeartbeatFields { identity?: Omit; heartbeatAt?: number; } /** Identity and first heartbeat for a JSON owner record being acquired. */ export declare const ownerHeartbeatFields: (now?: number) => Required; /** * Liveness of a persisted JSON owner record `{ pid, identity?, heartbeatAt? }`. * Malformed identity is treated as legacy; a non-numeric pid is dead. */ export declare const recordOwnerLiveness: (record: unknown, options?: Omit) => OwnerLiveness; export declare const ownerLiveness: (record: OwnerRecord, options?: OwnerLivenessOptions) => OwnerLiveness; /** * Liveness of a line-oriented lock owner (`[token\n]pid\ncreated\n{identity}`). * Short locks never heartbeat, so their creation time is the heartbeat with a * generous hold ceiling. */ export declare const lockOwnerLiveness: (pid: number, createdAt: number, identityLine: string | undefined, options?: Pick & { maxHoldMs?: number; }) => OwnerLiveness; /** * True only when a line-oriented lock owner is provably gone, whatever the * lock's age: its identity line names this host, PID namespace and boot, and * its pid is absent or now belongs to a process started at another time. * Legacy owners, other hosts, namespaces or boots, unusable pids, and alive or * unknown probes prove nothing, so callers keep their stale-age rule for them. */ export declare const lockOwnerProvablyDead: (pid: number, identityLine: string | undefined, options?: Pick) => boolean; /** How long a release keeps looking for a lock a reaper is holding. */ export declare const SHORT_LOCK_RELEASE_RETRY_MS = 25; /** Owner file text for a short lock acquired with `token`. */ export declare const formatShortLockOwner: (token: string) => string; export interface ShortLockOwner { token: string; /** Absent when the pid line is missing or not a positive decimal pid. */ pid?: number; /** Absent when the creation line is missing or not a decimal number. */ createdAt?: number; identityLine?: string; } /** * Parses a short-lock owner file field by field. A missing, empty or partial * file yields no pid: such a lock says nothing about its owner and is judged * by its age alone, never as a dead owner. */ export declare const parseShortLockOwner: (text: string) => ShortLockOwner; export interface ShortLockJudgeOptions { staleMs: number; /** The store's pre-identity probe for owners without an identity line. */ legacyAlive?: (pid: number) => boolean; probes?: LivenessProbes; } /** * True when a short lock may be reaped. * - No usable pid (missing, empty or partial owner file): only when the lock * is older than the stale window. Its owner may be mid-acquire. * - Within the stale window: only when the owner is provably dead. * - Older than max(SHORT_LOCK_MAX_HOLD_MS, staleMs): always. A live owner * this old has leaked the lock; no critical section runs that long. * - Otherwise: when the owner is dead (namespace-safe liveness). * A creation time that is missing or in the future falls back to the * directory's mtime. * The hold ceiling lets a suspended holder finish one write after a reaper * took its lock; see SHORT_LOCK_MAX_HOLD_MS. */ export declare const shortLockReapable: (ownerText: string | undefined, lockModifiedAt: number | undefined, options: ShortLockJudgeOptions) => boolean; /** Device and inode of a lock directory; bigint so Windows file ids stay exact. */ export interface LockInode { dev: bigint; ino: bigint; } /** * Creates the lock directory exclusively and publishes its owner file * atomically. `mkdir` stays the canonical claim, as in older versions. * Upgraded acquirers additionally refuse in-flight rename claims (see below). * The owner is written to a * temporary file inside the lock and hard-linked into place: a reader sees no * owner (judged by the directory's age) or a complete one, never an empty or * partial owner. Returns false on contention. * * The link refuses to publish over an existing owner. A reaper whose claim * held a live lock renames it back, and POSIX rename() silently replaces the * empty directory this call just created. Publishing then targets the other * owner's directory; link() fails EEXIST, so this returns false and removes * only its own temporary file, never the directory (it is the other owner's * lock). See publishShortLockOwner for the rename fallback. * * After publication, a strict claim scan is followed by canonical inode and * owner verification. A displaced successful holder remains in a claim, so * no successor may enter until it is restored or released. If displacement * occurs after this scan/check, the next acquirer sees that claim instead. * Fail closed on scan errors. Older binaries do not honor this barrier and * retain their rename-window race; ordinary mixed-version mkdir contention * still works. See docs/short-lock-protocol.md. */ export declare const tryAcquireShortLock: (lock: string, ownerText: string) => boolean; /** * Removes the lock directory `lock` only when `verify(claimed)` accepts the * directory this process claimed by renaming it. Returns true when it removed * the lock, false when the claim failed or held another owner's lock. * * Stale-lock recovery must be an exclusive claim. Stat-then-delete is * TOCTOU: two reapers (or a reaper and a fresh writer that recreated the * lock in between) can both pass their checks, and the slower rm then * deletes a lock the faster one already replaced. rename() is the claim — * only one process can move the directory, and removal targets the claimed * path, never the live lock path. A claim that turns out to hold a live * lock is renamed back before any destructive step; a live lock is never * deleted, even if the rename-back races a fresh writer. A live owner that * releases while its lock sits at the claim path takes it from there * (releaseShortLock), so the rename-back then finds nothing to restore. * * `expected` is the lock directory's inode when the caller judged it (stat it * before reading the owner); without it the lock is stat'd just before the * rename. When the claimed directory is a different inode, the lock was * reaped and re-acquired in between: the claim holds someone else's live * lock, so it is renamed back at once, without reading its owner or calling * `verify`, and never deleted. If that rename-back fails it is left for its * owner's release or the claim sweep. * * The claim itself is an acquisition barrier even on an inode mismatch. * A successor can populate the canonical directory but must withdraw before * entering its critical section while this claim exists. Restoration may * still fail; the barrier then persists until owner release or safe stale * cleanup. No compare-and-rename primitive or recursively reaped guard is * needed. Inode reuse is not detected here; `verify` also compares the owner. */ export declare const reapLockByRenameClaim: (lock: string, verify: (claimed: string) => boolean, expected?: LockInode | undefined) => boolean; /** * Removes claim directories beside `lock` older than `staleMs`: reapers or * releasers that died mid-claim, or rename-backs that lost a race. Claims * are acquisition barriers: age alone must not discard a displaced live * holder. Require the ordinary owner-staleness verdict too. Returns the count. * * A claim's age comes from the `t` stamp in its name, which is the * claimant's clock, not this process's: a claimant whose clock runs ahead * makes its claim look younger, one that runs behind makes it look older. * Claims without a stamp (older versions) fall back to the directory's mtime, * which is when its contents last changed, not when it was claimed. */ export declare const sweepStaleLockClaims: (lock: string, staleMs: number, now?: number) => number; /** * One stale-lock recovery step for a contended short lock: sweeps old claim * directories (at most once a second per lock and process), judges the lock * with shortLockReapable, and removes it through a rename claim that * re-verifies the same owner text and verdict. True when it removed the lock * and acquisition should retry at once. Never throws. */ export declare const reapStaleShortLock: (lock: string, options: ShortLockJudgeOptions) => boolean; /** * Releases a short lock acquired with `token`. The lock may be sitting at a * reaper's claim path (`.reap-*`) while the reaper verifies it; the * release then takes it from there, so the reaper's rename-back finds nothing * and cannot restore a released lock. A rename-back may also land between * two checks, so the release retries for a bounded few milliseconds. * Returns false when the lock was not found (reaped, or replaced by an * owner with another token, which is never touched). */ export declare const releaseShortLock: (lock: string, token: string, retryMs?: number) => boolean; /** * Runs `refresh(now)` on an unref'd interval until the returned stop function * is called. Refresh failures are swallowed: a missed beat only ages the * record toward the TTL. */ export declare const startOwnerHeartbeat: (refresh: (heartbeatAt: number) => void, intervalMs?: number) => (() => void); //# sourceMappingURL=atomic-write.d.ts.map