fix(web): declare a socket dead even when its probe cannot be sent (#17838)

* fix(web): declare a socket dead even when its probe cannot be sent

* chore: land the shared liveness policy with its first real adopter instead

* docs: drop the reference to a file this PR no longer adds
This commit is contained in:
Neil
2026-09-02 01:11:35 -07:00
committed by GitHub
parent e02347ae9b
commit c91edafec0
2 changed files with 38 additions and 3 deletions
@@ -0,0 +1,31 @@
import { describe, expect, it, vi } from 'vitest'
import { WebRuntimeConnectionHeartbeat } from './web-runtime-connection-heartbeat'
// A probe that cannot be written is the strongest evidence the link is gone. Gating the deadline on
// a successful send disarms the only branch that can declare the socket dead, so a saturated or
// half-open socket is never judged at all — the wedge fixed on the SSH transport in #17817.
describe('WebRuntimeConnectionHeartbeat when the probe cannot be sent', () => {
it('still declares the socket dead instead of watching it forever', () => {
let now = 0
const socket = { readyState: 1, close: vi.fn() } as unknown as WebSocket
const handleDeadSocket = vi.fn()
const heartbeat = new WebRuntimeConnectionHeartbeat({
now: () => now,
isDocumentVisible: () => true,
isConnected: () => true,
getSocket: () => socket,
// The saturated / half-open case: the send never leaves.
sendProbe: () => false,
handleDeadSocket
})
heartbeat.lastInboundFrameAt = 0
heartbeat.lastHeartbeatTickAt = 0
for (const tickAt of [10_000, 20_000, 30_000, 40_000, 50_000]) {
now = tickAt
heartbeat.runTick()
}
expect(handleDeadSocket).toHaveBeenCalledWith(socket)
})
})
@@ -69,9 +69,13 @@ export class WebRuntimeConnectionHeartbeat {
return
}
if (this.heartbeatProbeSentAt === null && now - this.lastInboundFrameAt >= HEARTBEAT_IDLE_MS) {
if (this.options.sendProbe()) {
this.heartbeatProbeSentAt = now
}
// Why the deadline is armed before the send and regardless of its result: a probe that could
// not be written is the strongest evidence the link is gone, not a reason to stop watching.
// Gating this on a successful send disarms the only branch above that can declare the socket
// dead, so a saturated or half-open socket would never be judged at all -- the same wedge
// fixed on the SSH transport in #17817. See also #17823.
this.heartbeatProbeSentAt = now
this.options.sendProbe()
}
}