Overhaul parser for spec compliance; add reconnecting EventSource client#1
Merged
Conversation
Rework the parser to be iterator-driven and generic over any AsyncSequence of bytes: - Errors from the byte stream are rethrown to the consumer instead of being swallowed (previously a network failure left the stream open and the consumer awaiting forever) - Cancellation propagates to the transport; no more detached parse task buffering events without bound - UTF-8 decodes with replacement characters, so invalid bytes no longer fabricate blank lines that split events - A single leading BOM is stripped per spec - Fixed the line splitter emitting a phantom empty line for CR CR - The last event ID persists across events, is stamped on every dispatched event, and commits at dispatch time so an incomplete final block cannot poison the resume ID - ServerSentEvent.type applies the spec's "message" default - Removed the never-populated comment field and unused split helpers URLSession helpers now send Accept: text/event-stream and Cache-Control: no-cache, and validate the response status code and content type, throwing SSEError otherwise. New EventSource client mirrors the spec's reconnection model: automatic reconnect on close or network error, retry-interval handling, Last-Event-ID resumption, and HTTP 204 to stop. The parser now builds on Linux (URLSession pieces are Darwin-gated); CI gains a Linux job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkByVdGaxGYiRiGnaQJ8Uc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkByVdGaxGYiRiGnaQJ8Uc
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8971b446b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The content-type check now requires the media type to be exactly text/event-stream (parameters allowed), rejecting lookalikes such as text/event-stream+json. An explicitly cleared last event ID (empty id: field) now removes a caller-set Last-Event-ID header on reconnect instead of resuming from the stale value.
URLSession.AsyncBytes is unavailable on Linux, which kept the URLSession helpers and EventSource Apple-only. SSEByteStream streams a data task's body through URLSessionDataDelegate — available on both Darwin and corelibs Foundation — so the entire library now works on Linux. Connections run on a session derived from the caller's session configuration (a session-level delegate is required for streaming); dropping the stream cancels the task and invalidates the session. The delegate-taking serverSentEvents variants are removed since per-task delegates don't compose with the streaming delegate. Header lookup goes through allHeaderFields for consistent behavior on corelibs Foundation, and new tests cover chunk-boundary parsing and error propagation through the byte stream.
corelibs URLSession does not support file: URLs for data tasks, so the streaming tests now run against a minimal scripted HTTP server that works on both Darwin and Linux. This also enables true end-to-end coverage: request headers are asserted on the wire, and a new EventSource test drives the full reconnect cycle (Last-Event-ID on reconnect, retry interval pickup, 204 termination). The byte-to-line state machine moves to a top-level SSELineIterator so tests can drive it directly, with a dedicated suite covering CR/LF/CRLF equivalence, empty lines, consecutive CRs, trailing terminators at end of stream, CRLF split across chunk boundaries, multi-byte characters split across chunks, and that NEL/U+2028/U+2029 are content, not boundaries.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the functional gaps between this library and the WHATWG server-sent events specification, and adds the missing pieces needed to use it as a complete SSE client.
Critical fixes
printand never finished its stream, leavingfor awaitsuspended forever on any network drop. Parsing is now iterator-driven and rethrows transport errors to the consumer.AsyncStreambuffer anymore; bytes are read only as events are consumed, and cancelling the consuming task stops the parse and releases the connection."", which the parser treated as a dispatch boundary — splitting one event in two and dropping data. Decoding now uses U+FFFD replacement per spec.Spec compliance
CR CRsequences.lastEventId), commits at dispatch time (so a truncated final block can't poison the resume ID), and id-only blocks commit without dispatching — all per spec.ServerSentEvent.typeapplies the spec's"message"default when theeventfield is absent or empty.URLSessionhelpers now sendAccept: text/event-streamandCache-Control: no-cache, and validate the response (status 200,text/event-streamcontent type), throwingSSEErrorinstead of parsing an error page as a stream.New:
EventSourceA reconnecting client mirroring the spec's
EventSourcemodel:retryintervalLast-Event-IDheaderStructure & cleanup
AsyncSequenceof bytes, decoupling it fromURLSession.AsyncBytes— it builds and tests on Linux (theURLSessionpieces are Darwin-gated), and CI gains a Linux job.Event.commentfield, the unusedtrim()helper, and the unusedAsyncSequence.splitextensions.asyncBytes(from:)API) and the feature checklist updated.Breaking changes
AsyncServerSentEvents.Eventis now the top-levelServerSentEvent(a typealias keeps the old spelling reachable); thecommentproperty is gone.AsyncServerSentEventsis generic over its byte source and its iteration can now throw, so consumers must usefor try await.serverSentEvents(from:)/(for:)now throwSSEErrorfor non-200 or non-text/event-streamHTTP responses.Test plan
Generated by Claude Code