fix(ui): keep lobby/game caches fresh across screens (no stale-status flash)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 58s

The per-screen in-memory caches (lobbycache, gamecache) were refreshed only by
the screen that owns them while it was mounted, so a state change that crossed
screens left the other screen's cache stale and it visibly redrew on the next
navigation:

- game -> lobby: the player's own move advanced the game cache but not the lobby
  snapshot, and an own move carries no self-directed push event, so returning to
  the lobby painted the pre-move status until the background refresh corrected it.
- lobby -> game (and from any other screen): an opponent's move / game-over
  refreshed the lobby (while mounted) but never the per-game cache, so opening
  that game flashed the pre-move board.

Make cache freshness cross-screen, owned by the single global stream handler
that runs for every live event regardless of the mounted screen:

- patchLobbyGame upserts the affected game's GameView into the lobby snapshot;
  the global handler calls it on opponent_moved / game_over / opponent_joined and
  on a match_found / game_started seed (so a game started elsewhere is present
  too). The game board still mirrors the player's own move and its own load() —
  the two updates no live event carries.
- advanceCached (a pure wrapper over the existing delta reducers) advances a
  not-currently-viewed game's cache from opponent_moved / game_over; the game in
  view is skipped so its mounted board stays the sole owner (no double apply).

End-state behaviour is unchanged (the background refresh always reconciled);
this removes the transient stale frame. Unit-tested patchLobbyGame and
advanceCached; docs/UI_DESIGN.md updated.
This commit is contained in:
Ilia Denisov
2026-06-14 10:42:43 +02:00
parent 38fa104f7f
commit cf70e6b1fc
7 changed files with 196 additions and 12 deletions
+18
View File
@@ -24,6 +24,24 @@ export function setLobby(s: LobbySnapshot): void {
snapshot = s;
}
/**
* patchLobbyGame upserts the given game into the cached lobby — replacing the matching entry, or
* prepending it when absent — so a game-state change seen while the lobby is unmounted (the player's
* own move, or any live game event the global stream handler applies from another screen) is already
* reflected the next time the lobby renders from the snapshot, instead of showing the stale status
* until the background refresh lands. Prepending a freshly started game likewise lets it appear at
* once. It is a no-op only when there is no snapshot yet (the lobby cold-loads on its first mount).
* The lobby re-groups and re-sorts on render, so the insert position carries no meaning.
*/
export function patchLobbyGame(g: GameView): void {
if (!snapshot) return;
const i = snapshot.games.findIndex((x) => x.id === g.id);
const games = snapshot.games.slice();
if (i === -1) games.unshift(g);
else games[i] = g;
snapshot = { ...snapshot, games };
}
/** clearLobby drops the cached lobby (called on logout). */
export function clearLobby(): void {
snapshot = null;