Browser back-button breaks cellblock state: mobile stuck on CELLMATE FOUND, desktop jumps to Scanning #109

Closed
opened 2026-06-22 21:10:19 +02:00 by bosun · 1 comment
Owner

Behavior (operator iPhone+desktop multiplayer playtest 2026-06-22, deployed round-18)

Browser back/forward navigation interacts badly with cellblock state:

Reproduction sequence (two-client versus session):

  1. Connect mobile + desktop clients into multiplayer session
  2. Both reach CELLMATE FOUND screen
  3. On mobile: press browser backward button
  4. On mobile: press browser forward button (returns to CELLMATE FOUND)
  5. On mobile: press browser backward button again

Observed result:

  • Desktop client: jumps to "Scanning the yard for a cellmate..." (as if matchmaking re-started)
  • Mobile client: stays at "CELLMATE FOUND" screen regardless of further desktop navigation
  • Mobile screen "sticks" — no matter what navigation happens on desktop, mobile doesn't update

Side observation:

  • There are TWO browser navigation buttons visible (backward + forward — Safari toolbar)
  • Forward button seems to work as intended (returns to expected screen)
  • Backward button breaks navigation / causes state desync

Severity: substantive

Substrate-state desync between server-state and client-displayed-state. Mobile gets "stuck" with no apparent recovery without page reload. Affects multiplayer playability when either player accidentally uses browser back-button.

Likely root cause (NEEDS PROBE)

Cellblock client probably doesn't handle browser history API events (popstate). Browser back-button moves through history entries without notifying the app's state-machine; the app doesn't realize the user "navigated back" and doesn't update its UI accordingly.

Distinct from cellblock#93 (ws.onclose latent gap) — though related family. #93 is socket-drop-handler-missing; this is browser-history-event-handler-missing. Both produce "stuck" client state but via different mechanisms.

Fix-direction (Shipwright lane, probe-first)

  1. Probe: does cellblock use any history.pushState / replaceState / popstate handling currently?
  2. If no: add popstate listener that maps browser-history-changes to app-state transitions (or replaces history entries with current state to suppress browser-navigation entirely)
  3. Consider: replace approach (replaceState every screen transition → browser-history never accumulates entries → back-button does nothing in-app) is simpler than handling popstate

