feat(sse): reconnecting client with Last-Event-ID replay and backoff#4
Merged
Conversation
Lift SendSync, SendAsync, SyncPipelineLike, and AsyncPipelineLike out of pagination/paginator.py into the new pipeline/dispatch.py module. The paginator re-exports all four so the public surface (pagination.__init__) is unchanged. The forthcoming SSE reconnection feature will import the same types from pipeline/dispatch without depending on pagination. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add five tests exercising the sync reconnect lifecycle: mid-stream-drop with Last-Event-ID replay, clean-EOF reconnect, server retry: hint with exponential backoff, failure-counter reset after progress, and non-2xx permanent-failure guard. Fix _stream to drive SseParser directly so retry: frames without accompanying data: lines still propagate the server hint to the connection's retry_base before the next sleep, honouring the server's reconnect delay on pure retry-hint frames. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…haustion
When the reconnect budget was exhausted, both the sync and async SSE clients
raised a bare ServiceResponseError("SSE reconnect budget exhausted"), discarding
the last transient transport error that caused the final reconnect. The error
was caught and swallowed mid-stream, leaving callers without the root cause.
Capture the last transient error caught while streaming and chain it as the
exception cause (raise ... from last_error) when the budget is hit. The message
and exception type are unchanged, so existing matchers still pass; the cause now
carries the original transport failure for diagnosis.
Also adds the spec-listed tests the suite was missing: empty-id clearing of the
replay header, upward-only backoff jitter, async clean-EOF reconnect, and async
retry-hint-then-budget exhaustion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Adds a reconnecting Server-Sent Events client to
dexpace-sdk-core, layered on the existing sans-io SSE parser. The parser already turns a byte stream into events; this adds the connection lifecycle that long-lived SSE consumers need — transparent reconnection withLast-Event-IDreplay and backoff — with no new runtime dependency.What's added
SseConnectionandAsyncSseConnection(http.sse) iterate events across reconnections. Each (re)connection is dispatched through aPipeline/AsyncPipeline(or a bare send-callable), so retry, auth, redirect, and tracing apply per connection.retryaccessor onSseParser/AsyncSseStream, so a reconnecting client can read the server's sticky reconnect hint even from aretry:-only frame that emits no event.SyncPipelineLike/AsyncPipelineLikeand the send-callable aliases) are lifted from the paginator intopipeline.dispatchso the paginator and the SSE client share one definition. The pagination package keeps re-exporting them, so its public surface is unchanged.Behavior
EventSourcesemantics); a non-success HTTP status is a permanent failure and raisesHttpResponseErrorwithout reconnecting.Last-Event-IDheader; an explicit empty id clears it.retry:value as its base (falling back to a configurable default), grows exponentially across consecutive failures, is jittered upward only (so a reconnect never wakes before the suggested interval), and is capped. The failure counter resets once a connection yields an event.max_reconnectsbounds consecutive failures; on exhaustion it raises, chaining the last transport error as the cause.data: [DONE]sentinel) or via the (async) context manager.Verification
ruff check,ruff format --check,mypy --strict(216 source files), and the fullpytestsuite (1013 passing) are green. New tests cover reconnect-on-drop and clean-EOF,Last-Event-IDreplay and empty-id clearing, server-retry:backoff, upward-jitter direction, exponential growth, failure-counter reset,max_reconnectsexhaustion with cause chaining, non-success raises, and async cancellation with deterministic cleanup — for both the sync and async clients.