← All writing

Technical note · 2026-02-26

After reconnect: replaying messages without losing drafts

Focus on two post-reconnect jobs: request replay from the last acknowledged sequence so history has no hole, and snapshot drafts so list previews do not overwrite unsent input.

  • WebSocket
  • Message replay
  • Drafts
  • Reconnection

Related case: Business communication and after-sales collaboration. This note follows local socket reconnect and conversation-draft logic. API fields are generalized; the short-reconnect threshold and cache keys are illustrative. It does not claim zero message loss under every network switch.

Connected again is not “history is aligned”

Sockets drop and return often in chat: network change, sleep, gateway restart. If the client only flips a “online” flag, the UI can already be missing a span of messages. Users see a hole, not “still catching up”.

Two separate questions:

  1. message hole: replay from which sequence?
  2. input draft: what the user typed while offline—whose version wins?

Use the last acknowledged sequence as the resume point

Locally, each session keeps last_ack_seq. After the client processes and acknowledges a message it advances that value. On reconnect it asks the server to replay everything after that sequence.

Online receive ─→ update last_ack_seq ─→ disconnect

Reconnect ─→ for each recorded session, replay(last_ack_seq)

                              server pushes the gap ─→ advance ack again

Two easy mistakes:

  • do not replay only the active conversation. Background sessions can miss messages too; list previews stay wrong if you only fix the selected chat.
  • short reconnects and long offline periods need different paths. A brief drop can replay immediately; a long offline period usually needs history pagination or a fuller resync, not unbounded replay.

The local implementation splits by reconnect duration: only short reconnects trigger replay from last_ack_seq; longer gaps go to an error/refresh path. The threshold is a product/protocol compromise, not a universal constant.

Drafts cannot live only in the list preview string

Conversation lists often render send_content as the preview. If drafts overwrite that field directly:

  • clearing the input cannot restore the real last message;
  • repeated edits lose the “content before draft” snapshot;
  • rich text such as images pollutes list semantics.

The local model treats a draft as a display state with a prefix, and keeps an original snapshot:

// Illustration: list field carries both original content and draft display
const DRAFT_PREFIX = '[draft]'

// When recording a draft
item.send_old_content = wasDraft ? item.send_old_content : item.send_content
item.send_content = DRAFT_PREFIX + draftText

// When clearing input
item.send_content = wasDraft ? (item.send_old_content || '') : item.send_content

Points to keep:

  • write send_old_content only on first entry into draft, so stacked edits do not smash the snapshot;
  • treat empty content or a lone <br> as “clear draft” and restore;
  • do not write the draft prefix back into the preview for the conversation being edited, so the composer and list do not fight each other.

Replay and drafts can run in parallel, both session-scoped

Replay is requested per session; drafts are cached per session key. On identity switch or forced reconnect, destroy the old socket first so a different account cannot reuse one connection and write ack/drafts to the wrong person.

Reconnect succeeded
  ├─ replay: for each clientInstance, push after last_ack_seq
  └─ drafts: composer restores the active session cache;
             list shows draft-state previews only where appropriate

What to verify

Cover at least: reconnect within ~2s (expect replay), a longer offline gap (expect history alignment), draft then switch away and back, clearing input restores original content, and forced identity reconnect. Assert more than “socket open”: sequence continuity matters, and draft prefixes should appear only on non-active list previews.

Split “fill the hole” and “keep the draft” into two session-scoped recovery paths, and reconnect stops becoming another way to corrupt chat state.