Acceptance criteria

  1. Probe identifies current history-API usage in cellblock client
  2. Browser back/forward buttons either (a) do nothing in-app (replaceState approach) OR (b) trigger correct app-state transitions (popstate approach)
  3. No client-state desync between server-state and displayed-state regardless of browser navigation
  4. Versus playability preserved (other player doesn't get stuck if one navigates back)
  5. Harness #81 extended (likely depends on #92 WS-mock for full versus coverage)

Cross-refs

  • cellblock#93 (ws.onclose; related but distinct mechanism — ws-drop vs browser-history)
  • cellblock#99 (mobile MP layout; same playtest session)
  • Operator playtest 2026-06-22 (post round-18, multi-client versus)

Anchor

2026-06-22 operator iPhone+desktop versus playtest. Empirically reproduced + side-observation distinguishing forward (works) vs backward (breaks).

## Behavior (operator iPhone+desktop multiplayer playtest 2026-06-22, deployed round-18) Browser back/forward navigation interacts badly with cellblock state: **Reproduction sequence** (two-client versus session): 1. Connect mobile + desktop clients into multiplayer session 2. Both reach CELLMATE FOUND screen 3. On mobile: press browser **backward** button 4. On mobile: press browser **forward** button (returns to CELLMATE FOUND) 5. On mobile: press browser **backward** button again **Observed result**: - Desktop client: jumps to "Scanning the yard for a cellmate..." (as if matchmaking re-started) - Mobile client: stays at "CELLMATE FOUND" screen regardless of further desktop navigation - Mobile screen "sticks" — no matter what navigation happens on desktop, mobile doesn't update **Side observation**: - There are TWO browser navigation buttons visible (backward + forward — Safari toolbar) - **Forward** button seems to work as intended (returns to expected screen) - **Backward** button breaks navigation / causes state desync ## Severity: substantive Substrate-state desync between server-state and client-displayed-state. Mobile gets "stuck" with no apparent recovery without page reload. Affects multiplayer playability when either player accidentally uses browser back-button. ## Likely root cause (NEEDS PROBE) Cellblock client probably doesn't handle browser history API events (popstate). Browser back-button moves through history entries without notifying the app's state-machine; the app doesn't realize the user "navigated back" and doesn't update its UI accordingly. Distinct from cellblock#93 (ws.onclose latent gap) — though related family. #93 is socket-drop-handler-missing; this is browser-history-event-handler-missing. Both produce "stuck" client state but via different mechanisms. ## Fix-direction (Shipwright lane, probe-first) 1. **Probe**: does cellblock use any history.pushState / replaceState / popstate handling currently? 2. If no: add popstate listener that maps browser-history-changes to app-state transitions (or replaces history entries with current state to suppress browser-navigation entirely) 3. Consider: replace approach (replaceState every screen transition → browser-history never accumulates entries → back-button does nothing in-app) is simpler than handling popstate ## Acceptance criteria 1. **Probe** identifies current history-API usage in cellblock client 2. Browser back/forward buttons either (a) do nothing in-app (replaceState approach) OR (b) trigger correct app-state transitions (popstate approach) 3. No client-state desync between server-state and displayed-state regardless of browser navigation 4. Versus playability preserved (other player doesn't get stuck if one navigates back) 5. Harness #81 extended (likely depends on #92 WS-mock for full versus coverage) ## Cross-refs - cellblock#93 (ws.onclose; related but distinct mechanism — ws-drop vs browser-history) - cellblock#99 (mobile MP layout; same playtest session) - Operator playtest 2026-06-22 (post round-18, multi-client versus) ## Anchor 2026-06-22 operator iPhone+desktop versus playtest. Empirically reproduced + side-observation distinguishing forward (works) vs backward (breaks).
Owner

AC-tick (pre-merge) — addressed in PR #117 (head bcaa4af):

  1. Probegrep for pushState|replaceState|popstate|hashchange|history.|location.hash in client/src/empty. Zero history-API usage confirmed at source.
  2. Back/Forward do nothing in-app — suppression via a boot pushState sentinel + popstate re-push (AC2 option (a)). Note: pure replaceState (the tracker's lean) does not achieve this — with zero accumulated in-app entries there's nothing to replace away, so Back still unloads the page; the pushState-sentinel trap is what actually makes Back a same-document no-op. Full reasoning in the PR decision tree.
  3. No state desync — the document never unloads, so server-state ↔ displayed-state stay consistent. Test asserts screen/phase unchanged across two Back presses (the operator's back→fwd→back sequence).
  4. Versus playability preserved — Back no longer drops our socket, so the opponent is never re-queued by an accidental Back (the desktop "Scanning the yard" jump the operator saw was a consequence of the mobile socket dropping).
  5. Harness #81 extended#109 versus.spec test drives history.back() against the #92 WS-mock; mutation-proven (remove the popstate listener → reds). 55/55 green, tsc clean.

Mechanism: pushState sentinel + popstate re-trap (NOT pure replaceState). No-op suppression (NOT Back→leave) per the operator's ratified direction. Awaiting Surveyor review + CI green before merge.

**AC-tick (pre-merge)** — addressed in PR #117 (head `bcaa4af`): 1. ✅ **Probe** — `grep` for `pushState|replaceState|popstate|hashchange|history.|location.hash` in `client/src/` → **empty**. Zero history-API usage confirmed at source. 2. ✅ Back/Forward **do nothing in-app** — suppression via a boot `pushState` sentinel + `popstate` re-push (AC2 option (a)). Note: pure `replaceState` (the tracker's lean) does *not* achieve this — with zero accumulated in-app entries there's nothing to replace away, so Back still unloads the page; the `pushState`-sentinel trap is what actually makes Back a same-document no-op. Full reasoning in the PR decision tree. 3. ✅ **No state desync** — the document never unloads, so server-state ↔ displayed-state stay consistent. Test asserts screen/phase unchanged across two Back presses (the operator's back→fwd→back sequence). 4. ✅ **Versus playability preserved** — Back no longer drops our socket, so the opponent is never re-queued by an accidental Back (the desktop "Scanning the yard" jump the operator saw was a *consequence* of the mobile socket dropping). 5. ✅ **Harness #81 extended** — `#109` `versus.spec` test drives `history.back()` against the #92 WS-mock; mutation-proven (remove the `popstate` listener → reds). 55/55 green, `tsc` clean. Mechanism: `pushState` sentinel + `popstate` re-trap (NOT pure `replaceState`). No-op suppression (NOT Back→leave) per the operator's ratified direction. Awaiting Surveyor review + CI green before merge.
bosun closed this issue 2026-06-23 09:44:50 +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#109
No description provided.