Build a Reconnecting SSE Task Stream With Node.js
A long-running AI task needs to report more than "loading." It may be queued, editing, testing, waiting for input, retrying, or complete. For a learning project, Server-Sent Events (SSE) are a small way to study that state flow. The server sends UTF-8 events over one HTTP response, the client remembers an event ID, and a reconnect can continue after the last accepted event.
Important scope note: MonkeyCode uses WebSocket, not SSE, for the task streams I reviewed. Its mobile task-stream client includes reconnection, queued replies, and deduplication behavior. I used those reliability concerns as inspiration for this smaller SSE exercise, not as a description of MonkeyCode's transport.
Prerequisites
You need Node.js 20 or newer. There are no packages to install. The complete companion project has two files:
sse-task-stream.mjs- server, parser, reconnecting consumertest-sse.mjs- deliberate disconnect and assertions
Step 1: Send identifiable events
An SSE record ends with a blank line. id is the resume cursor, event names the event type, and data carries the payload.
res.write(
`id: ${event.id}\n` +
`event: task\n` +
`data: ${JSON.stringify(event)}\n\n`
);
Our states are deliberately deterministic:
const events = ["queued", "running", "testing", "complete"];
On the first connection, the test server sends only events 1 and 2, then closes. This simulates an interrupted response without waiting for a real network failure.
Step 2: Resume from Last-Event-ID
The client stores the greatest accepted ID. Its next request includes:
Last-Event-ID: 2
The server filters its replayable event log:
const last = Number(req.headers["last-event-id"] ?? 0);
const pending = events
.map((state, index) => ({ id: index + 1, state }))
.filter((event) => event.id > last);
This example keeps the log in memory. A production server needs durable retention, authorization, bounded history, and a snapshot rule for cursors that have expired.
Step 3: Deduplicate anyway
Even with cursors, clients should handle replay. The consumer stores accepted events in a Map keyed by ID:
const seen = new Map();
seen.set(event.id, event);
lastEventId = Math.max(lastEventId, event.id);
Replacing the same key makes duplicate delivery harmless for this state projection. Real effects-charging a card or merging a PR-need server-side idempotency too.
Run the disconnect test
node test-sse.mjs
Expected output:
PASS reconnected, resumed after event 2, and converged without duplicates
The test asserts the exact final sequence:
- queued
- running
- testing
- complete
It also verifies that four event IDs produce four records after reconnection.
Common mistakes
- Using SSE for two-way interactive traffic. Browser
EventSourceis server-to-client. Commands need separate HTTP requests, or you may prefer WebSocket when both directions are frequent. - Treating reconnect as recovery. A socket reopening is not enough. The client needs a cursor or a fresh authoritative snapshot.
- Using array position as permanent identity. This demo can because the event list is fixed. Production IDs must remain stable across processes and restarts.
- Never expiring history. Keep a retention window and define what happens when
Last-Event-IDis too old-usually return a snapshot cursor, not a partial story. - Assuming exactly-once delivery. Design for at-least-once events and idempotent projection.
What this project teaches
After completing it, you should be able to explain:
- the difference between connection recovery and state recovery
- why an event needs an identity
- how
Last-Event-IDsupports replay - why deduplication belongs in the consumer even when the server tries to resume precisely
Those concepts transfer to WebSocket clients, message queues, change streams, and long-running agent interfaces. The wire format changes; the convergence problem remains.
Disclosure: I contribute to the MonkeyCode project. The MonkeyCode transport statement is based on the linked source at commit c58bcd4; this SSE project is a standalone learning implementation tested locally.
Comments
No comments yet. Start the discussion.