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.
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
};
// 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.
options.createSocket(url), never a hard-coded new WebSocket. This is what makes the wrapper testable.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.send while disconnected buffers the message; the queue flushes in order on the next open. send while connected goes straight through.close() is deliberate: it stops reconnecting for good, closes the underlying socket, and clears any pending reconnect timer. Only an unexpected drop reconnects.on(event, handler) covers open, message, close, reconnect, and error. reconnect fires when the connection re-opens after a drop, not on the first open.maxRetries failed attempts, stop retrying and emit a terminal close. Assume url and the options are valid; you do not need to validate them.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.