{"slug": "build-a-reconnecting-sse-task-stream-with-node-js", "title": "Build a Reconnecting SSE Task Stream With Node.js", "summary": "A developer built a reconnecting Server-Sent Events (SSE) task stream in Node.js to study state flow for long-running AI tasks. The implementation uses event IDs and the Last-Event-ID header to support replay and recovery after disconnection, with a test suite verifying duplicate-free convergence. The project serves as a learning exercise for concepts like idempotent consumers and cursor-based resumption, applicable to WebSocket clients and message queues.", "body_md": "A long-running AI task needs to report more than “loading.” It may be queued, editing, testing, waiting for input, retrying, or complete.\n\nFor 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.\n\nImportant scope note: [MonkeyCode](https://github.com/chaitin/MonkeyCode) uses **WebSocket**, not SSE, for the task streams I reviewed. Its [mobile task-stream client](https://github.com/chaitin/MonkeyCode/blob/c58bcd4dd4b7031f469a1271f276d22550b8f523/mobile/src/api/stream.ts) 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.\n\nYou need Node.js 20 or newer. There are no packages to install.\n\nThe complete companion project has two files:\n\n```\nsse-task-stream.mjs  # server, parser, reconnecting consumer\ntest-sse.mjs         # deliberate disconnect and assertions\n```\n\nAn SSE record ends with a blank line. `id`\n\nis the resume cursor, `event`\n\nnames the event type, and `data`\n\ncarries the payload.\n\n```\nres.write(\n  `id: ${event.id}\\n` +\n  `event: task\\n` +\n  `data: ${JSON.stringify(event)}\\n\\n`\n);\n```\n\nOur states are deliberately deterministic:\n\n``` js\nconst events = [\"queued\", \"running\", \"testing\", \"complete\"];\n```\n\nOn 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.\n\n`Last-Event-ID`\n\nThe client stores the greatest accepted ID. Its next request includes:\n\n```\nLast-Event-ID: 2\n```\n\nThe server filters its replayable event log:\n\n``` js\nconst last = Number(req.headers[\"last-event-id\"] ?? 0);\nconst pending = events\n  .map((state, index) => ({ id: index + 1, state }))\n  .filter((event) => event.id > last);\n```\n\nThis example keeps the log in memory. A production server needs durable retention, authorization, bounded history, and a snapshot rule for cursors that have expired.\n\nEven with cursors, clients should handle replay. The consumer stores accepted events in a `Map`\n\nkeyed by ID:\n\n``` js\nconst seen = new Map();\nseen.set(event.id, event);\nlastEventId = Math.max(lastEventId, event.id);\n```\n\nReplacing 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.\n\n```\nnode test-sse.mjs\n```\n\nExpected output:\n\n```\nPASS reconnected, resumed after event 2, and converged without duplicates\n```\n\nThe test asserts the exact final sequence:\n\n```\n1 queued\n2 running\n3 testing\n4 complete\n```\n\nIt also verifies that four event IDs produce four records after reconnection.\n\n**Using SSE for two-way interactive traffic.** Browser `EventSource`\n\nis server-to-client. Commands need separate HTTP requests, or you may prefer WebSocket when both directions are frequent.\n\n**Treating reconnect as recovery.** A socket reopening is not enough. The client needs a cursor or a fresh authoritative snapshot.\n\n**Using array position as permanent identity.** This demo can because the event list is fixed. Production IDs must remain stable across processes and restarts.\n\n**Never expiring history.** Keep a retention window and define what happens when `Last-Event-ID`\n\nis too old—usually return a snapshot cursor, not a partial story.\n\n**Assuming exactly-once delivery.** Design for at-least-once events and idempotent projection.\n\nAfter 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`\n\nsupports replay; and why deduplication belongs in the consumer even when the server tries to resume precisely.\n\nThose concepts transfer to WebSocket clients, message queues, change streams, and long-running agent interfaces. The wire format changes; the convergence problem remains.\n\nDisclosure: I contribute to the MonkeyCode project. The MonkeyCode transport statement is based on the linked source at commit\n\n`c58bcd4`\n\n; this SSE project is a standalone learning implementation tested locally.", "url": "https://wpnews.pro/news/build-a-reconnecting-sse-task-stream-with-node-js", "canonical_source": "https://dev.to/magickong/build-a-reconnecting-sse-task-stream-with-nodejs-g92", "published_at": "2026-07-14 06:16:17+00:00", "updated_at": "2026-07-14 06:31:50.984708+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Node.js", "MonkeyCode", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/build-a-reconnecting-sse-task-stream-with-node-js", "markdown": "https://wpnews.pro/news/build-a-reconnecting-sse-task-stream-with-node-js.md", "text": "https://wpnews.pro/news/build-a-reconnecting-sse-task-stream-with-node-js.txt", "jsonld": "https://wpnews.pro/news/build-a-reconnecting-sse-task-stream-with-node-js.jsonld"}}