R4: push enrichment — events carry a state delta, kill the last poll
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Enrich the in-app live stream into a delta channel so the UI renders a move from the event without a follow-up game.state, and make the matchmaking poll a stream-down fallback. - pkg/fbs: trailing fields on opponent_moved (move+game+bag_len), your_turn (move_count), match_found (state), game_over (game), notify (account/invitation/state), MoveResult (rack+bag_len); regenerate Go + TS. - backend: notify owns the FB encoding (encode.go + payload.go input structs); game/lobby/social map their domain types in. emitMove builds the move delta; game.Service.InitialState feeds match_found/game_started the recipient's initial StateView; friends/invitations notify carry their account/invitation. The move-commit response (submit_play/pass/exchange/resign) returns the actor's refilled rack + bag size. - gateway: MoveResult transcode carries rack+bag_len. - ui: pure lib/gamedelta.ts reducer advances the per-game cache keyed on move_count (idempotent + gap-safe); app.svelte seeds the cache on match_found/game_started; Game.svelte applies the delta (commit/pass/exchange/resign drop their load()); NewGame polls only while app.streamAlive is false. - docs: ARCHITECTURE §10, FUNCTIONAL(+ru), backend/gateway/ui READMEs; PRERELEASE R4 marked done + Refinements.
This commit is contained in:
@@ -25,7 +25,7 @@ import { parseStartParam } from './deeplink';
|
||||
import { clearSession, loadPrefs, loadSession, saveSession, savePrefs } from './session';
|
||||
import { connection, reportOffline, reportOnline, resetConnection } from './connection.svelte';
|
||||
import { isConnectionCode } from './retry';
|
||||
import { clearGameCache } from './gamecache';
|
||||
import { clearGameCache, setCachedGame } from './gamecache';
|
||||
import { clearLobby } from './lobbycache';
|
||||
import type { BoardLabelMode } from './boardlabels';
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface Toast {
|
||||
|
||||
export const app = $state<{
|
||||
ready: boolean;
|
||||
/** Whether the live-event stream is connected; drives the matchmaking poll fallback (R4). */
|
||||
streamAlive: boolean;
|
||||
session: Session | null;
|
||||
profile: Profile | null;
|
||||
toast: Toast | null;
|
||||
@@ -53,6 +55,7 @@ export const app = $state<{
|
||||
chatUnread: Record<string, number>;
|
||||
}>({
|
||||
ready: false,
|
||||
streamAlive: false,
|
||||
session: null,
|
||||
profile: null,
|
||||
toast: null,
|
||||
@@ -68,7 +71,6 @@ export const app = $state<{
|
||||
});
|
||||
|
||||
let unsubscribeStream: (() => void) | null = null;
|
||||
let streamAlive = false;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -100,7 +102,7 @@ function goForeground(): void {
|
||||
backgrounded = false;
|
||||
foregroundedAt = Date.now();
|
||||
if (!app.session) return;
|
||||
if (!streamAlive) openStream(); // silently re-establish a stream dropped while away
|
||||
if (!app.streamAlive) openStream(); // silently re-establish a stream dropped while away
|
||||
void refreshNotifications();
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ export function handleError(err: unknown): void {
|
||||
|
||||
function openStream(): void {
|
||||
closeStream();
|
||||
streamAlive = true;
|
||||
app.streamAlive = true;
|
||||
unsubscribeStream = gateway.subscribe(
|
||||
(e) => {
|
||||
reportOnline(); // a delivered event proves the gateway is reachable
|
||||
@@ -151,13 +153,19 @@ function openStream(): void {
|
||||
} else if (e.kind === 'your_turn') {
|
||||
showToast(t('game.yourTurn'), 'info');
|
||||
} else if (e.kind === 'match_found') {
|
||||
// Seed the cache from the event's initial state so the game renders instantly on arrival,
|
||||
// then navigate (R4).
|
||||
if (e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
navigate(`/game/${e.gameId}`);
|
||||
} else if (e.kind === 'notify') {
|
||||
// A started invited game seeds its cache so opening it is instant; the lobby badge stays
|
||||
// on the authoritative refresh (R4).
|
||||
if (e.sub === 'game_started' && e.state) setCachedGame(e.state.game.id, e.state, []);
|
||||
void refreshNotifications();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
streamAlive = false;
|
||||
app.streamAlive = false;
|
||||
// A background suspend drops the single-shot stream. Keep the indicator hidden while
|
||||
// backgrounded or just-resumed (bannerSuppressed); otherwise show "Connecting…" (the
|
||||
// reachability watcher and this scheduled retry recover it). Always schedule a retry.
|
||||
@@ -173,7 +181,7 @@ function scheduleReconnect(): void {
|
||||
if (reconnectTimer || !app.session) return;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
if (app.session && !streamAlive && !backgrounded && !documentHidden()) openStream();
|
||||
if (app.session && !app.streamAlive && !backgrounded && !documentHidden()) openStream();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
@@ -205,7 +213,7 @@ function closeStream(): void {
|
||||
}
|
||||
unsubscribeStream?.();
|
||||
unsubscribeStream = null;
|
||||
streamAlive = false;
|
||||
app.streamAlive = false;
|
||||
}
|
||||
|
||||
async function adoptSession(s: Session): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user