feat(ui): offline-first native boot + lazy server-guest reconciliation
Native (Capacitor) cold boot with no cached session now enters as a
device-local guest in auto-offline mode and lands straight in the lobby
(never /login), so the app opens and plays local vs_ai / hotseat with the
APK's bundled dictionaries and zero network. When the gateway becomes
reachable, reconcileServerGuest silently mints + adopts a server guest and
clears the auto-offline, lighting up online features. Web / PWA / Telegram /
VK are byte-for-byte unchanged.
- transport: exec gains { silent, allowOffline } so background reconciliation
bypasses the offline kill switch (like the reachability probe) and never
raises the terminal update overlay (a too-old client stays a local guest);
new authGuestSilent on the client interface, the real transport and the mock.
- app: native no-session boot branch; reconcileServerGuest fired at boot, by
the recovery poll and the online event; the poll routes the session-less
guest through reconciliation, since checkReachable needs a token it lacks.
- native: initNativeShell tolerates a missing Capacitor bridge.
- e2e: new native.spec.ts (inject window.androidBridge; boot -> offline lobby,
local vs_ai move, reconcile -> online, hotseat start) + playwright.config
bundles the dawgs into dist-e2e/dict for the loader's bundled tier.
This commit is contained in:
+27
-5
@@ -61,8 +61,17 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
// exec runs one unary op, auto-retrying transient transport failures with capped backoff (so a
|
||||
// dropped connection or a rate-limit recovers seamlessly) and driving the global Connecting
|
||||
// indicator. A successful round-trip marks the gateway reachable; a domain result_code is final.
|
||||
async function exec(messageType: string, payload: Uint8Array, signal?: AbortSignal): Promise<Uint8Array> {
|
||||
assertOnline();
|
||||
async function exec(
|
||||
messageType: string,
|
||||
payload: Uint8Array,
|
||||
signal?: AbortSignal,
|
||||
opts?: { silent?: boolean; allowOffline?: boolean },
|
||||
): Promise<Uint8Array> {
|
||||
// allowOffline lets the background guest-reconciliation call reach the gateway while the app is
|
||||
// still in auto-offline mode (that call IS the reachability check); every other call honours the
|
||||
// kill switch. silent suppresses the terminal update overlay on update_required (the caller — the
|
||||
// reconciliation — swallows it and stays a local guest); foreground calls still raise it.
|
||||
if (!opts?.allowOffline) assertOnline();
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
let res;
|
||||
try {
|
||||
@@ -78,8 +87,9 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
throw toGatewayError(e);
|
||||
}
|
||||
const err = toGatewayError(e);
|
||||
// A too-old client turned away on a foreground call raises the terminal update overlay.
|
||||
if (err.code === UPDATE_REQUIRED) reportUpdateRequired();
|
||||
// A too-old client turned away on a foreground call raises the terminal update overlay; a
|
||||
// silent (background reconciliation) call swallows it and stays a local guest.
|
||||
if (err.code === UPDATE_REQUIRED && !opts?.silent) reportUpdateRequired();
|
||||
if (retryable(err.code, messageType) && attempt < MAX_RETRIES) {
|
||||
reportOffline();
|
||||
await sleep(backoffMs(attempt + 1));
|
||||
@@ -93,7 +103,7 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
// reload to pick up the (possibly incompatible) fresh client (maintenance.svelte.ts).
|
||||
maintenanceRecovered();
|
||||
if (res.resultCode && res.resultCode !== 'ok') {
|
||||
if (res.resultCode === UPDATE_REQUIRED) reportUpdateRequired();
|
||||
if (res.resultCode === UPDATE_REQUIRED && !opts?.silent) reportUpdateRequired();
|
||||
throw new GatewayError(res.resultCode);
|
||||
}
|
||||
return res.payload;
|
||||
@@ -145,6 +155,18 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
async authGuest(locale) {
|
||||
return codec.decodeSession(await exec('auth.guest', codec.encodeGuestLogin(locale ?? '', browserOffset(), platformSubtype())));
|
||||
},
|
||||
async authGuestSilent(locale) {
|
||||
// Background reconciliation of a native local guest gaining the network: it runs while the app is
|
||||
// still in auto-offline mode (allowOffline bypasses the kill switch) and must never raise the
|
||||
// terminal update overlay (silent) — a too-old client stays a local guest instead of being
|
||||
// interrupted mid-play (docs/ARCHITECTURE.md §2, the gate×offline rule).
|
||||
return codec.decodeSession(
|
||||
await exec('auth.guest', codec.encodeGuestLogin(locale ?? '', browserOffset(), platformSubtype()), undefined, {
|
||||
silent: true,
|
||||
allowOffline: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
async authEmailRequest(email, language, pwa) {
|
||||
await exec('auth.email.request', codec.encodeEmailRequest(email, browserOffset(), language, pwa));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user