Stage 17 #2 fix: connection failures show only the spinner, never a toast
A dropped/reset/timed-out connection can surface as a Connect code other than Unavailable (Canceled/DeadlineExceeded/Unknown/…) which fell through to the generic 'internal' -> a red 'something went wrong' toast appeared alongside the Connecting spinner. Now toGatewayError (moved to the pure retry.ts, unit-tested) collapses every transport-level code to 'unavailable' so it is retried + flips offline; and handleError suppresses the toast for any connection code AND whenever the app is mid-reconnect (!connection.online), covering the race where a unary error lands before the stream reports the drop. Genuine server-internal / domain errors still toast while online.
This commit is contained in:
+35
-3
@@ -1,6 +1,6 @@
|
||||
// Retry policy for the gateway transport (Stage 17). When a unary call fails at the transport
|
||||
// level the app retries it with capped exponential backoff while showing the "Connecting…"
|
||||
// indicator, instead of flashing a red toast each time.
|
||||
// Retry policy + error classification for the gateway transport (Stage 17). When a unary call
|
||||
// fails at the transport level the app retries it with capped exponential backoff while showing
|
||||
// the "Connecting…" indicator, instead of flashing a red toast each time.
|
||||
//
|
||||
// Idempotency: a rate-limit rejection (ResourceExhausted) never reached the backend, so any op is
|
||||
// safe to retry. A transport 'unavailable' is ambiguous for a mutation (its response could have
|
||||
@@ -8,6 +8,38 @@
|
||||
// 'unavailable'; a mutation is surfaced instead (its button is disabled while offline and
|
||||
// re-enables on reconnect, so the player re-issues it deliberately).
|
||||
|
||||
import { Code, ConnectError } from '@connectrpc/connect';
|
||||
import { GatewayError } from './client';
|
||||
|
||||
/**
|
||||
* toGatewayError normalises a thrown Connect/transport error to a GatewayError with a stable code.
|
||||
* Connection-level failures — the server is unreachable, the request timed out, was reset or
|
||||
* cancelled, or a raw network error — all collapse to **'unavailable'**, so they are handled as
|
||||
* connectivity (the indicator + retry), never as a red error toast. A genuine server-side
|
||||
* 'internal' or a domain code is preserved.
|
||||
*/
|
||||
export function toGatewayError(e: unknown): GatewayError {
|
||||
if (e instanceof ConnectError) {
|
||||
switch (e.code) {
|
||||
case Code.Unauthenticated:
|
||||
return new GatewayError('session_invalid', e.message);
|
||||
case Code.ResourceExhausted:
|
||||
return new GatewayError('rate_limited', e.message);
|
||||
case Code.Unavailable:
|
||||
case Code.DeadlineExceeded:
|
||||
case Code.Canceled:
|
||||
case Code.Aborted:
|
||||
case Code.Unknown:
|
||||
return new GatewayError('unavailable', e.message);
|
||||
case Code.NotFound:
|
||||
return new GatewayError('not_found', e.message);
|
||||
default:
|
||||
return new GatewayError('internal', e.message);
|
||||
}
|
||||
}
|
||||
return new GatewayError('unavailable', String(e));
|
||||
}
|
||||
|
||||
/** READ_OPS is the set of side-effect-free message types (safe to auto-retry on any failure). */
|
||||
export const READ_OPS: ReadonlySet<string> = new Set([
|
||||
'profile.get',
|
||||
|
||||
Reference in New Issue
Block a user