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
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:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { advanceCached, applyGameOver, applyMoveDelta, seedInitialState, type MoveDelta } from './gamedelta';
|
||||
import { advanceCached, applyGameOver, applyMoveDelta, applyOpponentJoined, seedInitialState, type MoveDelta } from './gamedelta';
|
||||
import type { CachedGame } from './gamecache';
|
||||
import type { GameView, MoveRecord, PushEvent, StateView } from './model';
|
||||
|
||||
@@ -141,3 +141,30 @@ describe('advanceCached', () => {
|
||||
expect(advanceCached(cache(3, 0), yourTurn)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyOpponentJoined', () => {
|
||||
// joinedState is the refreshed StateView an opponent_joined event carries: the open game is now
|
||||
// active and both seats are filled.
|
||||
function joinedState(): StateView {
|
||||
const game: GameView = { ...gameView(0), status: 'active', players: 2, seats: [
|
||||
{ seat: 0, accountId: 'me', displayName: 'Me', score: 0, hintsUsed: 0, isWinner: false },
|
||||
{ seat: 1, accountId: 'opp', displayName: 'Opp', score: 0, hintsUsed: 0, isWinner: false },
|
||||
] };
|
||||
return { game, seat: 0, rack: ['x'], bagLen: 90, hintsRemaining: 0 };
|
||||
}
|
||||
|
||||
it("adopts the joined seats and status while preserving the cached rack and moves", () => {
|
||||
// The cached open game is still "searching": empty seats, status open, the starter's own rack.
|
||||
const cached: CachedGame = { view: { game: { ...gameView(2), status: 'open', seats: [] }, seat: 0, rack: ['a', 'b'], bagLen: 50, hintsRemaining: 1 }, moves: [move(0)] };
|
||||
const res = applyOpponentJoined(cached, joinedState());
|
||||
expect(res?.view.game.status).toBe('active');
|
||||
expect(res?.view.game.seats).toHaveLength(2);
|
||||
expect(res?.view.rack).toEqual(['a', 'b']); // the starter's rack is untouched by the join
|
||||
expect(res?.view.game.moveCount).toBe(2); // the cached count/board is preserved
|
||||
expect(res?.moves).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns undefined when nothing is cached for the game', () => {
|
||||
expect(applyOpponentJoined(undefined, joinedState())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user