You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Inject URLSession into the transports and drop the shared delegate (#18)
* Inject URLSession into the transports and drop the shared delegate
URLSession retains its delegate until `invalidate`, so a session-level
delegate closed a retain cycle: the transport owned the session, the
session retained the delegate, the delegate held the transport. Since a
transport was built per API client, every client leaked a session and
they accumulated without bound. The `lazy var session` compounded it:
`lazy var` is not atomic, so two concurrent sends could initialise it
twice and release the first value out from under an in-flight request.
Sessions are now injected. The transports own nothing, so there is
nothing to invalidate and a caller can share one session across clients
and keep connection reuse.
- `URLSessionTransport` — one-shot requests via `data(for:)`; the
session owns task start, cancel and Task-cancellation propagation.
- `StreamURLSessionTransport` — SSE only, on a per-task delegate
(iOS 15+) instead of a session-level one. The task retains its own
delegate until completion, so no shared dictionary and no cycle.
Response and body are delivered as `AsyncThrowingStream`s, which
removes the hand-rolled buffering and its lock: `yield` is synchronous
and ordered, and `finish` after `finish` is a no-op, so exactly-once
completion is the stdlib's bookkeeping. `task.resume()` runs before
`withTaskCancellationHandler` is installed, so `cancel` cannot overlap
`resume` by program order rather than by serialisation.
Defects found along the way and fixed here:
- chunks were forwarded through a fresh `Task` each, and hops carry no
ordering guarantee, so bytes could be reordered;
- a non-HTTP response cancelled the task without resuming the
continuation, hanging the caller permanently;
- the task handler was never removed on the error path;
- `EventParser` rescanned the whole buffer after every append, so
assembling one event cost time quadratic in its length;
- `EventSourceDecodableErrorParser` never released its buffer, growing
without bound on the long-lived TonConnect bridge; it is now capped,
keeping the bytes for the one paired decode before releasing them.
Adds tests for both parsers. Raises the platform floor to iOS 15, which
the per-task delegate requires.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Report cancellation instead of a missing response
Cancelling the surrounding Task finishes the response stream's iterator
with `nil` rather than an error: `AsyncThrowingStream`'s own cancellation
handler resumes the pending continuation at once, well before ours can
round-trip through CFNetwork and let the delegate finish the stream. The
`guard` therefore reported every cancellation as `noResponse`, and would
also have handed back a response that arrived just before the
cancellation to a caller that no longer wanted it.
Adds a `URLProtocol`-stubbed test target for the transport covering the
cancellation path, response and byte-order delivery, and the non-HTTP
response that used to hang the caller. The fourth case — a failure
arriving after the response must terminate only the body stream — is
driven through the delegate instead: a stubbed loader discards a
response it has already queued when the protocol then fails, so it
cannot deliver that pair in that order at all.