realtime-transport-contracts
Installation
SKILL.md
Realtime transport contracts
A realtime client's happy path (connect, receive, render) always works; the defects live in the seams around a dropped connection — reconnect, resume, dedupe, liveness, backpressure, and re-auth. Treat the connection as unreliable and every delta as potentially duplicated, reordered, or missing, and make each recovery step an explicit contract rather than a framework default.
Checklist (lead with the trap; details in references/)
- Reconnect with capped exponential backoff + jitter, and stop on non-retryable closes. A fixed-interval (or un-jittered) retry turns a server restart into a synchronized thundering herd; AWS's measurements show full jitter —
sleep = random(0, min(cap, base * 2**attempt))— cuts both contention and total recovery time. Reset the attempt counter only after a connection stays up past a stability window, or a flapping server resets your backoff every few seconds. Do not blindly retry every close: retry transient codes (1006 abnormal, 1011 server error, 1012 restart, 1013 try-again-later); do not loop on auth/policy (1008, app-level 4xxx) or protocol errors (1002/1003).EventSourcereconnects on its own, so the bug is the opposite — call.close()when you mean to stop, and know a204response tells the browser to stop retrying. - Resume the stream, do not restart it. After reconnect you must re-subscribe to channels and resume from a position. SSE sends the
Last-Event-IDheader automatically — but only if the server emittedid:lines and you did not reset the id, and only if the server actually honors the header and replays from it. For WebSocket you own resume: send your last-applied server cursor/sequence on reconnect. Without resume you either gap (dropped events) or blindly re-request a fresh snapshot and double-count. - Fold deltas defensively: dedupe, order, detect gaps. Deltas arrive duplicated (replay after reconnect), out of order, or gapped. Key every delta by a monotonic server sequence/version: drop
seq <= lastApplied(idempotent apply), holdseq > expectedin a small reorder buffer until the gap fills, and treat an unfillable gap as "resnapshot", never "interpolate". AMap/last-writer-wins merge that ignores sequence silently applies a stale delta over a newer one. - Reconcile the initial snapshot against the live stream on one cursor. The snapshot (REST/SSR) and the stream must share a version axis — apply only deltas newer than the snapshot version and backfill the gap between them, or you double-apply or drop the overlap. (For the SSR/first-render side of this seam see ssr-hydration-mismatch.)
- Add heartbeat + liveness; an OPEN socket can be dead.
readyState === OPEN(and even a live TCP socket) can be a zombie after a network drop or a proxy idle-timeout — the classic silent 1006. Browser JS cannot send protocol-level ping/pong (RFC 6455 frames are not exposed to the WebSocket API), so run an application-level ping and expect a pong within a timeout; on miss, close and reconnect. Gate reconnects onnavigator.onLine/offlineand Page Visibility so a backgrounded or offline tab does not spin. - Bound the buffers; the send path has no automatic backpressure.
WebSocket.bufferedAmountclimbs when yousend()faster than the socket drains; per the WHATWG spec a full send buffer forces the browser to close the connection. PollbufferedAmountand throttle/coalesce outgoing messages above a threshold; bound the inbound queue too and coalesce or shed rather than growing memory unbounded (or adopt a stream-backpressured transport where supported). - Refresh auth on a long-lived socket. Authentication at the handshake establishes identity only at that instant; hours later the token may be expired or revoked while the socket keeps processing messages. Refresh before expiry — in-band (push a fresh token over the open socket, server re-validates, no drop) or reconnect-with-new-token — and define an explicit reauth-required signal, since the protocol has no built-in "auth expired" response. (Refresh/rotation mechanics belong to frontend-auth-flow-contracts;
EventSourcecannot set anAuthorizationheader, so tokens often ride in the URL/query and get logged — that placement is frontend-security-baseline.) - Test the failure sequence, not the happy path. Assert: reconnect after a drop resumes with no gap and no duplicate; backoff carries jitter and caps; an auth/policy close is not retried; a missed heartbeat triggers reconnect; duplicated/out-of-order/gapped deltas converge to the same state; an expired token is refreshed without tearing the stream down.