fix(mobile): revert 5s connect-timeout — was force-closing healthy in-progress pairs

The 5s CONNECT_TIMEOUT_MS added in #1481 was a speculative band-aid for
'WS sometimes never fires onopen' — but in practice it was force-closing
perfectly fine connections that hadn't completed by 5s (notably first-time
QR pairing on iOS, where Local Network permission prompt blocks the WS open
for several seconds).

Worse, the force-close transitioned state to 'reconnecting' (not
'disconnected'), so waitForConnected() never rejected and sendRequest()
hung. Combined with reconnect re-firing the timer, the result was an
infinite 'Connecting…' loop with no surfaced error.

Pair flows hit this hardest because they call connect() directly without
the shared RpcClientProvider, so there's no UI state listener to even show
'reconnecting' to the user — just the spinner.

Removing the timer entirely. The handshake-timeout (5s, post-onopen) still
catches genuinely stuck handshakes. If we hit a real 'never fires onopen'
case later, we can revisit with proper instrumentation rather than a
guess at the symptom.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-05-05 22:14:41 -04:00
co-authored by Orca
parent 9ab10fc8f9
commit a151142bed
-33
View File
@@ -35,13 +35,6 @@ export type RpcClient = {
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]
const REQUEST_TIMEOUT_MS = 30_000
const HANDSHAKE_TIMEOUT_MS = 5_000
// Why: belt-and-suspenders against React Native WebSocket implementations
// that occasionally never fire onerror/onclose for an unreachable host
// (observed when waking the device with stale DNS). Without this safety
// net the UI sat on 'Connecting…' forever and only a Metro reload
// recovered. Five seconds is a generous upper bound — a healthy LAN WS
// typically connects in <100ms.
const CONNECT_TIMEOUT_MS = 5_000
export function connect(
endpoint: string,
@@ -55,7 +48,6 @@ export function connect(
let reconnectAttempt = 0
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let handshakeTimer: ReturnType<typeof setTimeout> | null = null
let connectTimer: ReturnType<typeof setTimeout> | null = null
let intentionallyClosed = false
// Why: fresh ephemeral keypair per connection provides forward secrecy.
@@ -107,24 +99,7 @@ export function connect(
ws = new WebSocket(endpoint)
connectTimer = setTimeout(() => {
connectTimer = null
// Why: WS still stuck before 'open' — force-close so onclose fires
// and reconnect logic kicks in (rather than sitting on 'connecting'
// until the user reloads Metro). Safe even if the WS is mid-open;
// close() will trigger the 'closing' → 'closed' transitions.
try {
ws?.close()
} catch {
// ignore — onclose will still wire up reconnect
}
}, CONNECT_TIMEOUT_MS)
ws.onopen = () => {
if (connectTimer) {
clearTimeout(connectTimer)
connectTimer = null
}
reconnectAttempt = 0
setState('handshaking')
@@ -267,10 +242,6 @@ export function connect(
clearTimeout(handshakeTimer)
handshakeTimer = null
}
if (connectTimer) {
clearTimeout(connectTimer)
connectTimer = null
}
if (intentionallyClosed) {
setState('disconnected')
rejectAllPending('Connection closed')
@@ -411,10 +382,6 @@ export function connect(
clearTimeout(handshakeTimer)
handshakeTimer = null
}
if (connectTimer) {
clearTimeout(connectTimer)
connectTimer = null
}
if (ws) {
ws.close()
ws = null