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
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:
@@ -25,8 +25,9 @@ import { parseStartParam } from './deeplink';
|
||||
import { clearSession, loadPrefs, loadSession, saveSession, savePrefs } from './session';
|
||||
import { connection, reportOffline, reportOnline, resetConnection } from './connection.svelte';
|
||||
import { isConnectionCode } from './retry';
|
||||
import { clearGameCache, setCachedGame } from './gamecache';
|
||||
import { clearLobby } from './lobbycache';
|
||||
import { clearGameCache, getCachedGame, setCachedGame } from './gamecache';
|
||||
import { advanceCached } from './gamedelta';
|
||||
import { clearLobby, patchLobbyGame } from './lobbycache';
|
||||
import type { BoardLabelMode } from './boardlabels';
|
||||
|
||||
export interface Toast {
|
||||
@@ -144,6 +145,16 @@ export function handleError(err: unknown): void {
|
||||
showToast(t(code ? errorKey(code) : 'error.generic'), 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* viewingGame reports whether the game board for the given game id is the current route. That screen
|
||||
* maintains its own cache from live events, so the global stream handler must not also advance it (a
|
||||
* double application). The chat / dictionary sub-screens leave the board unmounted, so they do not
|
||||
* count as viewing — there the global handler keeps the cache warm for the return to the board.
|
||||
*/
|
||||
function viewingGame(gameId: string): boolean {
|
||||
return router.route.name === 'game' && router.route.params.id === gameId;
|
||||
}
|
||||
|
||||
function openStream(): void {
|
||||
closeStream();
|
||||
app.streamAlive = true;
|
||||
@@ -168,15 +179,36 @@ function openStream(): void {
|
||||
showToast(t('chat.nudge'), 'info');
|
||||
} else if (e.kind === 'your_turn') {
|
||||
showToast(t('game.yourTurn'), 'info');
|
||||
} else if (e.kind === 'opponent_moved' || e.kind === 'game_over') {
|
||||
// Keep both caches fresh from any screen, so neither the lobby nor the game flashes a stale
|
||||
// frame on the next navigation. The lobby snapshot is patched from the event's own GameView
|
||||
// (independent of whether this device has the game cached). The per-game cache is advanced
|
||||
// only for a game not currently in view — the mounted game board owns its own cache (see
|
||||
// game/Game.svelte), so skipping it avoids a double apply.
|
||||
if (e.game) patchLobbyGame(e.game);
|
||||
if (!viewingGame(e.gameId)) {
|
||||
const c = advanceCached(getCachedGame(e.gameId), e);
|
||||
if (c) setCachedGame(e.gameId, c.view, c.moves);
|
||||
}
|
||||
} else if (e.kind === 'opponent_joined') {
|
||||
// The opponent took an open game's empty seat: mirror the new status into the lobby snapshot
|
||||
// from any screen. A mounted game board adopts the change in place itself (game/Game.svelte).
|
||||
if (e.state) patchLobbyGame(e.state.game);
|
||||
} else if (e.kind === 'match_found') {
|
||||
// Seed the cache from the event's initial state so the game renders instantly on arrival,
|
||||
// then navigate.
|
||||
if (e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
// Seed both caches from the event's initial state so the game renders instantly on arrival
|
||||
// and the new game is already in the lobby on a later return, then navigate.
|
||||
if (e.state) {
|
||||
setCachedGame(e.state.game.id, e.state, []);
|
||||
patchLobbyGame(e.state.game);
|
||||
}
|
||||
navigate(`/game/${e.gameId}`);
|
||||
} else if (e.kind === 'notify') {
|
||||
// A started invited game seeds its cache so opening it is instant; the lobby badge stays
|
||||
// on the authoritative refresh.
|
||||
if (e.sub === 'game_started' && e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
// A started invited game seeds its cache so opening it is instant and lands in the lobby
|
||||
// snapshot from any screen; the lobby badge stays on the authoritative refresh.
|
||||
if (e.sub === 'game_started' && e.state) {
|
||||
setCachedGame(e.state.game.id, e.state, []);
|
||||
patchLobbyGame(e.state.game);
|
||||
}
|
||||
void refreshNotifications();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user