import { createConnection } from 'node:net' import { randomUUID } from 'node:crypto' import { findTransport, type RuntimeMetadata } from '../../shared/runtime-bootstrap' import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope' import { isKeepaliveFrame, RuntimeRpcEnvelopeSchema } from './envelope-schema' import { RuntimeClientError, type RuntimeRpcResponse } from './types' import { MAX_TIMER_DELAY_MS, isSafeTimerDelayMs } from '../../shared/timer-delay' export async function sendRequest( metadata: RuntimeMetadata, method: string, params: unknown, timeoutMs: number, envelope?: RuntimeOrchestrationEnvelope ): Promise> { if (!isSafeTimerDelayMs(timeoutMs)) { throw new RuntimeClientError( 'invalid_argument', `Runtime request timeout must be an integer between 0 and ${MAX_TIMER_DELAY_MS}ms.` ) } return await new Promise((resolve, reject) => { const transport = findTransport(metadata, 'unix', 'named-pipe') if (!transport) { reject( new RuntimeClientError( 'runtime_unavailable', 'No compatible transport found in Orca runtime metadata.' ) ) return } const socket = createConnection(transport.endpoint) let buffer = '' let settled = false const requestId = randomUUID() const timeout = setTimeout(() => { if (settled) { return } settled = true socket.destroy() reject( new RuntimeClientError( 'runtime_timeout', 'Timed out waiting for the Orca runtime to respond.' ) ) }, timeoutMs) const finish = ( result: { ok: true; response: RuntimeRpcResponse } | { ok: false; error: Error } ): void => { if (settled) { return } settled = true clearTimeout(timeout) socket.end() if (result.ok === false) { reject(result.error) } else { resolve(result.response) } } socket.setEncoding('utf8') socket.once('error', () => { finish({ ok: false, error: new RuntimeClientError( 'runtime_unavailable', 'Could not connect to the running Orca app. Restart Orca and try again.' ) }) }) // Why: a clean peer close (FIN, no 'error') before a terminal frame never // settles the promise, so the call would otherwise hang until the full // timeout fires. Reject promptly. finish() guards double-settle, so this // no-ops on the normal success/error paths that already called socket.end(). socket.once('close', () => { finish({ ok: false, error: new RuntimeClientError( 'runtime_unavailable', 'The Orca runtime closed the connection before responding. Restart Orca and try again.' ) }) }) socket.on('data', (chunk: string) => { buffer += chunk // Why: the server may interleave `{"_keepalive":true}\n` frames with the // final success/failure frame to keep both idle timers alive during a // long-poll (see design doc §3.1). Read frames in a loop until we see a // terminal frame. Each keepalive refreshes the client-side timer so a // 10 min wait doesn't trip the 60 s default ceiling. let newlineIndex = buffer.indexOf('\n') while (newlineIndex !== -1 && !settled) { const line = buffer.slice(0, newlineIndex) buffer = buffer.slice(newlineIndex + 1) if (line.trim().length === 0) { newlineIndex = buffer.indexOf('\n') continue } let raw: unknown try { raw = JSON.parse(line) } catch { finish({ ok: false, error: new RuntimeClientError( 'invalid_runtime_response', 'The Orca runtime returned an invalid response frame.' ) }) return } // Fast-path: ignore keepalives without running the full schema. // setTimeout().refresh() is stable since Node 10 (Orca ships on // Node 20+ via Electron and the standalone CLI targets the same // major). See §7 risk #9. if (isKeepaliveFrame(raw)) { timeout.refresh() newlineIndex = buffer.indexOf('\n') continue } // Why: validate the envelope shape (id, ok, result/error, _meta) at // the decode boundary so version skew between the CLI and the Orca // main runtime surfaces as a single invalid_runtime_response instead // of a downstream mis-typed field access. `result` is left as // unknown — the TResult generic is the caller's responsibility. const parsed = RuntimeRpcEnvelopeSchema.safeParse(raw) if (!parsed.success) { finish({ ok: false, error: new RuntimeClientError( 'invalid_runtime_response', 'The Orca runtime returned an invalid response frame.' ) }) return } // Narrow out keepalive (already filtered above) so TS can see a // Success|Failure shape here. const frame = parsed.data if ('_keepalive' in frame) { timeout.refresh() newlineIndex = buffer.indexOf('\n') continue } const response = frame as RuntimeRpcResponse if (response.id !== requestId) { finish({ ok: false, error: new RuntimeClientError( 'invalid_runtime_response', 'The Orca runtime returned a mismatched response id.' ) }) return } if (response._meta?.runtimeId && response._meta.runtimeId !== metadata.runtimeId) { finish({ ok: false, error: new RuntimeClientError( 'runtime_unavailable', 'The Orca runtime changed while the request was in flight. Retry the command.' ) }) return } finish({ ok: true, response }) return } }) socket.on('connect', () => { socket.write( `${JSON.stringify({ id: requestId, authToken: metadata.authToken, method, params, orchestrationCapability: envelope?.orchestrationCapability, orchestrationContractVersion: envelope?.orchestrationContractVersion, orchestrationRequestId: envelope?.orchestrationRequestId })}\n` ) }) }) }