All questions

Reconnecting WebSocket

Premium

Reconnecting WebSocket

A reconnecting WebSocket is a wrapper around the browser's WebSocket that survives dropped connections: when the socket dies it opens a new one on its own, backs off between attempts, and buffers the messages you send while it is down. It is the workhorse behind live dashboards, chat, and collaborative editors — anything that needs a connection to stay up even though real networks constantly break.

You will implement reconnectingWebsocket(url, options). So it can be tested without a real server, the wrapper never calls new WebSocket itself — it builds the underlying socket through an injected options.createSocket(url) factory. In production you let it default to the real WebSocket; in the tests you pass a fake socket you can open, message, and close by hand.

Signature

type SocketLike = {
  send(data: any): void;
  close(): void;
  onopen: ((e: any) => void) | null;
  onmessage: ((e: any) => void) | null;
  onclose: ((e: any) => void) | null;
  onerror: ((e: any) => void) | null;
};

type Options = {
  createSocket?: (url: string) => SocketLike; // defaults to (u) => new WebSocket(u)
  maxRetries?: number; // reconnection attempts after a drop; default Infinity
  backoff?: (attempt: number) => number; // ms to wait before retry N
  maxBackoff?: number; // cap for the default backoff, in ms
};

function reconnectingWebsocket(url: string, options?: Options): {
  on(event: "open" | "message" | "close" | "reconnect" | "error", handler: (e: any) => void): () => void;
  addEventListener: /* alias of on */ typeof on;
  send(data: any): void; // sends now, or queues while down
  close(): void; // intentional close — stops reconnecting
};

Examples

// Production: default factory builds a real WebSocket.
const rw = reconnectingWebsocket("wss://chat.example.com");
rw.on("message", (e) => render(e.data));
rw.on("reconnect", () => hideOfflineBanner());
rw.send("hello"); // if the socket is open, sent now; if not, queued
// Test: inject a fake socket and drive it by hand — no real network.
const made = [];
const createSocket = (url) => {
  const s = { sent: [], onopen: null, onmessage: null, onclose: null, onerror: null,
              send: (d) => s.sent.push(d), close() {} };
  made.push(s);
  return s;
};
const rw = reconnectingWebsocket("wss://x", { createSocket, backoff: () => 10 });

rw.send("a");           // socket 0 has not opened yet -> queued
made[0].onopen();       // opens -> flushes the queue: made[0].sent === ["a"]
made[0].onclose({});    // an unexpected drop -> schedules a reconnect (~10ms)
// ...after the backoff, a NEW socket (made[1]) is created and opened.

Notes

  • Injected socket — always build the underlying socket via options.createSocket(url), never a hard-coded new WebSocket. This is what makes the wrapper testable.
  • Exponential backoff — on an unexpected close, wait backoff(attempt) ms and then open a new socket. attempt grows on each failed try and resets to 0 on a successful open, so a healthy connection that blips retries the short first delay again.
  • Queue while downsend while disconnected buffers the message; the queue flushes in order on the next open. send while connected goes straight through.
  • Intentional vs. unexpected closeclose() is deliberate: it stops reconnecting for good, closes the underlying socket, and clears any pending reconnect timer. Only an unexpected drop reconnects.
  • Eventson(event, handler) covers open, message, close, reconnect, and error. reconnect fires when the connection re-opens after a drop, not on the first open.
  • Giving up — after maxRetries failed attempts, stop retrying and emit a terminal close. Assume url and the options are valid; you do not need to validate them.

FAQ

Why does the wrapper take a socket factory instead of calling new WebSocket itself?
Injecting a createSocket(url) factory lets tests hand the wrapper a fake socket with no real network. The reconnect, backoff, and queue-and-flush logic can then be driven deterministically by simulating open, message, and close on the fake, while the wrapper still defaults to new WebSocket(url) in a real browser.
How does it tell an intentional close from a dropped connection?
A manuallyClosed flag. Calling close() sets it, so when the underlying onclose fires the wrapper knows not to reconnect. An unexpected close leaves the flag false, which is the signal to schedule a backoff and open a new socket.
What happens to messages sent while the connection is down?
They are pushed onto an in-memory queue instead of throwing. On the next successful open the wrapper flushes the queue in order, so no message is lost during a reconnect gap.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium