Skip to content

fix(core): ignore extra blank lines in event stream decoder - #111

Merged
dinwwwh merged 2 commits into
mainfrom
claude/event-stream-decoder-blank-lines-ad4cf7
Sep 26, 2026
Merged

dinwwwh merged 2 commits into
mainfrom
claude/event-stream-decoder-blank-lines-ad4cf7

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 26, 2026

Copy link
Copy Markdown
Member

Event streams that contain extra blank lines now decode cleanly. Previously a stream ending with an extra newline made the decoder throw "Event Stream ended before complete" after delivering every event, so toAsyncIteratorObject consumers saw an EventStreamDecoderError instead of normal completion. The spec treats extra blank lines as no-ops, so any producer or proxy that sends one triggered this.

Fixes

  • Streams ending in extra blank lines (\n, \r or \r\n) no longer throw on end
  • Consecutive blank lines between messages no longer emit a spurious empty {} event
  • Leading blank lines before the first message are ignored
  • Truncated streams still throw as before

Performance

  • Chunks containing a message boundary are scanned once instead of twice
  • Decode benchmarks are flat or faster; a 100KB message in one chunk decodes about 1.8x faster

Testing

  • The existing every-split-position and chunk-size tests now include leading, repeated and trailing blank lines for every line-ending style; both fail on main
  • Added an end-to-end toAsyncIteratorObject test covering the reported failure

The decoder treated exactly two line endings as a message delimiter, so an
extra blank line was kept as pending data. A stream ending in '\n\n\n' made
end() throw "Event Stream ended before complete", and consecutive delimiters
emitted a spurious {} event. The spec treats extra blank lines as no-ops.

A run of blank lines is now one delimiter, and line endings at a message
boundary are dropped, which also covers a CRLF split after a delimiter. The
delimiter scan uses a single exec loop instead of test() plus matchAll().
@pkg-pr-new

pkg-pr-new Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@111

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@111

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@111

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@111

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@111

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@111

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@111

commit: 1439bb1

@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 27.58%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 25 untouched benchmarks
⏩ 108 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ large message 100KB in one chunk 848.4 µs 665 µs +27.58%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/event-stream-decoder-blank-lines-ad4cf7 (1439bb1) with main (90876d0)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one small test-naming nit inline.

Reviewed changes

  • Greedy message delimiter — MESSAGE_DELIMITER_REGEX now requires 2+ line endings, so runs of blank lines collapse into a single delimiter and no longer emit a spurious empty {} event.
  • Leading blank-line stripping — when pending is empty, feed() drops a leading [\r\n]+ run, replacing the removed discardLeadingLF state.
  • Overlap window reduced — MAX_DELIMITER_OVERLAP 3 → 2, justified because pending never contains a blank line.
  • Single-pass scan — the global regex is driven via exec/lastIndex instead of test + matchAll.
  • Tests — split-position and chunk-size tests extended with leading/repeated/trailing blank lines; a new end-to-end toAsyncIteratorObject case reproduces the reported failure.

I re-ran the changed tests both ways: they fail on the base decoder (90876d0) and pass on this head (71665f7); the core decoder and fetch event-stream suites are green (50 tests).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/core/src/event-stream/decoder.test.ts Outdated
Adds tests for streams of only blank lines, comment-only messages among
extra blank lines, and a truncated message after extra blank lines, which
must still throw on end(). Renames the empty-chunk test to describe what it
now checks, per review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior pullfrog review (71665f7), the branch added one test-only commit (1439bb1) that broadens coverage of the blank-line handling fixed earlier:

  • Blank-line-only streams — new case asserts a stream of only \n/\r/\r\n blank lines emits nothing and does not throw on end().
  • Comment-only messages preserved — new case pins that comment-only messages (keep-alives) surrounded by extra blank lines are still emitted, so blank-line skipping cannot swallow them.
  • Incomplete message after blanks — new case asserts end() still throws when extra blank lines are followed by an incomplete message.
  • End-to-end fetch coverage — new toAsyncIteratorObject case feeds leading, repeated and trailing blank lines plus comment pings, and expects clean message delivery followed by normal completion.
  • Chunk-boundary tests extended — the split-position matrix now includes multi-ending delimiters and a leading delimiter; the chunk-size parity stream carries leading/extra/trailing blank lines; the empty-chunk test was renamed to describe what it actually covers.

I re-ran the changed tests both ways: reverting decoder.ts to the base (90876d0) makes the four new behavioral cases fail (spurious {} events and EventStream ended before complete), and they pass on the head (1439bb1). Both suites are green (53 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit 3738f90 into main Sep 26, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant