Add WsClient and HttpClient implementations for livekit-net. - #1073
Add WsClient and HttpClient implementations for livekit-net.#1073jhugman wants to merge 5 commits into
Conversation
| func recv() async throws -> Data? { | ||
| guard task.closeCode == .invalid else { return nil } | ||
| do { | ||
| let message: URLSessionWebSocketTask.Message = try await withCheckedThrowingContinuation { cont in | ||
| task.receive { cont.resume(with: $0) } | ||
| } | ||
| return Self.decode(message) | ||
| } catch { | ||
| if task.closeCode != .invalid { return nil } | ||
| throw Self.map(error) | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Cancelled websocket operations never finish, keeping the socket and its session alive
The websocket receive and connect steps wait on a callback with no cancellation path (withCheckedThrowingContinuation at Sources/LiveKit/Net/LKNetWSClient.swift:79-81), so when the caller cancels, the wait never ends on an idle socket and the connection is held open forever.
Impact: Cancelled network work can hang indefinitely and leak the underlying socket and session for the lifetime of the app.
Missing cooperative cancellation compared to the existing WebSocket implementation
Sources/LiveKit/Support/Network/WebSocket.swift:57-64 wraps the connect continuation in withTaskCancellationHandler and also has Delegate.cancelConnection() (Sources/LiveKit/Support/Network/WebSocket.swift:71-74,122-127) so a cancelled connect resumes the continuation; WebSocket.AsyncIterator.next() (Sources/LiveKit/Support/Network/WebSocket.swift:83-101) wraps task.receive in withTaskCancellationHandler and returns nil when Task.isCancelled.
The new LKNetWSConnection.init (Sources/LiveKit/Net/LKNetWSClient.swift:48-56) and recv() have neither: on cancellation the continuation stays suspended until the underlying URLSession callback eventually fires. For recv() on an idle, healthy socket no callback ever fires, so the suspended async frame keeps self (and thus the URLSessionWebSocketTask and the URLSession created with a delegate) alive indefinitely, and deinit's cleanup never runs. The same lack of cancellation applies to LKNetHTTPClientLive.request (Sources/LiveKit/Net/LKNetHTTPClient.swift:22-29), where the data task is not cancelled when the awaiting task is cancelled.
AGENTS.md: "Long-running Task requires cooperative cancellation to avoid memory leaks".
Prompt for agents
LKNetWSConnection.recv() and LKNetWSConnection.init(), plus LKNetHTTPClientLive.request(), await callback-based URLSession APIs via withCheckedThrowingContinuation without any cancellation handling. Mirror the pattern already used in Sources/LiveKit/Support/Network/WebSocket.swift: wrap the continuations in withTaskCancellationHandler, cancel/close the underlying task (or resume a stored connect continuation via a cancelConnection()-style method on WSConnectDelegate) from the onCancel closure, and treat cancellation in recv() as end-of-stream (return nil) as the existing AsyncIterator does. For the HTTP client, cancel the URLSessionDataTask in onCancel so the continuation is always resumed.
Was this helpful? React with 👍 or 👎 to provide feedback.
| @@ -0,0 +1,23 @@ | |||
| // Copyright 2026 LiveKit (Apache-2.0) | |||
There was a problem hiding this comment.
🟡 New source and test files are missing the required license header
The newly added files start with a one-line copyright comment instead of the mandated Apache header block (// Copyright 2026 LiveKit (Apache-2.0) at Sources/LiveKit/Net/NetBootstrap.swift:1), so the repository's formatting check will rewrite/fail on them.
Impact: The formatting/lint gate flags these files, blocking a clean build of the change.
Header enforced by .swiftformat
.swiftformat declares --header "/*\n * Copyright {year} LiveKit\n * ... Apache License ... */", and every existing source file (e.g. Sources/LiveKit/Support/Network/WebSocket.swift:1-16) carries that block. All six new files use the abbreviated one-liner instead: Sources/LiveKit/Net/NetBootstrap.swift:1, Sources/LiveKit/Net/LKNetHTTPClient.swift:1, Sources/LiveKit/Net/LKNetWSClient.swift:1, Tests/LiveKitCoreTests/Net/LKNetHTTPClientLiveTests.swift:1, Tests/LiveKitCoreTests/Net/LKNetWSConnectionTests.swift:1, Tests/LiveKitCoreTests/Net/NetBootstrapTests.swift:1, Tests/LiveKitCoreTests/Net/NetFFIRoundTripTests.swift:1.
| // Copyright 2026 LiveKit (Apache-2.0) | |
| /* | |
| * Copyright 2026 LiveKit | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Nice, but how it's consumed on the Rust side now? It's not? |
Initialize
livekit-netwith a Swift implementation of theWsClientandHttpClient. Production versions of these useURLSession.This requires livekit/rust-sdks#1290 to be built and released.
For testing, there is a self-test which calls through Rust into dummy implementations of
WsClientandHttpClient.