/** * S-07 — state side-channel route. * * WS endpoint: GET /sessions/:id/side * * Pushes IC-1 ServerToClient JSON frames (state, session-meta, error). * Does NOT carry binary output (that's /stream). * Lightweight channel for UI state updates without full output stream. * * Owner: T-1.6 */ import type { IncomingMessage } from "node:http"; import type { Socket } from "node:net"; import type { StateEvent } from "../../pi/events.js"; import { getSession } from "../../tmux/manager.js"; import type { WsClient, WsServer } from "../types.js"; export interface SideRouteOptions { wss: WsServer; isAuthenticated: (req: IncomingMessage) => boolean; getCurrentState?: () => StateEvent | null; } // Subscribers per session: sessionId → set of ws clients const _subscribers = new Map>(); /** * Broadcast a state event to all side-channel subscribers of a session. */ export function broadcastState(sessionId: string, event: StateEvent): void { const subs = _subscribers.get(sessionId); if (!subs) return; const msg = JSON.stringify({ type: "state", value: event.value, tool: event.tool, ts: event.ts, }); for (const ws of subs) { if (ws.readyState === 1 /* OPEN */) { ws.send(msg); } } } /** * Handle a WebSocket upgrade for /sessions/:id/side. */ export function handleSideUpgrade( sessionId: string, request: IncomingMessage, socket: Socket, head: Buffer, opts: SideRouteOptions, ): void { opts.wss.handleUpgrade(request, socket, head, (ws: WsClient) => { handleSideConnection(sessionId, ws, opts); }); } async function handleSideConnection( sessionId: string, ws: WsClient, opts: SideRouteOptions, ): Promise { const session = await getSession(sessionId).catch(() => null); if (!session) { ws.send( JSON.stringify({ type: "error", code: "session_not_found", message: `Session "${sessionId}" not found`, }), ); ws.terminate(); return; } // Push session-meta on connect ws.send( JSON.stringify({ type: "session-meta", name: session.name, description: session.description, createdAt: session.createdAt, }), ); // Push current state const currentState = opts.getCurrentState?.(); if (currentState) { ws.send( JSON.stringify({ type: "state", value: currentState.value, tool: currentState.tool, ts: currentState.ts, }), ); } // Register subscriber let subs = _subscribers.get(sessionId); if (!subs) { subs = new Set(); _subscribers.set(sessionId, subs); } subs.add(ws); ws.on("close", () => { subs?.delete(ws); if (subs?.size === 0) _subscribers.delete(sessionId); }); ws.on("error", () => { subs?.delete(ws); }); }