/** * Single-instance mailbox-bridge lock. * * Per-project isolation: each project (resolved via * `resolveProjectDir`) gets its own lock file under * `/.mailbox-bridge.lock`. Two projects on the same * machine never collide — different slugs, different lock files, * and (with default port 0) different OS-assigned ports. * * The lock holds: * - pid process id of the owning `wstack mailbox serve` * - host, port what the owner actually bound (port may be OS-assigned) * - url convenience copy for callers * - token the bearer token (so external agents don't have to * re-read it from .mailbox.token separately) * - generation monotonically increasing counter; bumped on every * acquire. Lets a release() call identify whether the * current lock file still belongs to *this* process * (e.g. after a stale cleanup raced with us). * - spawnedAt ISO timestamp — for observability, not load-bearing. * * Concurrency model: * - `acquireOrJoin` is atomic via rename(2). Two concurrent spawns * race to write a tmpfile then rename; one wins, the other reads * the winner's lock and joins. * - PID liveness is checked via `process.kill(pid, 0)` (POSIX) or * `tasklist` (Windows). A live PID means another instance owns * this project; a dead PID means the lock is stale and gets cleaned. * - `release` is best-effort: a missing lock file at shutdown is * fine (someone else already took over). A generation mismatch means * we lost the race and another instance owns it now — also fine. * * Token persistence: the token in the lock always matches the token in * `.mailbox.token`, so an external agent that reads either file gets the * same value. While an instance stays alive, every joining caller * (`kind: 'joined'`) reuses that one token. A cold (re)start — no lock, * or a lock whose owner PID is dead/unhealthy — mints a FRESH token and * rewrites both files atomically. External agents must therefore re-read * `.mailbox.token` after a bridge restart (a stale token returns 401). */ export declare const MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock"; export declare const MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token"; export interface MailboxBridgeLock { pid: number; host: string; port: number; url: string; token: string; generation: number; spawnedAt: string; } export type AcquireResult = { kind: 'acquired'; lock: MailboxBridgeLock; tokenPath: string; } | { kind: 'joined'; lock: MailboxBridgeLock; tokenPath: string; } | { kind: 'port-conflict'; existing: MailboxBridgeLock; }; export interface AcquireOptions { /** Project dir (already resolved by caller via resolveProjectDir). */ projectDir: string; /** Bind host. Required when acquiring. */ host: string; /** Explicit port. Pass null/undefined to mean "OS-assigned" (read it after listen). */ requestedPort: number | null; /** When true, fail loud on EADDRINUSE. When false, fall back to OS-assigned. */ strictPort: boolean; } /** * Try to acquire the mailbox-bridge lock for `projectDir`. If another * instance is already alive and healthy, join it. Otherwise claim * the slot for ourselves. * * Two-phase contract: * 1. Caller invokes `acquireOrJoin(...)` BEFORE calling * `server.listen()`. The returned lock either: * - `kind: 'joined'` → another instance is alive, the caller * should NOT bind; just print the URL/token * and exit cleanly (return 0). * - `kind: 'acquired'` → caller is the owner, proceeds to bind. * - `kind: 'port-conflict'` → caller asked for an explicit port * that's already taken by an unrelated * process; we return the existing owner * but the caller decides what to do * (loud-fail). * 2. After `server.listen()` resolves with a real port, the caller * invokes `finalize(lock, boundPort)` to atomically write the * final lock + token file with the OS-assigned port. * * The two-phase split keeps the listen() call out of the lock module — * the caller owns the HTTP server instance; the lock module owns * the on-disk contract. */ export declare function acquireOrJoin(opts: AcquireOptions): Promise; /** * Phase 2 — after server.listen() resolves, write the final lock * with the actually-bound port and the same token. Also writes the * .mailbox.token file with mode 0600 so external agents can read it. * * Returns the finalized lock so the caller can use the resolved URL. */ export declare function finalize(projectDir: string, tentative: MailboxBridgeLock, boundPort: number): Promise; /** * Phase 3 — best-effort cleanup on shutdown. Removes the lock + token * files IF this process is still the recorded owner (generation match). * * If the generation doesn't match, another acquire() has already * superseded us — we must NOT delete their lock. */ export declare function release(projectDir: string, generation: number): Promise; /** * Read the lock file and return it, with enough information for the * caller to distinguish: * - 'live' — PID alive, /healthz reachable; safe to use. * - 'probe-failed' — PID alive (or recently was) but /healthz * unreachable. Caller can still return the URL * + token to the host so its request layer can * retry; the host's fetch timeout will surface * the real error if the bridge is truly dead. * - 'absent' — no lock file existed, or it was malformed * (cleaned up best-effort). * * Distinguishing 'probe-failed' from 'absent' matters for the * "joined vs spawned" decision in tryAcquireMailboxBridge — we * don't want to spawn a second bridge just because /healthz flaked. */ export type LiveLockResult = { kind: 'live'; lock: MailboxBridgeLock; } /** * The lock exists but isn't usable. `pidAlive` tells the caller * which recovery to pick: * - `pidAlive: false` — the owning process is dead (stale lock). * Callers should treat this like `absent` and spawn a fresh * bridge rather than surfacing a dead URL. * - `pidAlive: true` — the process is alive but /healthz didn't * respond (booting/wedged/PID-reuse). Callers may return the * recorded URL/token and let a real request confirm liveness. */ | { kind: 'probe-failed'; lock: MailboxBridgeLock; pidAlive: boolean; } | { kind: 'absent'; }; export declare function readLiveLock(projectDir: string): Promise; //# sourceMappingURL=single-instance-mailbox.d.ts.map