/** * JsonRpcChild — shared stdio JSON-RPC subprocess transport for backends * (ADR-0023). * * Every backend adapter reuses one read-loop multiplexer, watchdog, and * process-group lifecycle: * - responses (id, no method) → resolve the matching pending request * - id + method → our response (id registered) or a * server→client request (hook first, then queue) * - method + no id → notification (backend-specific hook) * * Wire dialects differ per backend: zcode sends BARE frames (no `jsonrpc` * field — the backend's strict validator rejects it) while a standard * JSON-RPC 2.0 backend sends versioned frames. Subclasses override * {@link decorateOutbound} for the write dialect and * {@link handleNotification} / {@link handleServerRequest} for inbound * routing. */ import { type ChildProcess } from "node:child_process"; import type { BackendAdapter, BackendCapabilities, EventListener, ServerRequest } from "./adapter.js"; import type { ZcodeEvent, ZcodeResponse } from "./types.js"; export interface JsonRpcChildOptions { argv: string[]; env: NodeJS.ProcessEnv; /** Backend name used in error strings (e.g. "zcode" → "zcode backend pipe broken"). */ name: string; /** Human-readable label for the start log line. */ logLabel: string; /** CLI display name for the ENOENT hint. */ cliLabel: string; /** Env var users can set to point at the CLI binary (for the ENOENT hint). */ binEnvVar: string; } export declare abstract class JsonRpcChild implements BackendAdapter { /** Feature flags of the concrete backend — each subclass binds its kind's table. */ abstract readonly capabilities: BackendCapabilities; readonly proc: ChildProcess; protected readonly name: string; private readonly pending; private readonly serverRequests; private readonly listeners; private readerDead; /** Monotonic id for fire-and-forget sends (send()). Uses a high range to * avoid collisions with the server's request ids (low range). */ private sendIdCounter; /** Watchdog process that kills the backend group if this bridge dies (SIGKILL). */ private watchdog; protected constructor(opts: JsonRpcChildOptions); /** * Spawn a tiny detached watchdog that kills the backend process group if this * bridge process disappears. * * The detached/kill(-pid) cleanup in `close()` only runs when the bridge * exits cleanly enough for the signal handlers to fire (SIGTERM/SIGINT/etc). * If the bridge is SIGKILLed (Zed force-kill on reconnect, crash, OOM), the * handler never runs and the backend subprocess group is orphaned. The * watchdog closes that gap: it polls the bridge pid every 2s and, once the * bridge is gone, sends SIGKILL to the backend process group, then exits. * * The watchdog is its own process-group leader (detached) and `unref`'d, so * it never holds the event loop open and is not part of the backend group it * kills. It self-terminates as soon as the backend process exits, so a * normal shutdown leaves no lingering watchdog. */ private startWatchdog; private startReader; private route; /** Deliver a backend event to every listener registered for its session. */ protected dispatchEvent(ev: ZcodeEvent): void; private resolvePending; protected markReaderDead(reason: string): void; /** Route an inbound notification. Unhandled methods are ignored. */ protected abstract handleNotification(method: string, params: Record): void; /** * Intercept an unmatched server→client request before it queues. Return * true when consumed (e.g. a handshake auto-reply, or an arrival-time * responder) so the original never reaches the interaction queue verbatim. */ protected handleServerRequest(_req: ServerRequest): boolean; /** Adapt an outbound frame to the backend's wire dialect. */ protected decorateOutbound(msg: Record): Record; registerEventListener(sessionId: string, listener: EventListener): void; unregisterEventListener(sessionId: string, listener: EventListener): void; /** Non-blocking drain of pending server→client requests. */ pollServerRequests(): ServerRequest[]; /** * Re-queue server→client requests that belong to a different session (prepended * to preserve arrival order). Used by `handleServerRequests` to put back * requests it popped but doesn't own. */ requeueServerRequests(reqs: ServerRequest[]): void; /** Reply to a backend server→client request with a result (id + result). */ sendReply(id: number | string, result: unknown): void; /** Reply to a backend server→client request with an error. */ sendError(id: number | string, code: number, message: string): void; /** Fire-and-forget notification to the backend (no id, no response). */ notify(method: string, params?: Record): void; /** * Send a message with an id but WITHOUT registering a pending response * (fire-and-forget). Some backends route by id presence, so carrying an id * is more robust than a bare notify. If the backend replies, the reader's * `resolvePending` finds no pending entry and safely discards it. */ send(method: string, params?: Record): void; private writeFrame; /** * Synchronous request/response: register a pending promise, send, await. * Other notifications arriving during the wait are routed async by the * reader loop (they don't get swallowed). * * Returns `{error}` on dead backend, broken pipe, or timeout — never throws. */ request(id: number, method: string, params?: Record, timeoutMs?: number): Promise; /** * Kill the whole backend process group and wait for it to die. * * SIGTERM → wait up to 3s → SIGKILL if still alive. Note `proc.killed` is * NOT set by `process.kill(-pid)` (group signal), so we track liveness via * `exitCode === null` instead. Async so the caller can `await` a full reap * before the parent exits (an unref'd timer could be skipped on fast exit, * leaving orphans). */ close(): Promise; /** Terminate the watchdog process if it is still running. */ private killWatchdog; get isDead(): boolean; } //# sourceMappingURL=jsonrpc-child.d.ts.map