← Dev log

backend

What a client missed while it was gone

What a client missed while it was gone

Raw Socket.io gives you a connection, rooms, and emit/on. It gives you nothing about the gap. A client that briefly drops and reconnects just resumes receiving new events from that moment on — anything emitted during the drop is gone, with no signal it ever happened.

For a chat app, fine. For a live queue board or a counter operator's token feed, that silent gap is a stale screen with zero indication anything's wrong, sitting in a waiting room where nobody's watching it moment to moment.

Sequence numbers, not timestamps

Every event emitted into a room gets a monotonically increasing sequence number. Not a timestamp — timestamps can collide or mislead under clock skew, a counter can't. The server keeps a rolling buffer of recent events per room, each one tagged.

queueEvent(room: string, event: string, payload: any) {
  this.sequenceNumber++;
  this.eventQueue.push({
    room, event, payload,
    sequence: this.sequenceNumber,
    timestamp: Date.now(),
  });
}

On reconnect, the client reports the last sequence number it processed. The server replays everything higher, then resumes the live stream. If the gap is bigger than the buffer can safely represent, the right move is telling the client to fetch a fresh snapshot rather than handing back a partial, misleading replay.

Three things I'd have missed

  • The buffer needs a max size and a max age, cleaned on an interval. Unbounded is a slow memory leak on a long-running service. Size alone or age alone each miss a case the other catches.
  • Authorization has to be re-checked on the replay path. It's easy to authorize a client when they join a room and forget replay is a second path the same data flows through. Skip the check there and it's a clean bypass of the room-scoped auth you enforced correctly everywhere else.
  • A client can look connected at the transport level while frozen — a dead tab, a device that lost power without a clean close. A periodic ping/pong with a timeout catches it: no response in the window, treat it as disconnected and clean up, even though the raw socket never fired its own disconnect.

How much of this is worth it scales with how unattended and how consequential a stale screen is. A tab someone's looking at reconnects fast and the gap is invisible. A kiosk running unattended for hours is a genuinely different risk.