Mac: build GatewayProtocol target and typed presence handling

This commit is contained in:
Peter Steinberger
2025-12-09 15:35:06 +01:00
parent a7737912b0
commit 336c9d6caa
6 changed files with 262 additions and 101 deletions

View File

@@ -120,7 +120,8 @@ private actor GatewayChannelActor {
}
switch frame {
case let .res(res):
if let id = res.id, let waiter = pending.removeValue(forKey: id) {
let id = res.id
if let waiter = pending.removeValue(forKey: id) {
waiter.resume(returning: .res(res))
}
case let .event(evt):
@@ -182,12 +183,14 @@ private actor GatewayChannelActor {
throw NSError(domain: "Gateway", code: 2, userInfo: [NSLocalizedDescriptionKey: "unexpected frame"])
}
if res.ok == false {
let msg = (res.error?.message) ?? "gateway error"
let msg = (res.error?["message"]?.value as? String) ?? "gateway error"
throw NSError(domain: "Gateway", code: 3, userInfo: [NSLocalizedDescriptionKey: msg])
}
if let payload = res.payload?.value {
let payloadData = try JSONSerialization.data(withJSONObject: payload)
return payloadData
if JSONSerialization.isValidJSONObject(payload) {
let payloadData = try JSONSerialization.data(withJSONObject: payload)
return payloadData
}
}
return Data()
}

View File

@@ -97,8 +97,8 @@ final class InstancesStore: ObservableObject {
let frame = note.object as? GatewayFrame else { return }
switch frame {
case let .helloOk(hello):
let presence = hello.snapshot.presence
if let data = try? JSONEncoder().encode(presence) {
if JSONSerialization.isValidJSONObject(hello.snapshot.presence),
let data = try? JSONEncoder().encode(hello.snapshot.presence) {
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
}
default:

View File

@@ -2,12 +2,12 @@ import Foundation
/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads.
/// Marked `@unchecked Sendable` because it can hold reference types.
struct AnyCodable: Codable, @unchecked Sendable {
let value: Any
public struct AnyCodable: Codable, @unchecked Sendable {
public let value: Any
init(_ value: Any) { self.value = value }
public init(_ value: Any) { self.value = value }
init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
@@ -21,7 +21,7 @@ struct AnyCodable: Codable, @unchecked Sendable {
debugDescription: "Unsupported type")
}
func encode(to encoder: Encoder) throws {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let intVal as Int: try container.encode(intVal)

View File

@@ -12,76 +12,100 @@ public enum ErrorCode: String, Codable {
public struct Hello: Codable {
public let type: String
public let minProtocol: Int
public let maxProtocol: Int
public let minprotocol: Int
public let maxprotocol: Int
public let client: [String: AnyCodable]
public let caps: [String]?
public let auth: [String: AnyCodable]?
public let locale: String?
public let userAgent: String?
public let useragent: String?
public init(
type: String,
minProtocol: Int,
maxProtocol: Int,
minprotocol: Int,
maxprotocol: Int,
client: [String: AnyCodable],
caps: [String]?,
auth: [String: AnyCodable]?,
locale: String?,
userAgent: String?
useragent: String?
) {
self.type = type
self.minProtocol = minProtocol
self.maxProtocol = maxProtocol
self.minprotocol = minprotocol
self.maxprotocol = maxprotocol
self.client = client
self.caps = caps
self.auth = auth
self.locale = locale
self.userAgent = userAgent
self.useragent = useragent
}
private enum CodingKeys: String, CodingKey {
case type
case minprotocol = "minProtocol"
case maxprotocol = "maxProtocol"
case client
case caps
case auth
case locale
case useragent = "userAgent"
}
}
public struct HelloOk: Codable {
public let type: String
public let protocol: Int
public let _protocol: Int
public let server: [String: AnyCodable]
public let features: [String: AnyCodable]
public let snapshot: [String: AnyCodable]
public let snapshot: Snapshot
public let policy: [String: AnyCodable]
public init(
type: String,
protocol: Int,
_protocol: Int,
server: [String: AnyCodable],
features: [String: AnyCodable],
snapshot: [String: AnyCodable],
snapshot: Snapshot,
policy: [String: AnyCodable]
) {
self.type = type
self.protocol = protocol
self._protocol = _protocol
self.server = server
self.features = features
self.snapshot = snapshot
self.policy = policy
}
private enum CodingKeys: String, CodingKey {
case type
case _protocol = "protocol"
case server
case features
case snapshot
case policy
}
}
public struct HelloError: Codable {
public let type: String
public let reason: String
public let expectedProtocol: Int?
public let minClient: String?
public let expectedprotocol: Int?
public let minclient: String?
public init(
type: String,
reason: String,
expectedProtocol: Int?,
minClient: String?
expectedprotocol: Int?,
minclient: String?
) {
self.type = type
self.reason = reason
self.expectedProtocol = expectedProtocol
self.minClient = minClient
self.expectedprotocol = expectedprotocol
self.minclient = minclient
}
private enum CodingKeys: String, CodingKey {
case type
case reason
case expectedprotocol = "expectedProtocol"
case minclient = "minClient"
}
}
@@ -102,6 +126,12 @@ public struct RequestFrame: Codable {
self.method = method
self.params = params
}
private enum CodingKeys: String, CodingKey {
case type
case id
case method
case params
}
}
public struct ResponseFrame: Codable {
@@ -124,6 +154,13 @@ public struct ResponseFrame: Codable {
self.payload = payload
self.error = error
}
private enum CodingKeys: String, CodingKey {
case type
case id
case ok
case payload
case error
}
}
public struct EventFrame: Codable {
@@ -131,20 +168,27 @@ public struct EventFrame: Codable {
public let event: String
public let payload: AnyCodable?
public let seq: Int?
public let stateVersion: [String: AnyCodable]?
public let stateversion: [String: AnyCodable]?
public init(
type: String,
event: String,
payload: AnyCodable?,
seq: Int?,
stateVersion: [String: AnyCodable]?
stateversion: [String: AnyCodable]?
) {
self.type = type
self.event = event
self.payload = payload
self.seq = seq
self.stateVersion = stateVersion
self.stateversion = stateversion
}
private enum CodingKeys: String, CodingKey {
case type
case event
case payload
case seq
case stateversion = "stateVersion"
}
}
@@ -153,35 +197,47 @@ public struct PresenceEntry: Codable {
public let ip: String?
public let version: String?
public let mode: String?
public let lastInputSeconds: Int?
public let lastinputseconds: Int?
public let reason: String?
public let tags: [String]?
public let text: String?
public let ts: Int
public let instanceId: String?
public let instanceid: String?
public init(
host: String?,
ip: String?,
version: String?,
mode: String?,
lastInputSeconds: Int?,
lastinputseconds: Int?,
reason: String?,
tags: [String]?,
text: String?,
ts: Int,
instanceId: String?
instanceid: String?
) {
self.host = host
self.ip = ip
self.version = version
self.mode = mode
self.lastInputSeconds = lastInputSeconds
self.lastinputseconds = lastinputseconds
self.reason = reason
self.tags = tags
self.text = text
self.ts = ts
self.instanceId = instanceId
self.instanceid = instanceid
}
private enum CodingKeys: String, CodingKey {
case host
case ip
case version
case mode
case lastinputseconds = "lastInputSeconds"
case reason
case tags
case text
case ts
case instanceid = "instanceId"
}
}
@@ -196,24 +252,34 @@ public struct StateVersion: Codable {
self.presence = presence
self.health = health
}
private enum CodingKeys: String, CodingKey {
case presence
case health
}
}
public struct Snapshot: Codable {
public let presence: [[String: AnyCodable]]
public let presence: [PresenceEntry]
public let health: AnyCodable
public let stateVersion: [String: AnyCodable]
public let uptimeMs: Int
public let stateversion: StateVersion
public let uptimems: Int
public init(
presence: [[String: AnyCodable]],
presence: [PresenceEntry],
health: AnyCodable,
stateVersion: [String: AnyCodable],
uptimeMs: Int
stateversion: StateVersion,
uptimems: Int
) {
self.presence = presence
self.health = health
self.stateVersion = stateVersion
self.uptimeMs = uptimeMs
self.stateversion = stateversion
self.uptimems = uptimems
}
private enum CodingKeys: String, CodingKey {
case presence
case health
case stateversion = "stateVersion"
case uptimems = "uptimeMs"
}
}
@@ -222,92 +288,122 @@ public struct ErrorShape: Codable {
public let message: String
public let details: AnyCodable?
public let retryable: Bool?
public let retryAfterMs: Int?
public let retryafterms: Int?
public init(
code: String,
message: String,
details: AnyCodable?,
retryable: Bool?,
retryAfterMs: Int?
retryafterms: Int?
) {
self.code = code
self.message = message
self.details = details
self.retryable = retryable
self.retryAfterMs = retryAfterMs
self.retryafterms = retryafterms
}
private enum CodingKeys: String, CodingKey {
case code
case message
case details
case retryable
case retryafterms = "retryAfterMs"
}
}
public struct AgentEvent: Codable {
public let runId: String
public let runid: String
public let seq: Int
public let stream: String
public let ts: Int
public let data: [String: AnyCodable]
public init(
runId: String,
runid: String,
seq: Int,
stream: String,
ts: Int,
data: [String: AnyCodable]
) {
self.runId = runId
self.runid = runid
self.seq = seq
self.stream = stream
self.ts = ts
self.data = data
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case seq
case stream
case ts
case data
}
}
public struct SendParams: Codable {
public let to: String
public let message: String
public let mediaUrl: String?
public let mediaurl: String?
public let provider: String?
public let idempotencyKey: String
public let idempotencykey: String
public init(
to: String,
message: String,
mediaUrl: String?,
mediaurl: String?,
provider: String?,
idempotencyKey: String
idempotencykey: String
) {
self.to = to
self.message = message
self.mediaUrl = mediaUrl
self.mediaurl = mediaurl
self.provider = provider
self.idempotencyKey = idempotencyKey
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case to
case message
case mediaurl = "mediaUrl"
case provider
case idempotencykey = "idempotencyKey"
}
}
public struct AgentParams: Codable {
public let message: String
public let to: String?
public let sessionId: String?
public let sessionid: String?
public let thinking: String?
public let deliver: Bool?
public let timeout: Int?
public let idempotencyKey: String
public let idempotencykey: String
public init(
message: String,
to: String?,
sessionId: String?,
sessionid: String?,
thinking: String?,
deliver: Bool?,
timeout: Int?,
idempotencyKey: String
idempotencykey: String
) {
self.message = message
self.to = to
self.sessionId = sessionId
self.sessionid = sessionid
self.thinking = thinking
self.deliver = deliver
self.timeout = timeout
self.idempotencyKey = idempotencyKey
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case message
case to
case sessionid = "sessionId"
case thinking
case deliver
case timeout
case idempotencykey = "idempotencyKey"
}
}
@@ -319,25 +415,32 @@ public struct TickEvent: Codable {
) {
self.ts = ts
}
private enum CodingKeys: String, CodingKey {
case ts
}
}
public struct ShutdownEvent: Codable {
public let reason: String
public let restartExpectedMs: Int?
public let restartexpectedms: Int?
public init(
reason: String,
restartExpectedMs: Int?
restartexpectedms: Int?
) {
self.reason = reason
self.restartExpectedMs = restartExpectedMs
self.restartexpectedms = restartexpectedms
}
private enum CodingKeys: String, CodingKey {
case reason
case restartexpectedms = "restartExpectedMs"
}
}
public enum GatewayFrame: Codable {
case hello(Hello)
case hello-ok(HelloOk)
case hello-error(HelloError)
case helloOk(HelloOk)
case helloError(HelloError)
case req(RequestFrame)
case res(ResponseFrame)
case event(EventFrame)
@@ -351,17 +454,17 @@ public enum GatewayFrame: Codable {
}
switch type {
case "hello":
self = .hello(try decodePayload(Hello.self, from: raw))
self = .hello(try Self.decodePayload(Hello.self, from: raw))
case "hello-ok":
self = .helloOk(try decodePayload(HelloOk.self, from: raw))
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
case "hello-error":
self = .helloError(try decodePayload(HelloError.self, from: raw))
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
case "req":
self = .req(try decodePayload(RequestFrame.self, from: raw))
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
case "res":
self = .res(try decodePayload(ResponseFrame.self, from: raw))
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
case "event":
self = .event(try decodePayload(EventFrame.self, from: raw))
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
default:
self = .unknown(type: type, raw: raw)
}
@@ -382,7 +485,7 @@ public enum GatewayFrame: Codable {
}
private func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
let data = try JSONSerialization.data(withJSONObject: raw)
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)