import type { SidecarCommand, SidecarEvent, PtyCommand, PtyEvent } from './protocol'; import type { SidecarRegistration, RegistrationAck } from './registration-protocol'; type AnyCommand = SidecarCommand | PtyCommand; type AnyEvent = SidecarEvent | PtyEvent; type SidecarConnectorConfig = { apiUrl: string; // ws://127.0.0.1:5000/api/sidecar/register name: string; capabilities: string[]; onCommand: (cmd: AnyCommand, reply: (msg: AnyEvent) => void) => void; onConnected?: () => void; onDisconnected?: () => void; }; type SidecarConnection = { send: (msg: AnyEvent) => void; destroy: () => void; isConnected: () => boolean; }; const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000]; export function createSidecarConnector(config: SidecarConnectorConfig): SidecarConnection { let ws: WebSocket | null = null; let connected = false; let reconnectAttempt = 0; let reconnectTimer: Timer | null = null; let destroyed = false; let sidecarId: string | null = null; function connect() { if (destroyed) return; if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return; try { ws = new WebSocket(config.apiUrl); } catch { scheduleReconnect(); return; } ws.onopen = () => { reconnectAttempt = 0; // Send registration const registration: SidecarRegistration = { type: 'register', name: config.name, capabilities: config.capabilities, }; ws!.send(JSON.stringify(registration)); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data as string); // Handle registration ack if (msg.type === 'registered') { const ack = msg as RegistrationAck; sidecarId = ack.id; connected = true; console.log(`[sidecar] registered with API server (id=${sidecarId})`); config.onConnected?.(); return; } // Handle commands from API server const reply = (response: AnyEvent) => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(response)); } }; config.onCommand(msg as AnyCommand, reply); } catch { // skip malformed messages } }; ws.onclose = () => { const wasConnected = connected; connected = false; sidecarId = null; ws = null; if (wasConnected) { console.log('[sidecar] disconnected from API server'); config.onDisconnected?.(); } scheduleReconnect(); }; ws.onerror = () => { // onclose will fire after this }; } function scheduleReconnect() { if (destroyed || reconnectTimer) return; const delay = RECONNECT_DELAYS[Math.min(reconnectAttempt, RECONNECT_DELAYS.length - 1)]!; reconnectAttempt++; reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, delay); } function send(msg: AnyEvent): void { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(msg)); } } function destroy(): void { destroyed = true; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (ws) { ws.close(); ws = null; } connected = false; } // Start connecting connect(); return { send, destroy, isConnected: () => connected, }; }