HighX
Client

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.

On this page