An implementation of Server-Sent Events (SSE) for Swift using async sequences. Events are parsed from any AsyncSequence of bytes and exposed as an AsyncSequence of ServerSentEvent values, with an optional EventSource-style reconnecting client built on URLSession.
EventSource mirrors the specification's EventSource interface: it sends the Accept: text/event-stream header, automatically reconnects when the server closes the stream or the network drops, honors the server's retry interval, and resumes with the Last-Event-ID header.
let url = URL(string: "https://example.com/sse")!
for try await event in EventSource(url: url) {
print(event.type, event.data)
}Iteration only ends when the server responds with HTTP 204 (the spec's "stop reconnecting" signal), an unrecoverable response is received (thrown as SSEError), or the consuming task is cancelled. To resume a previous session, pass the last event ID you stored:
let source = EventSource(
request: URLRequest(url: url),
configuration: .init(retryInterval: 3000, lastEventId: storedId)
)The URLSession helpers open one connection, send the required headers, and validate that the response is a 200 with a text/event-stream content type (throwing SSEError otherwise):
let (sse, response) = try await URLSession.shared.serverSentEvents(from: url)
for try await event in sse {
print(event.data)
}
// For manual reconnection:
let lastEventId = await sse.state.lastEventId
let retryInterval = await sse.state.retryIntervalYou can also parse an existing byte stream directly:
let (bytes, response) = try await URLSession.shared.bytes(from: url)
for try await event in bytes.sse() {
print(event)
}The parser is generic over AsyncSequence with UInt8 elements, so it isn't tied to URLSession:
let events = AsyncServerSentEvents(bytes: someByteSequence)Parsing is lazy and driven by iteration: bytes are only consumed as you request events, errors from the byte stream are rethrown to the consumer, and cancelling the consuming task stops the parse.
Each ServerSentEvent carries:
data— the event payload, with multipledatafields joined by newlines per the specname— the raweventfield, if present;typeapplies the spec's"message"defaultid— theidfield explicitly present on this event's block, if anylastEventId— the last event ID in effect when the event was dispatched (persists across events, per the spec); this is the value to resume from
- WHATWG-compliant line parsing (
CR,LF,CRLF) - UTF-8 decoding with replacement characters and leading BOM removal
- Strict field parsing (
data,id,event,retry) with single-space value trim - Comment lines ignored per spec
- Last event ID persistence across events, committed at dispatch time
-
retryfield handling - Errors from the transport are rethrown to the consumer
-
Accept: text/event-streamrequest header and response validation (status and content type) - Automatic reconnection with
retryinterval andLast-Event-IDheader (EventSource) - HTTP 204 handled as "stop reconnecting"
The library works on Apple platforms and Linux. Instead of URLSession.AsyncBytes (which is unavailable on Linux), networking is built on SSEByteStream, a small delegate-based byte stream over URLSessionDataTask, so the parser, the URLSession helpers, and EventSource all behave identically everywhere. On Apple platforms, URLSession.AsyncBytes.sse() remains available as a convenience.