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
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
77 lines
3.4 KiB
TypeScript
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;
|
|
}
|