Files
scrabble-game/ui/src/lib/lobbycache.ts
T
Ilia Denisov 63ab85a5e5
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
feat(lobby): cap simultaneous quick games at 10
Limit a player to 10 active quick games (auto-match + AI); friend games created
by invitation are not counted. At the cap the backend refuses both new-game
entry points — quick enqueue and invitation creation — with 409
game_limit_reached, while accepting an incoming invitation stays allowed, so
friend games are capped from the other end. The lobby disables "New Game" and
shows a low-emphasis notice, driven by a new at_game_limit flag on games.list
(no per-event payload: a turn change does not move the count, and the lobby
already re-fetches games.list on entry and every game event).

- game.MaxActiveQuickGames + Store/Service.CountActiveQuickGames (active/open
  seats, no game_invitations row; hidden games still count -> dedicated count)
- Server.ensureUnderGameLimit gating handleEnqueue + handleCreateInvitation;
  game.ErrGameLimitReached -> 409 game_limit_reached
- FB GameList.at_game_limit (regenerated Go + TS) through the gateway transcode
  and UI codec; gameListDTO + lobbycache snapshot + Lobby.svelte + i18n
- tests: integration count rule + HTTP gate + accept bypass; server error map;
  gateway transcode round-trip; UI codec + lobbycache unit; e2e gamelimit
- docs: PRERELEASE (GL), FUNCTIONAL(+ru), ARCHITECTURE 8, UI_DESIGN, backend README
2026-06-16 22:51:18 +02:00

77 lines
3.4 KiB
TypeScript

// In-memory lobby snapshot, the lobby counterpart of gamecache.ts. The lobby re-fetches
// its lists on every entry, so without a cache the screen renders blank and "draws in"
// during the back-slide from a game. Caching the last lists lets the lobby render
// instantly (before/under the transition) and refresh in the background. Process-memory
// only; cleared on logout.
import type { AccountRef, GameView, Invitation } from './model';
interface LobbySnapshot {
games: GameView[];
invitations: Invitation[];
incoming: AccountRef[];
// atGameLimit rides the snapshot so the lobby renders the "New Game" button in its last
// known enabled/disabled state instantly (no flicker), then refreshes it in the background.
atGameLimit: boolean;
}
let snapshot: LobbySnapshot | null = null;
/** getLobby returns the last lobby lists, or null before the first load. */
export function getLobby(): LobbySnapshot | null {
return snapshot;
}
/** setLobby stores the latest lobby lists. */
export function setLobby(s: LobbySnapshot): void {
snapshot = s;
}
/**
* patchLobbyGame upserts the given game into the cached lobby — replacing the matching entry, or
* prepending it when absent — so a game-state change seen while the lobby is unmounted (the player's
* own move, or any live game event the global stream handler applies from another screen) is already
* reflected the next time the lobby renders from the snapshot, instead of showing the stale status
* until the background refresh lands. Prepending a freshly started game likewise lets it appear at
* once. It is a no-op only when there is no snapshot yet (the lobby cold-loads on its first mount).
* The lobby re-groups and re-sorts on render, so the insert position carries no meaning.
*/
export function patchLobbyGame(g: GameView): void {
if (!snapshot) return;
const i = snapshot.games.findIndex((x) => x.id === g.id);
const games = snapshot.games.slice();
if (i === -1) games.unshift(g);
else games[i] = g;
snapshot = { ...snapshot, games };
}
/**
* patchLobbyInvitation applies a live invitation change to the cached lobby's invitations list, so a
* change seen while the lobby is unmounted is reflected on its next render instead of a stale card
* lingering until the background refresh. A still-pending invitation is upserted (a new one
* prepended newest-first, an updated one replaced in place); an invitation that reached any terminal
* status (started, declined, cancelled, expired) is removed — the authoritative list holds only
* pending invitations. It is a no-op when there is no snapshot yet, or a terminal invitation is not
* in the list. The lobby re-renders from the snapshot, so the in-place upsert needs no re-sort.
*/
export function patchLobbyInvitation(inv: Invitation): void {
if (!snapshot) return;
const i = snapshot.invitations.findIndex((x) => x.id === inv.id);
let invitations: Invitation[];
if (inv.status !== 'pending') {
if (i === -1) return; // a terminal invitation already absent — nothing to remove
invitations = snapshot.invitations.filter((x) => x.id !== inv.id);
} else if (i === -1) {
invitations = [inv, ...snapshot.invitations];
} else {
invitations = snapshot.invitations.slice();
invitations[i] = inv;
}
snapshot = { ...snapshot, invitations };
}
/** clearLobby drops the cached lobby (called on logout). */
export function clearLobby(): void {
snapshot = null;
}