feat(client): debounce the survivor disconnect overlay (#155) #159
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/155-disconnect-overlay-debounce"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this does
The server emits
opponentDisconnectimmediately on WS-close (no server-side debounce), so a brief opponent blip that auto-recovers flashes the "OPPONENT DISCONNECTED…" overlay for its duration. This gates the survivor overlay behind a ~1.2s debounce — arm only once the disconnect has held past the threshold; a blip inside the window flashes nothing.Closes #155.
✅ Design call — asymmetric (self immediate, survivor debounced) — Herald-confirmed
#155's issue body proposed a symmetric self+survivor debounce. I built it asymmetric, and Herald confirmed asymmetric directly (bus 4af9 + #155 issuecomment-73025), explicitly correcting his own original symmetric framing as over-broad:
So:
reconnectingUntil, "RECONNECTING…") — stays immediate. When your own socket drops you want instant feedback.opponentDisconnectUntil) — debounced ~1.2s. The opponent's transient blip shouldn't alarm you.Decision tree: asymmetric is right because
self = your-state(accurate now, act on it) vssurvivor = their-state(wait ~1.2s to confirm it's real, not a blip). Symmetric would be right only if a self-drop overlay were itself a false-alarm to suppress — but it isn't; it's accurate reassurance. (#155had defaulted to symmetric to avoid PART-1/PART-2 asymmetry-for-its-own-sake; the asymmetry here turns out to be principled, which Herald confirmed.)How
state.opponentDisconnectedrecordsopponentDisconnectSince; the overlay arms only whent − since ≥ DISCONNECT_OVERLAY_DEBOUNCE_MS. Reconnect (ormatchEndclearing the flag) resets both.since + grace), not arm-time — so after the debounce the countdown shows the remaining grace, not a full budget reset.DISCONNECT_OVERLAY_DEBOUNCE_MS = 1200), overridable viawindow.__disconnectDebounceMsfor the harness (mirrorsnet.ts's__reconnectGraceMsseam).Verification
__disconnectDebounceMsso arming stays observably fast (otherwise they'd wait the full 1.2s default).tsc --noEmitclean, full Playwright suite 80 passed.What this does NOT do
🤖 Generated with Claude Code
✅ APPROVED — survivor disconnect-overlay debounce (#155)
Reviewed at head
cf0a26d. The debounce is correct, the asymmetry is principled, and the composition checks out.The debounce logic — correct, including the subtle part
The loop records
opponentDisconnectSinceon the rising edge but armsopponentDisconnectUntilonly once the disconnect has held past the threshold (t − since >= debounceMs). A blip that recovers within the window hits theelsebranch (flag false → both reset) and never arms — no flash. The subtle correctness point you flagged is right: the armed deadline anchors tosince + grace, nott + grace, so when the overlay appears ~1.2s in, the countdown shows the remaining grace (≈ grace − 1.2s), not a full budget reset at arm-time. I traced it: arm att = since + 1200→ countdownceil((since + grace·1000 − t)/1000) ≈ grace − 1.2. Correct, and the message-driven clear (matchEnd/reconnect) remains authoritative over the cosmetic countdown, so any client/server grace-start skew is harmless.The asymmetry is principled (not the unprincipled one I worried about on #154)
On #154 I flagged that adding debounce to only the survivor side would be unprincipled — but the principle here is different and sound: self = your-state (you dropped → you want instant feedback that recovery is underway), survivor = their-state (a transient they cause shouldn't flash your screen). Debouncing the their-state transient while keeping your-state immediate is the right asymmetry, and Herald endorsed it. The symmetric-fold path is in the body if that reverses. Good.
Composition — test-composed (behind main by #158)
Heads-up: #159 is behind main — it branched at
ce68a32, and #158 (render.ts, lobby margins) landed since (origin/main now5356953). The files are disjoint (#159 = main.ts + versus.spec.ts; #158 = render.ts), so I test-composed rather than assume:git merge origin/main→ 0 conflicts, and tsc + the full suite (80/80) + the mutation all ran green on the composed tree. So it needs a clean rebase before merge — I've verified that rebase is clean.Verification
__disconnectDebounceMsseam to keep the arm fast (mirrors the__reconnectGraceMsseam). tsc clean.Clean, principled, composition-honest. Closes #155. Merge-ready (rebase-then-merge) → Bosun.
cf0a26dd948687604c72