feat(lobby): keep lobby/game caches fresh from any screen + invitation delta channel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 48s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s

Builds on the cross-screen cache work: the global stream handler now keeps both
caches current no matter which screen is mounted, and invitations become a live
delta channel so the lobby's invitations list is fresh from any screen too.

Client (boundary already started):
- advanceCached now also folds opponent_joined into a not-currently-viewed game's
  cache via a new pure reducer applyOpponentJoined (extracted and reused by the
  mounted game board), so opening an open game that filled while you were elsewhere
  is flash-free.
- patchLobbyInvitation upserts a still-pending invitation and removes a terminal
  one (started/declined/cancelled/expired); the global notify handler calls it on
  the invitation / invitation_update sub-kinds.

Invitations delta channel (no wire/gateway/connector change — the notification
already carries the full invitation with id/status/game_id end to end):
- notify: a new in-app-only NotifyInvitationUpdate sub + NotificationInvitationUpdate
  constructor (shares encoding with NotificationInvitation). The Telegram connector
  renders no message for it, so a decline/cancel never becomes an out-of-app push.
- lobby: emit the changed invitation to every participant on respond (accept/decline),
  on the final accept's game start, and on cancel — so each participant's lobby patches
  its list in place. The authoritative list holds only pending invitations, so the
  client's pending-vs-terminal rule matches it exactly.

Tests: applyOpponentJoined + patchLobbyInvitation unit tests (TDD), the
NotificationInvitationUpdate encoding unit test, and integration assertions that
decline/cancel/accept publish invitation_update to every participant. Full local
suite green (backend unit+integration, UI check/unit/build/bundle/e2e). Docs:
ARCHITECTURE §10 (notify catalog + in-app-only note) and UI_DESIGN updated.
This commit is contained in:
Ilia Denisov
2026-06-14 11:22:47 +02:00
parent 5312b11f0e
commit 56dbf86472
13 changed files with 269 additions and 22 deletions
+25
View File
@@ -42,6 +42,31 @@ export function patchLobbyGame(g: GameView): void {
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;