Stage 17 #2: Connecting indicator + auto-retry, instead of red toasts
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s
Connectivity failures become state, not a toast on every attempt. A global online signal (lib/connection.svelte.ts) flips on a transport unavailable / rate_limited and on the live stream's drop, driving a pure-CSS header spinner + 'Connecting…' in place of the title and softly disabling the in-game server actions (commit / exchange / pass / hint; local board/rack/reset stay live). - transport: exec auto-retries with capped exponential backoff — every op on a rate-limit (rejected before processing, safe), reads only on unavailable (a mutation is never blindly re-sent, to avoid double-applying one whose response was lost; its button is disabled while offline so the player re-issues on reconnect). A reachability watcher (profile.get probe) and any successful traffic clear the signal. - the old red error.unavailable toast is gone (handleError suppresses connection codes; the indicator replaces it). A server-data screen still opens with the spinner and fills on reconnect (global indicator + read auto-retry), so navigation is never dead. - pure retry policy unit-tested (retry.ts); a mock-only window.__conn hook drives a Chromium+WebKit e2e (indicator shows offline, the action disables, both clear on reconnect). Full suite + build green. - docs: ARCHITECTURE transport note, FUNCTIONAL (+ _ru), PLAN tracker (incl. #1 — the bot already drains all updates, no change). Also records #1 as investigated/no-change in PLAN. Other server-action buttons (chat send, profile save, …) still degrade to a safe no-op offline; visual disable is easy to extend.
This commit is contained in:
+32
-7
@@ -10,6 +10,11 @@ import { createConnectTransport } from '@connectrpc/connect-web';
|
||||
import { Gateway } from '../gen/edge/v1/edge_pb';
|
||||
import { GatewayError, type GatewayClient } from './client';
|
||||
import * as codec from './codec';
|
||||
import { registerProbe, reportOffline, reportOnline } from './connection.svelte';
|
||||
import { backoffMs, isConnectionCode, retryable } from './retry';
|
||||
|
||||
const MAX_RETRIES = 6;
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
function toGatewayError(e: unknown): GatewayError {
|
||||
if (e instanceof ConnectError) {
|
||||
@@ -38,15 +43,35 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
const headers = (): Record<string, string> | undefined =>
|
||||
token ? { authorization: `Bearer ${token}` } : undefined;
|
||||
|
||||
// The reachability probe the connection watcher fires while offline: a cheap authenticated read
|
||||
// (it must reject when there is no session, so the watcher keeps waiting rather than reporting up).
|
||||
registerProbe(async () => {
|
||||
if (!token) throw new Error('no session');
|
||||
await client.execute({ messageType: 'profile.get', payload: codec.empty(), requestId: '' }, { headers: headers() });
|
||||
});
|
||||
|
||||
// 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): Promise<Uint8Array> {
|
||||
let res;
|
||||
try {
|
||||
res = await client.execute({ messageType, payload, requestId: '' }, { headers: headers() });
|
||||
} catch (e) {
|
||||
throw toGatewayError(e);
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
let res;
|
||||
try {
|
||||
res = await client.execute({ messageType, payload, requestId: '' }, { headers: headers() });
|
||||
} catch (e) {
|
||||
const err = toGatewayError(e);
|
||||
if (retryable(err.code, messageType) && attempt < MAX_RETRIES) {
|
||||
reportOffline();
|
||||
await sleep(backoffMs(attempt + 1));
|
||||
continue;
|
||||
}
|
||||
if (isConnectionCode(err.code)) reportOffline();
|
||||
throw err;
|
||||
}
|
||||
reportOnline();
|
||||
if (res.resultCode && res.resultCode !== 'ok') throw new GatewayError(res.resultCode);
|
||||
return res.payload;
|
||||
}
|
||||
if (res.resultCode && res.resultCode !== 'ok') throw new GatewayError(res.resultCode);
|
||||
return res.payload;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user