useConnectionStatus
One honest signal for whether the server is reachable — derived, never polled.
Connection health from the signals the client genuinely has, so apps stop hand-rolling a degraded-connection state machine in an auth provider.
import { useConnectionStatus } from '@voltro/client'
const connection = useConnectionStatus('app')
{(connection.status === 'degraded' || connection.status === 'offline') && (
<OfflineBanner status={connection.status} />
)}status is 'connected' | 'recovering' | 'degraded' | 'offline'.
recovering is the configurable grace window after an observed transport loss
(1.5 seconds by default): raw diagnostics already know the socket is down, but
the UI can stay quiet while the client reconnects. If every affected
subscription produces its own snapshot or resumed delta inside that window,
the state returns directly to connected; there is no failure flash. If the
window expires, it changes once to degraded. offline is browser-reported
(navigator.onLine plus its online / offline events) and wins over both.
For a unary call, a later rpc success or declared handler reply proves the
server answered and clears a generic transport failure. For a failed
subscription, only a later snapshot or resumed delta from that same tag clears
its pending state. An unrelated heartbeat cannot label a screen healthy while
one of its streams is still terminal. Local client and unknown failures leave
connection state unchanged. The browser coming back online does not prove that
the rpc socket recovered; raw state stays disconnected until the protocol is
ready. An explicit reportSuccess() may clear a generic transport failure, but
does not override pending subscription recovery.
Use this hook to render aggregate connection state. To classify the failure
that is firing right now, branch on event.kind inside
useOnRpcError. React state updates
after the event and cannot classify that event retroactively.
Also returned: rawTransport (immediate, never grace-filtered), online,
failureCount, lastFailureAt, failedSubscriptions,
pendingMutationReplays, and mutationReplay. There is deliberately no polling
ping — connected means "no observed transport failure is still unresolved",
not "just verified".
Configure the bounded policy in the web app. The defaults are shown here:
// apps/web/app.config.ts
export default {
web: {
api: {
recovery: {
graceMs: 1_500,
retry: { initialMs: 0, thenMs: 500, factor: 1.5, maxMs: 5_000 },
maxPendingMutations: 32,
replayWindowMs: 10_000,
// Usually derived beside the WS path. Set only when your edge routes
// WebSocket and HTTP through unrelated paths:
// capabilitiesUrl: '/api/_voltro/runtime-capabilities',
},
},
},
}Values are clamped (graceMs 0–30 s, pending mutations 0–256, replay window
250 ms–60 s), so a broken config cannot create an unbounded client queue.
The first socket retry has no deliberate delay. If it fails, the waits are
500, 750, 1125, 1687.5, 2531.25, 3796.875, then 5000 ms repeatedly. Each wait
starts after the preceding attempt fails; DNS, TLS and upgrade time come on
top. A protocol-ready connection resets the sequence for the next outage.
graceMs controls presentation only; it does not postpone connection attempts.
Retry settings may be partial: omitted or non-finite fields use their defaults.
maxMs is clamped to 50–60000 ms, initialMs to 0–maxMs, thenMs to
50–maxMs, and factor to 1–10. Delay settings are truncated to integer ms;
later exponential steps may be fractional. The positive thenMs minimum
prevents an endless zero-delay loop.
Change this in the web app's app.config.ts, then rebuild and redeploy the
web app. Programmatic mount() accepts the same recovery options, including
per-API overrides; React Native uses MobileApiBinding.recovery.
Normal socket retries keep the runtime and its subscription bases/revisions.
refreshAuth() explicitly rebuilds with fresh auth headers for the same subject;
reconnect() rebuilds for a subject change. Neither action waits for backoff.
Header resolvers run per runtime build; a socket cut no longer causes their
re-evaluation through a competing rebuild. The web host still handles supported
auth errors with bounded wireAuthRefresh. Custom hosts can call
SupervisorHandle.refreshAuth() after token rotation.
For a measurement, open browser DevTools on the affected page:
voltroDiagnostics.enable()
voltroDiagnostics.cut('app', 6000)
// Wait for recovery, then export the evidence:
voltroDiagnostics.download('app')The export contains socket.retry-scheduled with attempt, delayMs,
generation, and owner (socket or runtime-build). Compare it with
socket.created, socket.raw-open, protocol.ready, and each subscription's
first delivery. This separates backoff, connection setup and data recovery.
The 400-event ring can drop older events; keep the native WebSocket timing
capture too when diagnosing a busy page.
Custom transport adapters: startApiSupervisor retries rejected
buildClient promises. Once a runtime exists, its transport must retry socket
failures itself (buildApiRuntime does this). onSocketIssue reports health
and drives the wedge backstop; it no longer schedules another runtime build.
retryDelayMs overrides only failed construction retries. Use recovery.retry
for sockets. Disposal must stop the adapter's retry loop.