feat(ui): preload ongoing games and cache the draft for an instant, jump-free game open
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 46s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m2s
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 46s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m2s
Opening a game from the lobby for the first time this session showed a brief loading flash, and every open showed a two-step rack->board jump: the saved draft (pending composition) was fetched separately and applied only after the board had already painted the full rack. Both stem from the full state and the draft not being available synchronously at first paint. Cache the draft alongside view+history (CachedGame.draft), make applyDraft take the already-fetched JSON so it runs synchronously, and fetch the draft in the same Promise.all as state+history. setCachedGame preserves the cached draft when the delta path omits it and clears it on a committed move (mirroring the server). A new preload module warms the per-game cache (state, history, draft) for the lobby's ongoing games with bounded concurrency, so opening any of them is instant. Tests: gamecache (preserve/clear/setCachedDraft) and preload (warm/skip) units; existing draft-restore e2e still green.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { GameView, StateView } from './model';
|
||||
|
||||
// Mock the gateway singleton so preload's fan-out is observed without a transport.
|
||||
const mocks = vi.hoisted(() => ({
|
||||
gameState: vi.fn(),
|
||||
gameHistory: vi.fn(),
|
||||
draftGet: vi.fn(),
|
||||
}));
|
||||
vi.mock('./gateway', () => ({
|
||||
gateway: { gameState: mocks.gameState, gameHistory: mocks.gameHistory, draftGet: mocks.draftGet },
|
||||
}));
|
||||
|
||||
import { preloadGames } from './preload';
|
||||
import { clearGameCache, getCachedGame, setCachedGame } from './gamecache';
|
||||
|
||||
function gameView(id: string, status: GameView['status'] = 'active'): GameView {
|
||||
return {
|
||||
id,
|
||||
variant: 'scrabble_en',
|
||||
dictVersion: 'v1',
|
||||
status,
|
||||
players: 2,
|
||||
toMove: 0,
|
||||
turnTimeoutSecs: 300,
|
||||
multipleWordsPerTurn: true,
|
||||
moveCount: 0,
|
||||
endReason: '',
|
||||
lastActivityUnix: 0,
|
||||
seats: [],
|
||||
};
|
||||
}
|
||||
|
||||
function stateView(id: string): StateView {
|
||||
return { game: gameView(id), seat: 0, rack: ['A', 'B'], bagLen: 50, hintsRemaining: 1 };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearGameCache();
|
||||
mocks.gameState.mockReset().mockImplementation((id: string) => Promise.resolve(stateView(id)));
|
||||
mocks.gameHistory.mockReset().mockImplementation((id: string) => Promise.resolve({ gameId: id, moves: [] }));
|
||||
mocks.draftGet.mockReset().mockImplementation((id: string) => Promise.resolve(id === 'g1' ? 'DRAFT1' : ''));
|
||||
});
|
||||
|
||||
describe('preloadGames', () => {
|
||||
it('warms ongoing, uncached games with state, history and draft', async () => {
|
||||
await preloadGames([gameView('g1'), gameView('g2')]);
|
||||
expect(getCachedGame('g1')?.view.game.id).toBe('g1');
|
||||
expect(getCachedGame('g1')?.draft).toBe('DRAFT1');
|
||||
expect(getCachedGame('g2')?.draft).toBe('');
|
||||
});
|
||||
|
||||
it('skips finished games', async () => {
|
||||
await preloadGames([gameView('done', 'finished')]);
|
||||
expect(getCachedGame('done')).toBeUndefined();
|
||||
expect(mocks.gameState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips games already in the cache (kept fresh by the live stream)', async () => {
|
||||
setCachedGame('g1', stateView('g1'), [], 'KEEP');
|
||||
await preloadGames([gameView('g1'), gameView('g2')]);
|
||||
expect(mocks.gameState).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.gameState).toHaveBeenCalledWith('g2', expect.any(Boolean));
|
||||
expect(getCachedGame('g1')?.draft).toBe('KEEP'); // untouched
|
||||
});
|
||||
|
||||
it('does nothing for an empty list', async () => {
|
||||
await preloadGames([]);
|
||||
expect(mocks.gameState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user