DEV Community

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 consumer
  • test-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:

  1. queued
  2. running
  3. testing
  4. complete

It also verifies that four event IDs produce four records after reconnection.

Common mistakes

  • Using SSE for two-way interactive traffic. Browser EventSource is 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-ID is 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-ID supports 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.