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:
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { applyGameOver, applyMoveDelta, seedInitialState, type MoveDelta } from './gamedelta';
|
||||
import { advanceCached, applyGameOver, applyMoveDelta, seedInitialState, type MoveDelta } from './gamedelta';
|
||||
import type { CachedGame } from './gamecache';
|
||||
import type { GameView, MoveRecord, StateView } from './model';
|
||||
import type { GameView, MoveRecord, PushEvent, StateView } from './model';
|
||||
|
||||
function gameView(moveCount: number, over = false): GameView {
|
||||
return {
|
||||
@@ -98,3 +98,46 @@ describe('applyGameOver', () => {
|
||||
expect(res.cache?.view.game.moveCount).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
function movedEvent(moveCount: number, player: number, bagLen = 45): PushEvent {
|
||||
return { kind: 'opponent_moved', gameId: 'g1', move: move(player), game: gameView(moveCount), bagLen };
|
||||
}
|
||||
|
||||
function gameOverEvent(moveCount: number): PushEvent {
|
||||
return { kind: 'game_over', gameId: 'g1', result: 'win', scoreLine: '10:0', game: gameView(moveCount, true) };
|
||||
}
|
||||
|
||||
describe('advanceCached', () => {
|
||||
it("advances the cache from an opponent's move for a game not on screen", () => {
|
||||
const res = advanceCached(cache(3, 0), movedEvent(4, 1, 42));
|
||||
expect(res?.view.game.moveCount).toBe(4);
|
||||
expect(res?.view.bagLen).toBe(42);
|
||||
expect(res?.moves).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns undefined on a gap, so the cache is left for the next open to cold-load', () => {
|
||||
expect(advanceCached(cache(3, 0), movedEvent(6, 1))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for an already-applied move (idempotent re-delivery / own echo)', () => {
|
||||
expect(advanceCached(cache(4, 0), movedEvent(4, 1))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('settles a game_over event when the cached board is current', () => {
|
||||
const res = advanceCached(cache(10, 0), gameOverEvent(10));
|
||||
expect(res?.view.game.status).toBe('finished');
|
||||
});
|
||||
|
||||
it('returns undefined for game_over when the cached board is behind the final count', () => {
|
||||
expect(advanceCached(cache(9, 0), gameOverEvent(10))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when nothing is cached for the game', () => {
|
||||
expect(advanceCached(undefined, movedEvent(1, 1))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores event kinds that do not advance a game board', () => {
|
||||
const yourTurn: PushEvent = { kind: 'your_turn', gameId: 'g1', deadlineUnix: 0, moveCount: 4 };
|
||||
expect(advanceCached(cache(3, 0), yourTurn)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user