perf: reject mobile rpc waiters after retry cap (#4041)

This commit is contained in:
Neil 2026-05-31 02:12:53 -07:00 committed by GitHub
parent ea11f524b5
commit 35e1c221a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 1 deletions

View File

@ -479,4 +479,29 @@ describe('mobile rpc-client connection timeout', () => {
client.close()
})
it('rejects requests waiting for reconnect after the retry cap', async () => {
const client = connect('ws://desktop.invalid', 'token', 'server-key')
const socket = mockSockets[0]!
socket.open()
socket.receive(JSON.stringify({ type: 'e2ee_ready' }))
socket.receive('encrypted:{"type":"e2ee_authenticated"}')
socket.close()
const waitingRequestError = client.sendRequest('status.get').then(
() => null,
(error: Error) => error
)
await vi.runAllTimersAsync()
expect(client.getState()).toBe('reconnecting')
expect(client.getReconnectAttempt()).toBe(12)
await expect(waitingRequestError).resolves.toMatchObject({
message: 'Connection retry limit reached'
})
await expect(client.sendRequest('status.get')).rejects.toThrow('Connection retry limit reached')
client.close()
})
})

View File

@ -199,6 +199,13 @@ export function connect(
// in the logs.
let stateEnteredAt = Date.now()
function rejectConnectWaiters(reason: string) {
const error = new Error(reason)
for (const waiter of connectWaiters.splice(0)) {
waiter.reject(error)
}
}
function setState(next: ConnectionState) {
if (state === next) return
const prev = state
@ -218,7 +225,7 @@ export function connect(
} else if (next === 'disconnected' || next === 'auth-failed') {
const reason =
next === 'auth-failed' ? 'Unauthorized — pairing may be revoked' : 'Connection closed'
for (const w of connectWaiters.splice(0)) w.reject(new Error(reason))
rejectConnectWaiters(reason)
}
for (const listener of stateListeners) {
listener(next)
@ -239,6 +246,11 @@ export function connect(
function waitForConnected(): Promise<void> {
if (state === 'connected') return Promise.resolve()
if (intentionallyClosed) return Promise.reject(new Error('Client closed'))
if (state === 'reconnecting' && reconnectAttempt >= GIVE_UP_AFTER_ATTEMPTS && !reconnectTimer) {
// Why: after the retry cap there is no future state transition to
// release callers waiting before their per-request timeout starts.
return Promise.reject(new Error('Connection retry limit reached'))
}
return new Promise((resolve, reject) => {
connectWaiters.push({ resolve, reject })
})
@ -699,6 +711,7 @@ export function connect(
reason: 'give-up-cap',
endpoint: redactedEndpoint(endpoint)
})
rejectConnectWaiters('Connection retry limit reached')
return
}
const delay = RECONNECT_DELAYS[Math.min(reconnectAttempt, RECONNECT_DELAYS.length - 1)]!