()
| 141 | } |
| 142 | |
| 143 | if (this.authRecoveryAttempts >= MAX_AUTH_RECOVERY_ATTEMPTS) { |
| 144 | this.stopAfterUnauthorized(); |
| 145 | return true; |
| 146 | } |
| 147 | |
| 148 | const decision = await this.onUnauthorized(); |
| 149 | if (decision === 'stop' || this.destroyed) { |
| 150 | this.stopAfterUnauthorized(); |
| 151 | return true; |
| 152 | } |
| 153 | |
| 154 | this.authRecoveryAttempts++; |
| 155 | this.scheduleReconnect(); |
| 156 | return true; |
| 157 | } |
| 158 | |
| 159 | private stopAfterUnauthorized(): void { |
| 160 | this.destroyed = true; |
| 161 | if (this.reconnectTimer !== null) { |
| 162 | clearTimeout(this.reconnectTimer); |
| 163 | this.reconnectTimer = null; |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | private async connectOnce(): Promise<void> { |
| 168 | const gen = this.generation; |
| 169 | |
| 170 | // Close any existing socket to avoid leaking connections. |
| 171 | if (this.ws) { |
| 172 | const oldWs = this.ws; |
| 173 | this.ws = null; |
| 174 | oldWs.close(); |
| 175 | } |
| 176 | |
| 177 | const token = await this.getToken(); |
| 178 | if (gen !== this.generation || this.destroyed) return; |
| 179 | const ticket = await fetchConnectionTicket(this.url, token); |
| 180 | // disconnect() may have run while we were awaiting the token. Bail before |
| 181 | // creating the socket so we don't leak a WebSocket + ping timer past |
| 182 | // provider unmount (e.g. sign-out, navigation, strict-mode remount). |
| 183 | if (gen !== this.generation || this.destroyed) return; |
| 184 | |
| 185 | return new Promise((resolve, reject) => { |
| 186 | const ws = new WebSocket(connectUrlFor(this.url, ticket), [WEBSOCKET_PROTOCOL]); |
| 187 | this.ws = ws; |
| 188 | |
| 189 | // Guard against double-resolution: the handshake timeout, the |
| 190 | // WebSocket 'error' event, and disconnect() can all try to settle |
| 191 | // this promise. |
| 192 | let settled = false; |
| 193 | const settleResolve = (): void => { |
| 194 | if (settled) return; |
| 195 | settled = true; |
| 196 | this.clearHandshakeTimer(); |
| 197 | this.abortHandshake = null; |
| 198 | resolve(); |
| 199 | }; |
| 200 | const settleReject = (err: Error): void => { |
no test coverage detected