Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayWebSocketTransport.swift
NianJiu 067edbcb2d fix(ios): make Talk audio lifecycle-safe (#103072)
* fix(ios): cancel queued push-to-talk invokes

* fix(ios): make push-to-talk cancellation lifecycle-safe

* style(ios): use switch expression for PTT result

* fix(ios): bind audio capture to lifecycle owners

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* style(ios): format rebased lifecycle changes

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* fix(ios): preserve routing hydration across reconnects

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* fix(ios): cancel routing hydration on target switch

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* chore(ios): refresh native i18n inventory

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* fix(i18n): preserve native localization IDs

* chore: leave changelog to release automation

* refactor(apple): split lifecycle and gateway helpers

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>

* fix(ios): make lifecycle parsing nonisolated

* fix(ios): consume canonical Talk events

---------

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-10 15:03:40 +01:00

111 lines
3.7 KiB
Swift

import Foundation
public protocol WebSocketTasking: AnyObject {
var state: URLSessionTask.State { get }
func resume()
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
func send(_ message: URLSessionWebSocketTask.Message) async throws
func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void)
func receive() async throws -> URLSessionWebSocketTask.Message
func receive(completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
}
extension URLSessionWebSocketTask: WebSocketTasking {}
private final class WebSocketPingContinuationGate: @unchecked Sendable {
private let lock = NSLock()
private var didResume = false
func resumeOnce(_ resume: () -> Void) {
self.lock.lock()
if self.didResume {
self.lock.unlock()
return
}
self.didResume = true
self.lock.unlock()
resume()
}
}
public struct WebSocketTaskBox: @unchecked Sendable {
public let task: any WebSocketTasking
public init(task: any WebSocketTasking) {
self.task = task
}
public var state: URLSessionTask.State {
self.task.state
}
public func resume() {
self.task.resume()
}
public func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
self.task.cancel(with: closeCode, reason: reason)
}
public func send(_ message: URLSessionWebSocketTask.Message) async throws {
try await self.task.send(message)
}
public func receive() async throws -> URLSessionWebSocketTask.Message {
try await self.task.receive()
}
public func receive(
completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
{
self.task.receive(completionHandler: completionHandler)
}
public func sendPing() async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let gate = WebSocketPingContinuationGate()
self.task.sendPing { error in
// URLSession can race ping callbacks with cancellation; only the first
// pong result owns this checked continuation or Swift traps the app.
gate.resumeOnce {
ThrowingContinuationSupport.resumeVoid(continuation, error: error)
}
}
}
}
}
public protocol WebSocketSessioning: AnyObject {
func makeWebSocketTask(url: URL) -> WebSocketTaskBox
func makeWebSocketTask(request: URLRequest) -> WebSocketTaskBox
}
extension WebSocketSessioning {
/// Compatibility path for existing session conformers. URLSession and pinning sessions
/// override this requirement so operator headers remain attached to the upgrade request.
public func makeWebSocketTask(request: URLRequest) -> WebSocketTaskBox {
guard let url = request.url else { preconditionFailure("WebSocket request URL is required") }
return self.makeWebSocketTask(url: url)
}
}
extension URLSession: WebSocketSessioning {
public func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
self.makeWebSocketTask(request: URLRequest(url: url))
}
public func makeWebSocketTask(request: URLRequest) -> WebSocketTaskBox {
let task = self.webSocketTask(with: request)
// Avoid "Message too long" receive errors for large snapshots / history payloads.
task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB
return WebSocketTaskBox(task: task)
}
}
public struct WebSocketSessionBox: @unchecked Sendable {
public let session: any WebSocketSessioning
public init(session: any WebSocketSessioning) {
self.session = session
}
}