Open the WebSocket connection. No-op while one is open or after close().
()
| 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 | |
| 127 | ws.onclose = (ev?: CloseEvent) => { |
| 128 | traceWsLifecycle('close', ev ? { code: ev.code, reason: ev.reason, wasClean: ev.wasClean } : undefined); |
| 129 | this.connected = false; |
| 130 | this.ws = null; |
| 131 | this.handlers.onConnectionState(false); |
| 132 | // Unexpected drop (daemon restart, sleep, network blip) → reconnect. |
| 133 | // onServerHello re-sends every kept subscription via client_hello, and |
| 134 | // the server answers a too-large seq gap with resync_required, so live |
| 135 | // updates resume without a page reload. |
| 136 | this.scheduleReconnect(); |
| 137 | }; |
| 138 | } |
| 139 | |
| 140 | private scheduleReconnect(): void { |
| 141 | if (this.closed || this.reconnectTimer !== null) return; |
no test coverage detected