| 67 | } |
| 68 | |
| 69 | export class DaemonEventSocket { |
| 70 | private ws: WebSocket | null = null; |
| 71 | private connected = false; |
| 72 | private closed = false; |
| 73 | |
| 74 | /** subscriptions we manage: sessionId → last known cursor {seq, epoch} */ |
| 75 | private readonly subscriptions = new Map<string, SessionCursor>(); |
| 76 | |
| 77 | /** subscriptions queued while not yet connected */ |
| 78 | private readonly pendingSubscriptions: PendingSubscription[] = []; |
| 79 | private readonly terminalAttachments = new Map<string, TerminalAttachment>(); |
| 80 | |
| 81 | private msgSeq = 0; |
| 82 | |
| 83 | /** Automatic reconnect (exponential backoff, reset on a successful hello). */ |
| 84 | private reconnectAttempts = 0; |
| 85 | private reconnectTimer: ReturnType<typeof setTimeout> | null = null; |
| 86 | |
| 87 | constructor( |
| 88 | private readonly wsUrl: string, |
| 89 | private readonly clientId: string, |
| 90 | private readonly handlers: DaemonEventSocketHandlers, |
| 91 | ) {} |
| 92 | |
| 93 | /** Open the WebSocket connection. No-op while one is open or after close(). */ |
| 94 | connect(): void { |
| 95 | if (this.ws !== null || this.closed) return; |
| 96 | |
| 97 | traceWsLifecycle('connect', { url: this.wsUrl, attempt: this.reconnectAttempts }); |
| 98 | const credential = getCredential(); |
| 99 | const protocols = |
| 100 | credential !== undefined ? [`${WS_BEARER_PROTOCOL_PREFIX}${credential}`] : undefined; |
| 101 | const ws = new WebSocket(this.wsUrl, protocols); |
| 102 | this.ws = ws; |
| 103 | |
| 104 | ws.onopen = () => { |
| 105 | // Don't mark as connected yet — wait for server_hello |
| 106 | traceWsLifecycle('open'); |
| 107 | }; |
| 108 | |
| 109 | ws.onmessage = (ev: MessageEvent) => { |
| 110 | try { |
| 111 | const frame = JSON.parse(String(ev.data)) as WireServerFrame; |
| 112 | traceWsIn(frame); |
| 113 | this.handleFrame(frame); |
| 114 | } catch (error) { |
| 115 | traceWsLifecycle('parse-error', { error: String(error) }); |
| 116 | this.handlers.onError(0, `Failed to parse WS frame: ${String(error)}`, false); |
| 117 | } |
| 118 | }; |
| 119 | |
| 120 | ws.onerror = () => { |
| 121 | // The error details are not exposed by the browser WS API; the close |
| 122 | // event with a reason code follows immediately. |
| 123 | traceWsLifecycle('error'); |
| 124 | this.handlers.onError(0, 'WebSocket error', false); |
| 125 | }; |
| 126 |
nothing calls this directly
no outgoing calls
no test coverage detected