| 226 | } |
| 227 | |
| 228 | connect(wsUrl: string): void { |
| 229 | this.wsUrl = wsUrl; |
| 230 | if (this.ws?.readyState === WebSocket.OPEN) return; |
| 231 | |
| 232 | const url = new URL(wsUrl); |
| 233 | url.searchParams.set("sessionId", this.sessionId); |
| 234 | url.searchParams.set("token", this.token); |
| 235 | |
| 236 | this.ws = new WebSocket(url.toString()); |
| 237 | |
| 238 | this.ws.onopen = () => { |
| 239 | this.isConnected = true; |
| 240 | this.reconnectAttempts = 0; |
| 241 | this.onConnectionChange?.(true); |
| 242 | this.startPing(); |
| 243 | }; |
| 244 | |
| 245 | this.ws.onmessage = (event) => { |
| 246 | try { |
| 247 | const data = JSON.parse(event.data as string) as CollabEvent; |
| 248 | this.dispatch(data); |
| 249 | } catch { |
| 250 | // ignore malformed frames |
| 251 | } |
| 252 | }; |
| 253 | |
| 254 | this.ws.onclose = () => { |
| 255 | this.isConnected = false; |
| 256 | this.onConnectionChange?.(false); |
| 257 | this.stopPing(); |
| 258 | this.scheduleReconnect(); |
| 259 | }; |
| 260 | |
| 261 | this.ws.onerror = () => { |
| 262 | this.ws?.close(); |
| 263 | }; |
| 264 | } |
| 265 | |
| 266 | disconnect(): void { |
| 267 | // Set max attempts to prevent reconnect |