| 388 | } |
| 389 | |
| 390 | async connect() { |
| 391 | if (this.connected) return; |
| 392 | if (this.connectPromise) return this.connectPromise; |
| 393 | |
| 394 | this._manualDisconnect = false; |
| 395 | this.connecting = true; |
| 396 | this.connectPromise = (async () => { |
| 397 | if (!this.socket) { |
| 398 | this.initializeSocket(); |
| 399 | } |
| 400 | |
| 401 | if (this.socket.connected) return; |
| 402 | |
| 403 | // Ensure the current runtime-bound session + CSRF cookies exist before initiating |
| 404 | // the Engine.IO handshake. This is required for seamless reconnect after backend |
| 405 | // restarts that rotate runtime_id and session cookie names. |
| 406 | try { |
| 407 | await getCsrfToken(); |
| 408 | } catch (error) { |
| 409 | this.debugLog("csrf prefetch failed - continuing", { |
| 410 | error: error instanceof Error ? error.message : String(error), |
| 411 | }); |
| 412 | } |
| 413 | |
| 414 | await new Promise((resolve, reject) => { |
| 415 | const onConnect = () => { |
| 416 | this.socket.off("connect_error", onError); |
| 417 | resolve(); |
| 418 | }; |
| 419 | const onError = (error) => { |
| 420 | this.socket.off("connect", onConnect); |
| 421 | reject(error instanceof Error ? error : new Error(String(error))); |
| 422 | }; |
| 423 | |
| 424 | this.socket.once("connect", onConnect); |
| 425 | this.socket.once("connect_error", onError); |
| 426 | this.socket.connect(); |
| 427 | }); |
| 428 | })() |
| 429 | .catch((error) => { |
| 430 | throw new Error(`WebSocket connection failed: ${error.message || error}`); |
| 431 | }) |
| 432 | .finally(() => { |
| 433 | this.connecting = false; |
| 434 | this.connectPromise = null; |
| 435 | }); |
| 436 | |
| 437 | return this.connectPromise; |
| 438 | } |
| 439 | |
| 440 | async disconnect() { |
| 441 | if (!this.socket) return; |