import { type ChildProcess, spawn } from "node:child_process"; import path from "node:path"; /** * Thin TypeScript wrapper around e2e/peer/peer.py — the off-browser "mesh peer" * that talks to the non-browser node over the TCP phone API. It sends text, * blocks until a specific text is received, or reports the node's number. */ const PYTHON = process.env.E2E_PEER_PYTHON ?? path.resolve("e2e/peer/.venv/bin/python"); const SCRIPT = path.resolve("e2e/peer/peer.py"); const HOST = process.env.E2E_PEER_HOST ?? "127.0.0.1"; const PORT = process.env.E2E_PEER_PORT ?? "14404"; function spawnPeer(args: string[]): ChildProcess { return spawn(PYTHON, [SCRIPT, "--host", HOST, "--port", PORT, ...args]); } /** Invoke `onLine` for each complete stdout line. */ function onStdoutLines(child: ChildProcess, onLine: (line: string) => void): void { let buf = ""; child.stdout?.on("data", (chunk: Buffer) => { buf += chunk.toString(); let idx: number; while ((idx = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if (line) onLine(line); } }); } /** Send a text from the peer node. Broadcast unless `to` (a node number) is given. */ export function peerSend( text: string, opts: { to?: number; wantAck?: boolean } = {}, ): Promise { const args = ["send", text]; if (opts.to != null) args.push("--to", String(opts.to)); if (opts.wantAck) args.push("--want-ack"); const child = spawnPeer(args); let stderr = ""; child.stderr?.on("data", (d) => { stderr += d.toString(); }); return new Promise((resolve, reject) => { child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`peer send exited ${code}: ${stderr.trim()}`)), ); child.on("error", reject); }); } /** Read the peer node's own node number. */ export function peerNodeNum(): Promise { const child = spawnPeer(["node-num"]); let num: number | null = null; let stderr = ""; child.stderr?.on("data", (d) => { stderr += d.toString(); }); onStdoutLines(child, (line) => { const m = line.match(/^NODE_NUM=(\d+)/); if (m) num = Number(m[1]); }); return new Promise((resolve, reject) => { child.on("exit", (code) => num != null ? resolve(num) : reject(new Error(`peer node-num failed (${code}): ${stderr.trim()}`)), ); child.on("error", reject); }); } export type RecvHandle = { /** Resolves with the sender node number once the awaited text arrives. */ received: Promise; /** Kill the listener early. */ stop: () => void; }; /** * Start listening on the peer node for `text`. The returned promise resolves * once the listener is subscribed (so the caller can then trigger the browser * send without racing). The handle's `received` resolves when the text lands. */ export function startPeerRecv( text: string, opts: { fromNode?: number; timeout?: number } = {}, ): Promise { const { fromNode, timeout = 60 } = opts; const args = ["recv", text, "--timeout", String(timeout)]; if (fromNode != null) args.push("--from-node", String(fromNode)); const child = spawnPeer(args); let stderr = ""; child.stderr?.on("data", (d) => { stderr += d.toString(); }); let resolveReceived!: (n: number) => void; let rejectReceived!: (e: Error) => void; const received = new Promise((res, rej) => { resolveReceived = res; rejectReceived = rej; }); const handle: RecvHandle = { received, stop: () => child.kill() }; return new Promise((resolveReady, rejectReady) => { let from: number | null = null; onStdoutLines(child, (line) => { if (line === "READY") resolveReady(handle); const m = line.match(/^RECEIVED=(\d+)/); if (m) from = Number(m[1]); }); child.on("exit", (code) => { if (code === 0 && from != null) { resolveReceived(from); } else { const err = new Error( `peer recv exited ${code} (no match for "${text}"): ${stderr.trim()}`, ); rejectReady(err); rejectReceived(err); } }); child.on("error", (e) => { rejectReady(e); rejectReceived(e); }); }); }