WebSocket session resume is how a live connection recovers from a drop without losing or duplicating a single message: the client remembers the last message it processed, and on reconnect it asks the server to replay everything after that point. It is the protocol behind the Discord and Slack gateway resume, MQTT session continuity, and Kafka-style consumer offsets. This question is about the resume itself — assume the socket already dropped and reconnected; your job is to make the message stream continuous across that gap.
Every server message is a data frame { seq, data }, where seq is a counter that increases by one for each message. You track lastSeq, the highest contiguous seq you have handed to the app. The transport is injected — you never open a real socket — so this is pure bookkeeping over sequence numbers.
function websocketSessionResume(options: {
send: (frame: object) => void; // write a control frame (the resume request) to the server
onMessage: (data: unknown) => void; // sink for DE-DUPED, in-order messages to the app
sessionId?: string; // optional id echoed in the resume frame
startSeq?: number; // optional starting high-water mark (default 0)
}): {
receive: (frame: object) => void; // the transport calls this with every inbound frame
resume: () => void; // after reconnect: send { type: 'resume', lastSeq }
readonly lastSeq: number; // highest contiguous seq delivered
};
const delivered = [];
const sent = [];
const s = websocketSessionResume({
onMessage: (d) => delivered.push(d),
send: (f) => sent.push(f),
});
s.receive({ seq: 1, data: 'a' });
s.receive({ seq: 2, data: 'b' });
s.receive({ seq: 2, data: 'b' }); // a redelivered duplicate — dropped
// delivered === ['a', 'b'], s.lastSeq === 2
// The socket dropped after seq 5, then reconnected. Ask for a replay:
s.resume();
// sent === [{ type: 'resume', lastSeq: 5 }]
// The server replays 4..8 (at-least-once, so 4 and 5 come again):
[4, 5, 6, 7, 8].forEach((seq) => s.receive({ seq, data: 'm' + seq }));
// 4 and 5 are dropped as duplicates; only 6, 7, 8 are delivered — s.lastSeq === 8
send and onMessage in options and never touch a real WebSocket. This is what makes the protocol testable by hand.lastSeq advances only when the next expected seq arrives. A frame whose seq is lastSeq + 1 is delivered; anything past that leaves a gap.seq is lastSeq or lower has already been processed. Drop it; never deliver it twice. This is what turns at-least-once delivery into exactly-once processing.seq is more than one past lastSeq means an earlier message is still missing. Hold it, and flush it in order once the hole is filled.resume() sends { type: 'resume', lastSeq } so the server knows exactly where to replay from. Replayed frames arrive as ordinary data frames.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.