eventsource-parser

A fast, source-agnostic streaming parser for Server-Sent Events (SSE), used to build EventSource clients and stream LLM responses.

Library
npm
v4.1.1
507 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
70 /100 Good
Development Activity 72
Maintenance 68
Community 44
Maturity 56
Momentum 40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
82 /100 Excellent
Architecture 88
Code Quality 92
Innovation 82
Learning Curve 65

eventsource-parser is a low-level, streaming parser for the Server-Sent Events (SSE) protocol used by the browser EventSource API. It makes no assumptions about how you retrieve the underlying byte stream — you create a parser instance and feed it chunks of text, complete or partial, and it calls back with fully parsed messages (id, event, and data fields), reconnection retry intervals, and stream comments as they arrive. A TransformStream variant (eventsource-parser/stream) is also exported for environments that support the Streams API, letting you pipe a fetch response body directly into a stream of parsed EventSourceMessage objects.

Because it doesn’t own the transport, eventsource-parser is the building block underneath higher-level clients — its author’s own eventsource-client package, EventSource polyfills, and countless bespoke integrations for streaming LLM chat completions over SSE. Internally the parser is tuned for exactly that workload: trailing fragments are buffered and joined once rather than concatenated on every feed() call (avoiding O(N²) blowups on payloads split across many small chunks), the common single-line event shape is dispatched without ever touching the internal data buffer, and an optional maxBufferSize guards against unbounded memory growth from a server that never terminates a line.

What You Get

  • Chunk-based feed() API - pass partial or complete SSE payloads in any order and the parser accumulates state across calls until a full message is dispatched.
  • EventSourceParserStream transform - a TransformStream<string, EventSourceMessage> for piping a decoded fetch response body straight into parsed events.
  • Full SSE field support - id, event, retry, multi-line data, and comment (:) lines are all parsed per the WHATWG spec, including BOM stripping and mixed \n/\r/\r\n line endings.
  • maxBufferSize guard - caps how much unterminated data the parser will buffer, emitting a max-buffer-size-exceeded ParseError and refusing further feed() calls until reset().
  • Typed error callbacks - onError receives a typed ParseError (invalid-retry, unknown-field, max-buffer-size-exceeded) instead of throwing, so malformed streams degrade gracefully.

Common Use Cases

  • Streaming LLM chat responses - parsing token-by-token SSE output from OpenAI-, Anthropic-, or self-hosted-compatible chat completion endpoints.
  • Building a custom EventSource client - implementing reconnect logic, custom headers, or non-browser runtimes (Node.js, Deno, Bun, edge/serverless) where the native EventSource API isn’t available or flexible enough.
  • Polyfilling EventSource in environments without it - pairing the parser with a fetch-based transport to reproduce the browser API’s behavior server-side.
  • Proxying or relaying SSE streams - re-parsing and re-emitting events when building a gateway or middleware that inspects or transforms a Server-Sent Events stream in transit.

Under The Hood

Architecture The library is split cleanly by responsibility: parse.ts implements the core state machine (createParser) that consumes string chunks and dispatches parsed fields through closures over mutable parser state (pendingFragments, data, id, eventType), stream.ts wraps that core in a TransformStream subclass (EventSourceParserStream) for Streams-API environments, types.ts defines the public ParserConfig/ParserCallbacks/EventSourceMessage contracts, and errors.ts defines a small typed ParseError class. There is no shared mutable module state — each createParser() call closes over its own buffers — and the fast path (processLines) is deliberately duplicated from the slow, spec-correct CR/LF-aware path to avoid per-chunk branching overhead; changing the core buffering strategy (fragment array vs. string concatenation) is the one change that would ripple through the whole hot path.

Tech Stack Written in strict TypeScript (ES2018 target, ESM-only, verbatimModuleSyntax, noUncheckedIndexedAccess) with zero runtime dependencies. Builds go through tsc --build for declaration output plus a separate bundle-size check via esbuild/terser; oxlint and oxfmt (the Rust-based Oxc toolchain) handle linting and formatting instead of ESLint/Prettier; knip checks for unused exports; releases are managed with Changesets; and tests run under vitest across Node, Bun, and Deno via dedicated test:node/test:bun/test:deno scripts.

Code Quality The test suite (parse.test.ts, stream.test.ts, plus dedicated multibyte and benchmark-fixture suites) totals roughly four times the line count of the library source itself, covering SSE edge cases like split multi-byte characters, BOM variants, mixed line terminators, and buffer-limit termination. The tsconfig.json enables an unusually strict rule set (noUnusedLocals, noPropertyAccessFromIndexSignature, noFallthroughCasesInSwitch, strict), and CI workflows (test.yml, release.yml) gate merges on lint plus the full cross-runtime test matrix. Comments throughout parse.ts explain non-obvious micro-optimizations (e.g. why buffering avoids an O(N²) trap) rather than restating what the code does.

API Design The public surface is intentionally narrow: one factory function (createParser) taking a callbacks object, plus one TransformStream subclass for the Streams-API path — there’s no class to instantiate or state to manage beyond calling feed() and, on reconnect, reset(). Getting started requires no boilerplate beyond supplying an onEvent callback, and the same callback shapes (onId, onRetry, onComment, onError) are reused identically between the callback-based and stream-based APIs, so switching between them doesn’t require relearning the message shape.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers