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:
+28
-9
@@ -1,15 +1,21 @@
|
||||
// In-memory per-game cache. A game the player has opened once is kept here so a
|
||||
// later re-entry renders instantly from the cache while a fresh fetch updates it in
|
||||
// the background — removing the blank "loading" flash and the full redraw on every
|
||||
// lobby <-> game navigation. It is intentionally process-memory only (no persistence):
|
||||
// stale entries are corrected by the background refresh, and the cache is cleared on
|
||||
// logout.
|
||||
// In-memory per-game cache. A game the player has opened once — or one warmed ahead of time
|
||||
// by the lobby preload (see lib/preload) — is kept here so opening it renders instantly from
|
||||
// the cache while a fresh fetch updates it in the background, removing the blank "loading"
|
||||
// flash and the full redraw on every lobby <-> game navigation. It is intentionally
|
||||
// process-memory only (no persistence): stale entries are corrected by the background
|
||||
// refresh, and the cache is cleared on logout.
|
||||
|
||||
import type { MoveRecord, StateView } from './model';
|
||||
|
||||
export interface CachedGame {
|
||||
view: StateView;
|
||||
moves: MoveRecord[];
|
||||
/**
|
||||
* The player's saved draft (composition) as the opaque JSON the server stores, so a warm
|
||||
* open paints the pending tiles already on the board with no rack→board jump. An empty
|
||||
* string means no draft; undefined means it has not been fetched for this entry yet.
|
||||
*/
|
||||
draft?: string;
|
||||
}
|
||||
|
||||
const cache = new Map<string, CachedGame>();
|
||||
@@ -19,9 +25,22 @@ export function getCachedGame(id: string): CachedGame | undefined {
|
||||
return cache.get(id);
|
||||
}
|
||||
|
||||
/** setCachedGame stores the latest state+history for a game. */
|
||||
export function setCachedGame(id: string, view: StateView, moves: MoveRecord[]): void {
|
||||
cache.set(id, { view, moves });
|
||||
/**
|
||||
* setCachedGame stores the latest state+history for a game. Omitting draft preserves the
|
||||
* cached composition: the live-event delta path advances view+moves without refetching the
|
||||
* draft and must not drop it, while passing an empty string clears it (a committed move does,
|
||||
* mirroring the server).
|
||||
*/
|
||||
export function setCachedGame(id: string, view: StateView, moves: MoveRecord[], draft?: string): void {
|
||||
const prev = cache.get(id);
|
||||
cache.set(id, { view, moves, draft: draft ?? prev?.draft });
|
||||
}
|
||||
|
||||
/** setCachedDraft updates only the saved draft of an already-cached game (a no-op otherwise),
|
||||
* keeping the cache coherent with the draft persisted while composing. */
|
||||
export function setCachedDraft(id: string, draft: string): void {
|
||||
const prev = cache.get(id);
|
||||
if (prev) cache.set(id, { ...prev, draft });
|
||||
}
|
||||
|
||||
/** clearGameCache drops every cached game (called on logout). */
|
||||
|
||||
Reference in New Issue
Block a user