Connection and offline
Reconnect, resume, and the bounded offline mutation queue.
The connection is handled for you. This page is what it is doing, and the two places you should still write UI.
Status
createBrowserTransport(url, {
onStatusChange: (status) => setBanner(status === "open" ? null : "Reconnecting…"),
});"connecting", "open", or "closed". Queries keep serving their last known
value while the connection is down, so a banner is usually the only UI change a
disconnect needs. In React: useConnectionStatus.
A heartbeat runs every 10 seconds and the connection is presumed dead after 5 without an answer. Those defaults are deliberately tight: a WebSocket can die without a close frame, and until that is noticed the client shows stale data while claiming to be live — which, in an application where changes arrive on their own, is indistinguishable from nobody typing.
Reconnect
Bounded exponential backoff with jitter. On a new connection, a subscription
that already has a cursor is resumed rather than re-subscribed; if the
server cannot resume it, the error frame carries the subscriptionId and the
client subscribes again under the same ID.
A patch that does not follow the cursor is a gap, so the client requests a snapshot rather than guessing how to repair the cache. A duplicate patch is ignored without triggering a resubscribe.
Coming back early
Backoff is right for a server refusing connections and wrong for a laptop
leaving a tunnel — by then the delay has grown to tens of seconds.
createBrowserTransport listens for the browser's online event and calls
client.retryNow(), which resets the backoff and reconnects.
It is evidence rather than proof (online fires for a captive portal too), so
it only shortens a wait that was going to end anyway.
When a resume cannot be honoured
The server forgets a subscription when its connection closes, so a client
reconnecting after a real drop is usually told not_found and subscribes again.
That new subscription's sequence starts again at one — sequences are per
subscription, and there is no global one.
@highx/client handles this: a snapshot arriving at cursor zero replaces. If
you write your own client, treat the first snapshot of a re-established
subscription as authoritative rather than comparing sequences that mean
different things — otherwise the snapshot is dropped, the connection reports
itself healthy, and the data never changes again.
Deduplication
Identical path and argument pairs share one server subscription and one cache entry, reference counted. Argument key order does not matter. The subscription is released when the last caller unsubscribes.
Offline mutations
Editing keeps working while disconnected. Mutations queue and go when the
connection returns, under an explicit bound — beyond maxQueuedMutations the
client rejects rather than growing without limit or discarding work
silently.
A queued mutation keeps its request ID across retries — the same value the server uses as its idempotency key — so a replay cannot duplicate an effect.
Idempotent is not the same as safe. A queued write replays with whatever it
was based on when it was made, so on reconnect it can land on top of everything
written while that client was away. Nothing in the framework can judge that for
you — only your schema knows what a version is. Carry one in the write and
refuse a stale one;
useDraft does the client half.
What you never have to write
Reconnect, resume from a cursor, snapshot replacement when a cursor is too old,
duplicate and out-of-order patches, and the offline queue all live in
@highx/client. No useEffect of yours needs to know a socket dropped.