feat: add lifecycle-bound SSE stream and typed per-endpoint adapter#153
feat: add lifecycle-bound SSE stream and typed per-endpoint adapter#153OmarAlJarrah wants to merge 1 commit into
Conversation
Introduce SseStream: an AutoCloseable Iterable<ServerSentEvent> that owns
the underlying HTTP response. Closing the stream — explicitly, via use {} /
try-with-resources, or implicitly when iteration runs to completion — closes
the response and releases its connection, so a partial consume never strands
the body. This mirrors the close-on-partial-consume invariant PagedIterable
enforces. The previously doc-only "do not iterate twice" warning is now an
enforced single-pass guard, and iteration after close is rejected.
Add a reusable per-endpoint adapter, TypedSseStream<T>, that maps raw events
to typed models via a caller-supplied SseEventMapper. The mapper receives the
event name and joined data and returns a decoded value, Skip, or a Done
sentinel; it is the seam where the Serde SPI is invoked and where per-API
done-sentinel and error-envelope conventions live. Mapping is applied lazily,
one element at a time, so a partial consume decodes only the events taken.
Closing the typed adapter propagates to the underlying stream.
Both surfaces are hand-written runtime primitives usable today; a code
generator can target them later without embedding any per-API convention in
core.
Closes #35
Closes #62
|
This adds two SSE consumption primitives to Issues
Worth double-checking
|
Summary
Adds a lifecycle-bound Server-Sent Events stream and a reusable typed adapter, so streaming endpoints can be consumed safely and decoded into models without per-API conventions leaking into core.
SseStream— AutoCloseable Iterable bound to the response (#35)The SSE surface was a bare
Sequencewhose reader explicitly disclaimed ownership of the response, so a partial consume could strand the connection.SseStreamnow wraps the WHATWG parser and owns the response:AutoCloseable, Iterable<ServerSentEvent>. Closing it — explicitly, viause {}/ try-with-resources, or implicitly when iteration runs to completion — closes the underlying response/body and releases its pooled connection. This mirrors the close-on-partial-consume invariantPagedIterableenforces.close()is rejected;close()is idempotent and safe to call concurrently (e.g. cancellation from another thread).use {}still releases the response.Response.sseStream()extension opens a stream bound to the response body lifecycle.TypedSseStream<T>+SseEventMapper<T>— per-endpoint typed adapter (#62)A reusable runtime adapter turning
SseStreamintoAutoCloseable Iterable<T>by applying a caller-supplied(eventName, data) -> Result<T>mapper:Skip(keep-alives, bare cursors), or aDonesentinel that ends the stream and closes it. It is the seam where theSerdeSPI is invoked and where per-API done-sentinel / error-envelope conventions live — core holds none of them.Serdedeserialize inside it) runs only when the consumer pulls the next element, so a partial consume decodes only the events taken.SseStream.typed(mapper)extension for ergonomic wrapping.This is a hand-written runtime primitive — no code generation — fully usable today; a generator can target it later without embedding any per-API convention in core.
Tests
SseStreamTestandTypedSseStreamTestcover: full-iteration auto-close, explicit close,use {}close on partial consume, idempotent close, single-pass and is-closed guards, out-of-band close mid-iteration, reader-exception propagation,Response.sseStream()lifecycle binding and the no-body error, typed mapping via a recordingDeserializer, lazy per-element decode,Skip/Donehandling, mapper-exception propagation, and close propagation.Gated build (scoped,
--no-daemon)Result: BUILD SUCCESSFUL.
:sdk-core:apiDumpwas run and the regeneratedsdk-core/api/sdk-core.apiis committed.Closes #35
Closes #62