All state one daemon process owns; serve() constructs the single instance and
threads it through every handler.
import type * as Server from './server.ts'; // Internal — a snapshot of the lifecycle flags the handlers coordinate on. const flags = { shuttingDown: false, consecutiveCrashes: 0, listenSucceeded: true }; flags.consecutiveCrashes; // 0 — reset after any run that leaves the browser connected
browser: PlaywrightBrowser | null
Resolved Chrome handle. null until the initial in-flight launch settles
(see browserReady) and again briefly during recoverBrowser. Run
handlers MUST go through awaitBrowser(state) rather than reading this
field directly — it's null until the first awaiting caller resolves it.
The split lets run listen() and write the info file
immediately, instead of blocking on Chrome launch. Under CI load Chrome
launch tail latencies regularly exceed 100 s; gating socket readiness
on Chrome would propagate that latency to every daemon start/status/
ping and exhaust client-side spawn timeouts in both the Node and Deno
paths (CI runs 26006815757 + 26007923495).
browserReady: Promise<PlaywrightBrowser>
Promise of the in-flight Chrome launch. Settles exactly once for the
initial launch and is replaced whenever recoverBrowser runs. Awaited
inside handleRun (via awaitBrowser) so a run request that arrives
before Chrome is up waits the same amount of time it would have waited
for the synchronous launch — but daemon start/ping/status return as
soon as the socket is up. Decoupling Chrome from socket readiness is the
fix for the "Daemon did not start within Ns" timeout class on slow CI.
baseConfig: ResolvedConfig
Captured at startup; used to relaunch the browser on crash recovery.
cwd: string
startedAt: number
pkgMtime: number
runQueue: Promise<unknown>
shuttingDown: boolean
pendingClients: Set<net.Socket>
socketServer: net.Server
idleTimer: NodeJS.Timeout | null
socketPath: string
infoPath: string
lockPath: string
consecutiveCrashes: number
Reset to 0 after any run that left the browser connected.
listenSucceeded: boolean
true after listen() resolves. Gates file cleanup in shutdown: a
process whose listen() failed (concurrent-spawn EADDRINUSE) does not own the
socket or info file, so the loser must not unlink them — only the winner does.
Single-source esbuild incremental-context cache, persisted across daemon runs.
Mutated by reference inside buildIncrementally; disposed on daemon shutdown.
pageSlot: DaemonRunHandles["pageSlot"]
Single-source Page slot for single-group daemon runs. Lives across runs;
Browser.setup reuses slot.page (when connected) instead of newPage().
Closed on daemon shutdown; cleared (set to null) when the page disconnects.