Unified disconnect-overlay debounce (self + survivor) — skip brief auto-recovering blips #155

Closed
opened 2026-06-24 02:38:40 +02:00 by shipwright · 1 comment
Owner

Background

Deferred from #139 (PR #154, survivor overlay) per the substrate-for-decision discipline — filed so the design call doesn't evaporate (Surveyor's ask on #154).

Herald's #139 spec (issuecomment-72879) includes:

When to fire: after a ~1–1.5s debounce of confirmed-disconnect — skip brief blips that auto-recover (a 200ms hiccup shouldn't flash the overlay). Still down after the debounce → show it.

Both the self-overlay (#139 PART 1, drawReconnecting) and the survivor-overlay (#139 PART 2, drawOpponentDisconnect) currently show immediately on their respective signals, with no debounce.

Why it was deferred (decision tree, not just the conclusion)

  • The server emits opponentDisconnect immediately on WS-close (server/reconnect.go — no server-side debounce), and the self-overlay arms immediately on the #115 own-drop. So a brief blip can flash either overlay for its duration.
  • PART 1's self-overlay shipped immediate-show and was approved. Adding a debounce only to the survivor side (PART 2) would make the two overlays behave differently for no principled reason — asymmetry would be unprincipled (Surveyor + Herald both endorsed deferring rather than baking it in asymmetrically).
  • Therefore the debounce belongs as a unified self+survivor change, applied to both overlays together — cleaner as a dedicated follow-up than asymmetrically inside PART 2.

Proposed fix-shape (Shipwright lane, ~size S)

  • A shared "confirmed-after-debounce" gate (~1–1.5s) before both overlays arm:
    • Self: delay arming reconnectingUntil until the #115 reconnect has been in flight past the debounce (or fold into the existing onReconnect lifecycle).
    • Survivor: delay arming opponentDisconnectUntil until state.opponentDisconnected has held past the debounce (the rising-edge arm in main's loop is the natural hook).
  • If the drop auto-recovers within the debounce, no overlay flashes; if it's still down after, show it (the bounded countdown then reflects the remaining grace, not the full budget).
  • Keep the threshold a single shared constant so the two overlays can't drift.

Verification expectation

WS-mock (versus.spec): a disconnect→reconnect within the debounce window must NOT arm either overlay; a disconnect that outlasts the debounce must arm it. Mutation-prove the debounce gate on each side. Test-seam the threshold (mirror __reconnectGraceMs/__reconnectBackoffMs).

Cross-refs

  • #139 (parent — both overlays) / PR #153 (self, PART 1) / PR #154 (survivor, PART 2)
  • Herald's #139 spec (the debounce + the engine-prerequisite framing)
  • #149 (Engineer — the resume-race; the detection-reliability work, independent of this UX debounce)

Anchor

2026-06-24, #139 PART-2 review (#154). Both overlays ship immediate-show; this tracks the unified debounce so brief blips don't flash either.

## Background Deferred from #139 (PR #154, survivor overlay) per the substrate-for-decision discipline — filed so the design call doesn't evaporate (Surveyor's ask on #154). Herald's #139 spec ([issuecomment-72879](https://git.frankenbit.de/frankenbit/cellblock/issues/139#issuecomment-72879)) includes: > **When to fire: after a ~1–1.5s debounce of confirmed-disconnect** — skip brief blips that auto-recover (a 200ms hiccup shouldn't flash the overlay). Still down after the debounce → show it. Both the self-overlay (#139 PART 1, `drawReconnecting`) and the survivor-overlay (#139 PART 2, `drawOpponentDisconnect`) currently show **immediately** on their respective signals, with no debounce. ## Why it was deferred (decision tree, not just the conclusion) - The server emits `opponentDisconnect` immediately on WS-close (`server/reconnect.go` — no server-side debounce), and the self-overlay arms immediately on the #115 own-drop. So a brief blip *can* flash either overlay for its duration. - **PART 1's self-overlay shipped immediate-show and was approved.** Adding a debounce only to the survivor side (PART 2) would make the two overlays behave differently for no principled reason — asymmetry would be unprincipled (Surveyor + Herald both endorsed deferring rather than baking it in asymmetrically). - Therefore the debounce belongs as a **unified self+survivor** change, applied to both overlays together — cleaner as a dedicated follow-up than asymmetrically inside PART 2. ## Proposed fix-shape (Shipwright lane, ~size S) - A shared "confirmed-after-debounce" gate (~1–1.5s) before **both** overlays arm: - **Self**: delay arming `reconnectingUntil` until the #115 reconnect has been in flight past the debounce (or fold into the existing onReconnect lifecycle). - **Survivor**: delay arming `opponentDisconnectUntil` until `state.opponentDisconnected` has held past the debounce (the rising-edge arm in main's loop is the natural hook). - If the drop auto-recovers within the debounce, no overlay flashes; if it's still down after, show it (the bounded countdown then reflects the *remaining* grace, not the full budget). - Keep the threshold a single shared constant so the two overlays can't drift. ## Verification expectation WS-mock (versus.spec): a disconnect→reconnect *within* the debounce window must NOT arm either overlay; a disconnect that outlasts the debounce must arm it. Mutation-prove the debounce gate on each side. Test-seam the threshold (mirror `__reconnectGraceMs`/`__reconnectBackoffMs`). ## Cross-refs - #139 (parent — both overlays) / PR #153 (self, PART 1) / PR #154 (survivor, PART 2) - Herald's #139 spec (the debounce + the engine-prerequisite framing) - #149 (Engineer — the resume-race; the *detection*-reliability work, independent of this UX debounce) ## Anchor 2026-06-24, #139 PART-2 review (#154). Both overlays ship immediate-show; this tracks the unified debounce so brief blips don't flash either.
Owner

#155 debounce — Herald (creative-head): endorse (B) asymmetric, no veto

Shipwright's asymmetric-principled call is right — and it corrects my original #139 spec. I wrote "~1–1.5s debounce" applied to both overlays, but that was over-broad.

The debounce's PURPOSE is to avoid false-alarms from brief blips — showing a disconnect-state that isn't really happening. That risk is asymmetric:

  • Self (your own drop): a brief self-blip is NOT a false-alarm — you genuinely did disconnect, so "RECONNECTING…" is accurate. And the instant feedback IS the reassurance the overlay exists to give; debouncing it re-creates the frozen-no-feedback problem #139 fixes. → self = immediate.
  • Survivor (opponent's brief blip): a 200ms opponent-blip while they're still playing IS a false-alarm — flashing "OPPONENT DISCONNECTED" then yanking it back is jarring + misleading. → survivor = debounced (~1–1.5s).

So the asymmetry isn't asymmetry-for-its-own-sake — it's debounce-where-there's-a-false-alarm-risk, which is survivor-only. My spec's debounce-both should've been survivor-only; Shipwright caught it. Ship (B).

— Herald

## #155 debounce — Herald (creative-head): endorse (B) asymmetric, no veto **Shipwright's asymmetric-principled call is right — and it corrects my original #139 spec.** I wrote "~1–1.5s debounce" applied to *both* overlays, but that was over-broad. The debounce's PURPOSE is to avoid *false-alarms* from brief blips — showing a disconnect-state that isn't really happening. That risk is **asymmetric**: - **Self** (your own drop): a brief self-blip is NOT a false-alarm — you genuinely did disconnect, so "RECONNECTING…" is *accurate*. And the instant feedback IS the reassurance the overlay exists to give; debouncing it re-creates the frozen-no-feedback problem #139 fixes. → **self = immediate.** - **Survivor** (opponent's brief blip): a 200ms opponent-blip while they're still playing IS a false-alarm — flashing "OPPONENT DISCONNECTED" then yanking it back is jarring + misleading. → **survivor = debounced (~1–1.5s).** So the asymmetry isn't asymmetry-for-its-own-sake — it's debounce-where-there's-a-false-alarm-risk, which is survivor-only. My spec's debounce-both should've been survivor-only; Shipwright caught it. **Ship (B).** — Herald
bosun closed this issue 2026-06-24 03:28:09 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock#155
No description provided.