fix: stabilize gateway ws + iOS

This commit is contained in:
Peter Steinberger
2026-01-19 06:22:01 +00:00
parent 73afbc9193
commit 3776de906f
14 changed files with 105 additions and 46 deletions

View File

@@ -44,7 +44,7 @@ public protocol WebSocketSessioning: AnyObject {
}
extension URLSession: WebSocketSessioning {
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
public func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
let task = self.webSocketTask(with: url)
// Avoid "Message too long" receive errors for large snapshots / history payloads.
task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB
@@ -54,6 +54,10 @@ extension URLSession: WebSocketSessioning {
public struct WebSocketSessionBox: @unchecked Sendable {
public let session: any WebSocketSessioning
public init(session: any WebSocketSessioning) {
self.session = session
}
}
public struct GatewayConnectOptions: Sendable {
@@ -472,7 +476,7 @@ public actor GatewayChannelActor {
public func request(
method: String,
params: [String: ClawdbotProtocol.AnyCodable]?,
params: [String: AnyCodable]?,
timeoutMs: Double? = nil) async throws -> Data
{
do {
@@ -525,8 +529,8 @@ public actor GatewayChannelActor {
if res.ok == false {
let code = res.error?["code"]?.value as? String
let msg = res.error?["message"]?.value as? String
let details: [String: ClawdbotProtocol.AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in
acc[pair.key] = ClawdbotProtocol.AnyCodable(pair.value.value)
let details: [String: AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in
acc[pair.key] = AnyCodable(pair.value.value)
}
throw GatewayResponseError(method: method, code: code, message: msg, details: details)
}

View File

@@ -26,6 +26,8 @@ public actor GatewayNodeSession {
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
private var canvasHostUrl: String?
public init() {}
public func connect(
url: URL,
token: String?,
@@ -107,9 +109,9 @@ public actor GatewayNodeSession {
public func sendEvent(event: String, payloadJSON: String?) async {
guard let channel = self.channel else { return }
let params: [String: ClawdbotProtocol.AnyCodable] = [
"event": ClawdbotProtocol.AnyCodable(event),
"payloadJSON": ClawdbotProtocol.AnyCodable(payloadJSON ?? NSNull()),
let params: [String: AnyCodable] = [
"event": AnyCodable(event),
"payloadJSON": AnyCodable(payloadJSON ?? NSNull()),
]
do {
_ = try await channel.request(method: "node.event", params: params, timeoutMs: 8000)
@@ -174,16 +176,16 @@ public actor GatewayNodeSession {
private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async {
guard let channel = self.channel else { return }
var params: [String: ClawdbotProtocol.AnyCodable] = [
"id": ClawdbotProtocol.AnyCodable(request.id),
"nodeId": ClawdbotProtocol.AnyCodable(request.nodeId),
"ok": ClawdbotProtocol.AnyCodable(response.ok),
"payloadJSON": ClawdbotProtocol.AnyCodable(response.payloadJSON ?? NSNull()),
var params: [String: AnyCodable] = [
"id": AnyCodable(request.id),
"nodeId": AnyCodable(request.nodeId),
"ok": AnyCodable(response.ok),
"payloadJSON": AnyCodable(response.payloadJSON ?? NSNull()),
]
if let error = response.error {
params["error"] = ClawdbotProtocol.AnyCodable([
"code": ClawdbotProtocol.AnyCodable(error.code.rawValue),
"message": ClawdbotProtocol.AnyCodable(error.message),
params["error"] = AnyCodable([
"code": AnyCodable(error.code.rawValue),
"message": AnyCodable(error.message),
])
}
do {
@@ -194,7 +196,7 @@ public actor GatewayNodeSession {
}
private func decodeParamsJSON(
_ paramsJSON: String?) throws -> [String: ClawdbotProtocol.AnyCodable]?
_ paramsJSON: String?) throws -> [String: AnyCodable]?
{
guard let paramsJSON, !paramsJSON.isEmpty else { return nil }
guard let data = paramsJSON.data(using: .utf8) else {
@@ -207,13 +209,13 @@ public actor GatewayNodeSession {
return nil
}
return dict.reduce(into: [:]) { acc, entry in
acc[entry.key] = ClawdbotProtocol.AnyCodable(entry.value)
acc[entry.key] = AnyCodable(entry.value)
}
}
private func broadcastServerEvent(_ evt: EventFrame) {
for (id, continuation) in self.serverEventSubscribers {
if continuation.yield(evt) == .terminated {
if case .terminated = continuation.yield(evt) {
self.serverEventSubscribers.removeValue(forKey: id)
}
}

View File

@@ -10,6 +10,14 @@ public enum GatewayPayloadDecoding {
return try JSONDecoder().decode(T.self, from: data)
}
public static func decode<T: Decodable>(
_ payload: AnyCodable,
as _: T.Type = T.self) throws -> T
{
let data = try JSONEncoder().encode(payload)
return try JSONDecoder().decode(T.self, from: data)
}
public static func decodeIfPresent<T: Decodable>(
_ payload: ClawdbotProtocol.AnyCodable?,
as _: T.Type = T.self) throws -> T?
@@ -17,4 +25,12 @@ public enum GatewayPayloadDecoding {
guard let payload else { return nil }
return try self.decode(payload, as: T.self)
}
public static func decodeIfPresent<T: Decodable>(
_ payload: AnyCodable?,
as _: T.Type = T.self) throws -> T?
{
guard let payload else { return nil }
return try self.decode(payload, as: T.self)
}
}

View File

@@ -12,6 +12,17 @@ public enum InstanceIdentity {
UserDefaults(suiteName: suiteName) ?? .standard
}
#if canImport(UIKit)
private static func readMainActor<T: Sendable>(_ body: @MainActor () -> T) -> T {
if Thread.isMainThread {
return MainActor.assumeIsolated { body() }
}
return DispatchQueue.main.sync {
MainActor.assumeIsolated { body() }
}
}
#endif
public static let instanceId: String = {
let defaults = Self.defaults
if let existing = defaults.string(forKey: instanceIdKey)?
@@ -28,7 +39,9 @@ public enum InstanceIdentity {
public static let displayName: String = {
#if canImport(UIKit)
let name = UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines)
let name = Self.readMainActor {
UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines)
}
return name.isEmpty ? "clawdbot" : name
#else
if let name = Host.current().localizedName?.trimmingCharacters(in: .whitespacesAndNewlines),
@@ -65,10 +78,12 @@ public enum InstanceIdentity {
public static let deviceFamily: String = {
#if canImport(UIKit)
switch UIDevice.current.userInterfaceIdiom {
case .pad: return "iPad"
case .phone: return "iPhone"
default: return "iOS"
return Self.readMainActor {
switch UIDevice.current.userInterfaceIdiom {
case .pad: return "iPad"
case .phone: return "iPhone"
default: return "iOS"
}
}
#else
return "Mac"
@@ -78,11 +93,12 @@ public enum InstanceIdentity {
public static let platformString: String = {
let v = ProcessInfo.processInfo.operatingSystemVersion
#if canImport(UIKit)
let name: String
switch UIDevice.current.userInterfaceIdiom {
case .pad: name = "iPadOS"
case .phone: name = "iOS"
default: name = "iOS"
let name = Self.readMainActor {
switch UIDevice.current.userInterfaceIdiom {
case .pad: return "iPadOS"
case .phone: return "iOS"
default: return "iOS"
}
}
return "\(name) \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
#else