fix(ui): keep lobby/game caches fresh across screens (no stale-status flash) #55

Merged
developer merged 1 commits from feature/ui-cross-screen-cache-freshness into development 2026-06-14 08:51:23 +00:00
Owner

Что и зачем

Кэши экранов (lobbycache, gamecache) обновлялись только тем экраном, который ими владеет, и только пока он смонтирован. Поэтому изменение состояния, пересекающее экраны, оставляло кэш другого экрана устаревшим — и он заметно перерисовывался при следующей навигации:

  • game → lobby: свой ход обновлял кэш игры, но не снапшот лобби, а на свой ход push-события игроку нет — возврат в лобби рисовал до-ходовый статус, пока фоновый refresh не поправит (тот самый замеченный мерцающий эффект).
  • lobby → game и с любого другого экрана: ход оппонента / game-over освежал лобби (пока оно открыто), но не кэш игры — открытие игры мигало до-ходовой доской.

Решение

Свежесть кэшей делается межэкранной, владелец — единый глобальный обработчик стрима, который выполняется на каждое live-событие независимо от смонтированного экрана:

  • patchLobbyGame (теперь upsert) вписывает GameView затронутой игры в снапшот лобби; глобальный обработчик зовёт его на opponent_moved / game_over / opponent_joined и при сидировании match_found / game_started (чтобы игра, начатая на другом экране, тоже была в лобби). Экран игры по-прежнему сам зеркалит свой ход и собственный load() — это два обновления, которых нет ни в одном событии.
  • advanceCached (чистая обёртка над уже существующими редьюсерами дельты) накатывает opponent_moved / game_over на кэш не открытой сейчас игры; игра в просмотре пропускается, чтобы её смонтированная доска осталась единственным владельцем (без двойного применения).

Поведение в конечном состоянии не меняется (фоновый refresh и раньше всё примирял) — убран переходный устаревший кадр. Контракты/wire/семантика событий не тронуты.

Граница (осознанно вне охвата)

  • Список приглашений и incoming в снапшоте лобби с третьего экрана не патчатся точечно (у них свой notify-путь + бейдж); следующий вход в лобби их cold-load'ит, как и раньше.
  • your_turn без сопровождающего opponent_moved (старый пир / потерянный payload) лобби не патчит — fallback на cold-load.
  • Прогрев кэша игры для off-screen opponent_joined отложен (редкий путь, cold-load на открытии покрывает) — кандидат на будущую «более глубокую» задачу.

Тесты

TDD (RED → GREEN): новые юниты patchLobbyGame (5) и advanceCached (7). Полный набор как в CI: check 0 ошибок · test:unit 176 · build ok · bundle-size в бюджете · e2e 114 (Chromium + WebKit).

## Что и зачем Кэши экранов (`lobbycache`, `gamecache`) обновлялись только тем экраном, который ими владеет, и только пока он смонтирован. Поэтому изменение состояния, пересекающее экраны, оставляло кэш другого экрана устаревшим — и он заметно перерисовывался при следующей навигации: - **game → lobby:** свой ход обновлял кэш игры, но не снапшот лобби, а на свой ход push-события игроку нет — возврат в лобби рисовал до-ходовый статус, пока фоновый refresh не поправит (тот самый замеченный мерцающий эффект). - **lobby → game и с любого другого экрана:** ход оппонента / game-over освежал лобби (пока оно открыто), но не кэш игры — открытие игры мигало до-ходовой доской. ## Решение Свежесть кэшей делается **межэкранной**, владелец — единый глобальный обработчик стрима, который выполняется на каждое live-событие независимо от смонтированного экрана: - `patchLobbyGame` (теперь **upsert**) вписывает `GameView` затронутой игры в снапшот лобби; глобальный обработчик зовёт его на `opponent_moved` / `game_over` / `opponent_joined` и при сидировании `match_found` / `game_started` (чтобы игра, начатая на другом экране, тоже была в лобби). Экран игры по-прежнему сам зеркалит **свой** ход и собственный `load()` — это два обновления, которых нет ни в одном событии. - `advanceCached` (чистая обёртка над уже существующими редьюсерами дельты) накатывает `opponent_moved` / `game_over` на кэш **не открытой сейчас** игры; игра в просмотре пропускается, чтобы её смонтированная доска осталась единственным владельцем (без двойного применения). Поведение в конечном состоянии не меняется (фоновый refresh и раньше всё примирял) — убран переходный устаревший кадр. Контракты/wire/семантика событий не тронуты. ## Граница (осознанно вне охвата) - Список приглашений и `incoming` в снапшоте лобби с третьего экрана не патчатся точечно (у них свой `notify`-путь + бейдж); следующий вход в лобби их cold-load'ит, как и раньше. - `your_turn` без сопровождающего `opponent_moved` (старый пир / потерянный payload) лобби не патчит — fallback на cold-load. - Прогрев кэша **игры** для off-screen `opponent_joined` отложен (редкий путь, cold-load на открытии покрывает) — кандидат на будущую «более глубокую» задачу. ## Тесты TDD (RED → GREEN): новые юниты `patchLobbyGame` (5) и `advanceCached` (7). Полный набор как в CI: `check` 0 ошибок · `test:unit` 176 · `build` ok · `bundle-size` в бюджете · e2e 114 (Chromium + WebKit).
developer added 1 commit 2026-06-14 08:43:15 +00:00
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
cf70e6b1fc
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.
owner approved these changes 2026-06-14 08:50:41 +00:00
developer merged commit 5312b11f0e into development 2026-06-14 08:51:23 +00:00
developer deleted branch feature/ui-cross-screen-cache-freshness 2026-06-14 08:51:23 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#55