Gateway: finalize WS control plane

This commit is contained in:
Peter Steinberger
2025-12-09 14:41:41 +01:00
parent 9ef1545d06
commit b2e7fb01a9
23 changed files with 5209 additions and 2495 deletions

View File

@@ -1,4 +1,3 @@
import Darwin
import Foundation
import OSLog
@@ -9,73 +8,52 @@ struct ControlRequestParams: @unchecked Sendable {
actor AgentRPC {
static let shared = AgentRPC()
struct HeartbeatEvent: Codable {
let ts: Double
let status: String
let to: String?
let preview: String?
let durationMs: Double?
let hasMedia: Bool?
let reason: String?
}
static let heartbeatNotification = Notification.Name("clawdis.rpc.heartbeat")
static let agentEventNotification = Notification.Name("clawdis.rpc.agent")
private struct ControlResponse: Decodable {
let type: String
let id: String
let ok: Bool
let payload: AnyCodable?
let error: String?
}
struct AnyCodable: Codable {
let value: Any
init(_ value: Any) { self.value = value }
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 }
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
if container.decodeNil() { self.value = NSNull(); return }
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
if let array = try? container.decode([AnyCodable].self) { self.value = array; return }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let intVal as Int: try container.encode(intVal)
case let doubleVal as Double: try container.encode(doubleVal)
case let boolVal as Bool: try container.encode(boolVal)
case let stringVal as String: try container.encode(stringVal)
case is NSNull: try container.encodeNil()
case let dict as [String: AnyCodable]: try container.encode(dict)
case let array as [AnyCodable]: try container.encode(array)
default:
let context = EncodingError.Context(
codingPath: encoder.codingPath,
debugDescription: "Unsupported type")
throw EncodingError.invalidValue(self.value, context)
}
}
}
private var process: Process?
private var stdinHandle: FileHandle?
private var stdoutHandle: FileHandle?
private var buffer = Data()
private var waiters: [CheckedContinuation<String, Error>] = []
private var controlWaiters: [String: CheckedContinuation<Data, Error>] = [:]
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "agent.rpc")
private var starting = false
private let gateway = GatewayChannel()
private var configured = false
private struct RpcError: Error { let message: String }
private var gatewayURL: URL {
let port = UserDefaults.standard.integer(forKey: "gatewayPort")
let effectivePort = port > 0 ? port : 18789
return URL(string: "ws://127.0.0.1:\(effectivePort)")!
}
private var gatewayToken: String? {
ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"]
}
func start() async throws {
if configured { return }
await gateway.configure(url: gatewayURL, token: gatewayToken)
configured = true
}
func shutdown() async {
// no-op for WS; socket managed by GatewayChannel
}
func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool {
do {
_ = try await controlRequest(method: "set-heartbeats", params: ControlRequestParams(raw: ["enabled": AnyHashable(enabled)]))
return true
} catch {
logger.error("setHeartbeatsEnabled failed \(error.localizedDescription, privacy: .public)")
return false
}
}
func status() async -> (ok: Bool, error: String?) {
do {
let data = try await controlRequest(method: "status")
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
(obj["ok"] as? Bool) ?? true {
return (true, nil)
}
return (false, "status error")
} catch {
return (false, error.localizedDescription)
}
}
func send(
text: String,
@@ -84,305 +62,25 @@ actor AgentRPC {
deliver: Bool,
to: String?) async -> (ok: Bool, text: String?, error: String?)
{
if self.process?.isRunning != true {
do {
try await self.start()
} catch {
return (false, nil, "rpc worker not running: \(error.localizedDescription)")
}
}
do {
var payload: [String: Any] = [
"type": "send",
"text": text,
"session": session,
"thinking": thinking ?? "default",
"deliver": deliver,
let params: [String: AnyHashable] = [
"message": AnyHashable(text),
"sessionId": AnyHashable(session),
"thinking": AnyHashable(thinking ?? "default"),
"deliver": AnyHashable(deliver),
"to": AnyHashable(to ?? ""),
"idempotencyKey": AnyHashable(UUID().uuidString),
]
if let to { payload["to"] = to }
let data = try JSONSerialization.data(withJSONObject: payload)
guard let stdinHandle else { throw RpcError(message: "stdin missing") }
stdinHandle.write(data)
stdinHandle.write(Data([0x0A]))
let parsed = try await self.nextJSONObject()
if let ok = parsed["ok"] as? Bool, let type = parsed["type"] as? String, type == "result" {
if ok {
if let payloadDict = parsed["payload"] as? [String: Any],
let payloads = payloadDict["payloads"] as? [[String: Any]],
let first = payloads.first,
let txt = first["text"] as? String
{
return (true, txt, nil)
}
return (true, nil, nil)
}
}
if let err = parsed["error"] as? String {
return (false, nil, err)
}
return (false, nil, "rpc returned unexpected response: \(parsed)")
_ = try await controlRequest(method: "agent", params: ControlRequestParams(raw: params))
return (true, nil, nil)
} catch {
self.logger.error("rpc send failed: \(error.localizedDescription, privacy: .public)")
await self.stop()
return (false, nil, error.localizedDescription)
}
}
func status() async -> (ok: Bool, error: String?) {
if self.process?.isRunning != true {
do {
try await self.start()
} catch {
return (false, "rpc worker not running: \(error.localizedDescription)")
}
}
do {
let payload: [String: Any] = ["type": "status"]
let data = try JSONSerialization.data(withJSONObject: payload)
guard let stdinHandle else { throw RpcError(message: "stdin missing") }
stdinHandle.write(data)
stdinHandle.write(Data([0x0A]))
let parsed = try await self.nextJSONObject()
if let ok = parsed["ok"] as? Bool, ok { return (true, nil) }
return (false, parsed["error"] as? String ?? "rpc status failed: \(parsed)")
} catch {
self.logger.error("rpc status failed: \(error.localizedDescription, privacy: .public)")
await self.stop()
return (false, error.localizedDescription)
}
}
func setHeartbeatsEnabled(_ enabled: Bool) async -> Bool {
guard self.process?.isRunning == true else { return false }
do {
let payload: [String: Any] = ["type": "set-heartbeats", "enabled": enabled]
let data = try JSONSerialization.data(withJSONObject: payload)
guard let stdinHandle else { throw RpcError(message: "stdin missing") }
stdinHandle.write(data)
stdinHandle.write(Data([0x0A]))
let line = try await nextLine()
let parsed = try JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any]
if let ok = parsed?["ok"] as? Bool, ok { return true }
return false
} catch {
self.logger.error("rpc set-heartbeats failed: \(error.localizedDescription, privacy: .public)")
await self.stop()
return false
}
}
func controlRequest(method: String, params: ControlRequestParams? = nil) async throws -> Data {
if self.process?.isRunning != true {
try await self.start()
}
let id = UUID().uuidString
var frame: [String: Any] = ["type": "control-request", "id": id, "method": method]
if let params { frame["params"] = params.raw }
let data = try JSONSerialization.data(withJSONObject: frame)
guard let stdinHandle else { throw RpcError(message: "stdin missing") }
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Data, Error>) in
self.controlWaiters[id] = cont
stdinHandle.write(data)
stdinHandle.write(Data([0x0A]))
}
}
// MARK: - Process lifecycle
func start() async throws {
if self.starting { return }
self.starting = true
defer { self.starting = false }
let process = Process()
let command = CommandResolver.clawdisCommand(subcommand: "rpc")
process.executableURL = URL(fileURLWithPath: command.first ?? "/usr/bin/env")
process.arguments = Array(command.dropFirst())
process.currentDirectoryURL = URL(fileURLWithPath: CommandResolver.projectRootPath())
var env = ProcessInfo.processInfo.environment
env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
process.environment = env
let stdinPipe = Pipe()
let stdoutPipe = Pipe()
process.standardInput = stdinPipe
process.standardOutput = stdoutPipe
process.standardError = Pipe()
try process.run()
self.process = process
self.stdinHandle = stdinPipe.fileHandleForWriting
self.stdoutHandle = stdoutPipe.fileHandleForReading
stdoutPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
guard let self else { return }
let data = handle.availableData
if data.isEmpty { return }
Task { await self.ingest(data: data) }
}
Task.detached { [weak self] in
// Ensure all waiters are failed if the worker dies (e.g., crash or SIGTERM).
process.waitUntilExit()
await self?.stop()
}
}
func shutdown() async {
await self.stop()
}
private func stop() async {
self.stdoutHandle?.readabilityHandler = nil
let proc = self.process
proc?.terminate()
if let proc, proc.isRunning {
try? await Task.sleep(nanoseconds: 700_000_000)
if proc.isRunning {
kill(proc.processIdentifier, SIGKILL)
}
}
proc?.waitUntilExit()
self.process = nil
self.stdinHandle = nil
self.stdoutHandle = nil
self.buffer.removeAll(keepingCapacity: false)
let waiters = self.waiters
self.waiters.removeAll()
for waiter in waiters {
waiter.resume(throwing: RpcError(message: "rpc process stopped"))
}
let control = self.controlWaiters
self.controlWaiters.removeAll()
for (_, waiter) in control {
waiter.resume(throwing: RpcError(message: "rpc process stopped"))
}
}
private func ingest(data: Data) {
self.buffer.append(data)
while let range = buffer.firstRange(of: Data([0x0A])) {
let lineData = self.buffer.subdata(in: self.buffer.startIndex..<range.lowerBound)
self.buffer.removeSubrange(self.buffer.startIndex...range.lowerBound)
guard let line = String(data: lineData, encoding: .utf8) else { continue }
// Event frames are pushed without request/response pairing (e.g., heartbeats/agent).
if self.handleEventLine(line) {
continue
}
if self.handleControlResponse(line) {
continue
}
if let waiter = waiters.first {
self.waiters.removeFirst()
waiter.resume(returning: line)
}
}
}
/// Read the next line that successfully parses as JSON. Non-JSON lines (e.g., stray stdout logs)
/// are skipped to keep the RPC bridge resilient to accidental prints.
private func nextJSONObject(maxSkips: Int = 30) async throws -> [String: Any] {
var skipped = 0
while true {
let line = try await self.nextLine()
guard let data = line.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
skipped += 1
if skipped >= maxSkips {
throw RpcError(message: "rpc returned non-JSON output: \(line)")
}
continue
}
return obj
}
}
private func parseHeartbeatEvent(from line: String) -> HeartbeatEvent? {
guard let data = line.data(using: .utf8) else { return nil }
guard
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = obj["type"] as? String,
type == "event",
let evt = obj["event"] as? String,
evt == "heartbeat",
let payload = obj["payload"] as? [String: Any]
else {
return nil
}
let decoder = JSONDecoder()
guard let payloadData = try? JSONSerialization.data(withJSONObject: payload) else { return nil }
return try? decoder.decode(HeartbeatEvent.self, from: payloadData)
}
private func parseAgentEvent(from line: String) -> ControlAgentEvent? {
guard let data = line.data(using: .utf8) else { return nil }
guard
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = obj["type"] as? String,
type == "event",
let evt = obj["event"] as? String,
evt == "agent",
let payload = obj["payload"]
else {
return nil
}
guard let payloadData = try? JSONSerialization.data(withJSONObject: payload) else { return nil }
return try? JSONDecoder().decode(ControlAgentEvent.self, from: payloadData)
}
private func handleEventLine(_ line: String) -> Bool {
if let hb = self.parseHeartbeatEvent(from: line) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: Self.heartbeatNotification, object: hb)
NotificationCenter.default.post(name: .controlHeartbeat, object: hb)
}
return true
}
if let agent = self.parseAgentEvent(from: line) {
DispatchQueue.main.async {
NotificationCenter.default.post(name: Self.agentEventNotification, object: agent)
NotificationCenter.default.post(name: .controlAgentEvent, object: agent)
}
return true
}
return false
}
private func handleControlResponse(_ line: String) -> Bool {
guard let data = line.data(using: .utf8) else { return false }
guard let parsed = try? JSONDecoder().decode(ControlResponse.self, from: data) else { return false }
guard parsed.type == "control-response" else { return false }
self.logger.debug("control response parsed id=\(parsed.id, privacy: .public) ok=\(parsed.ok, privacy: .public)")
guard let waiter = self.controlWaiters.removeValue(forKey: parsed.id) else {
self.logger.debug("control response with no waiter id=\(parsed.id, privacy: .public)")
return true
}
if parsed.ok {
let payloadData: Data = {
if let payload = parsed.payload {
return (try? JSONEncoder().encode(payload)) ?? Data()
}
// Use an empty JSON array to keep callers happy when payload is missing.
return Data("[]".utf8)
}()
waiter.resume(returning: payloadData)
} else {
waiter.resume(throwing: RpcError(message: parsed.error ?? "control error"))
}
return true
}
private func nextLine() async throws -> String {
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<String, Error>) in
self.waiters.append(cont)
}
try await start()
let rawParams = params?.raw.reduce(into: [String: Any]()) { $0[$1.key] = $1.value }
return try await gateway.request(method: method, params: rawParams)
}
}

View File

@@ -77,20 +77,26 @@ final class ControlChannel: ObservableObject {
case degraded(String)
}
enum Mode: Equatable {
case local
case remote(target: String, identity: String)
}
@Published private(set) var state: ConnectionState = .disconnected
@Published private(set) var lastPingMs: Double?
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "control")
private let gateway = GatewayChannel()
private var gatewayURL: URL {
let port = UserDefaults.standard.integer(forKey: "gatewayPort")
let effectivePort = port > 0 ? port : 18789
return URL(string: "ws://127.0.0.1:\(effectivePort)")!
}
private var gatewayToken: String? {
ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"]
}
private var eventTokens: [NSObjectProtocol] = []
func configure() async {
do {
self.state = .connecting
try await AgentRPC.shared.start()
await gateway.configure(url: gatewayURL, token: gatewayToken)
self.startEventStream()
self.state = .connected
PresenceReporter.shared.sendImmediate(reason: "connect")
} catch {
@@ -98,16 +104,16 @@ final class ControlChannel: ObservableObject {
}
}
func configure(mode: Mode) async throws {
// Mode is retained for API compatibility; transport is always stdio now.
await self.configure()
}
func configure(mode _: Any? = nil) async throws { await self.configure() }
func health(timeout: TimeInterval? = nil) async throws -> Data {
let params = timeout.map { ControlRequestParams(raw: ["timeoutMs": AnyHashable(Int($0 * 1000))]) }
do {
let start = Date()
let payload = try await AgentRPC.shared.controlRequest(method: "health", params: params)
var params: [String: AnyHashable]? = nil
if let timeout {
params = ["timeout": AnyHashable(Int(timeout * 1000))]
}
let payload = try await self.request(method: "health", params: params)
let ms = Date().timeIntervalSince(start) * 1000
self.lastPingMs = ms
self.state = .connected
@@ -119,14 +125,14 @@ final class ControlChannel: ObservableObject {
}
func lastHeartbeat() async throws -> ControlHeartbeatEvent? {
let data = try await AgentRPC.shared.controlRequest(method: "last-heartbeat")
if data.isEmpty { return nil }
return try? JSONDecoder().decode(ControlHeartbeatEvent.self, from: data)
// Heartbeat removed in new protocol
return nil
}
func request(method: String, params: ControlRequestParams? = nil) async throws -> Data {
func request(method: String, params: [String: AnyHashable]? = nil) async throws -> Data {
do {
let data = try await AgentRPC.shared.controlRequest(method: method, params: params)
let rawParams = params?.reduce(into: [String: Any]()) { $0[$1.key] = $1.value }
let data = try await gateway.request(method: method, params: rawParams)
self.state = .connected
return data
} catch {
@@ -136,9 +142,48 @@ final class ControlChannel: ObservableObject {
}
func sendSystemEvent(_ text: String) async throws {
_ = try await self.request(
method: "system-event",
params: ControlRequestParams(raw: ["text": AnyHashable(text)]))
_ = try await self.request(method: "system-event", params: ["text": AnyHashable(text)])
}
private func startEventStream() {
for tok in eventTokens { NotificationCenter.default.removeObserver(tok) }
eventTokens.removeAll()
let ev = NotificationCenter.default.addObserver(
forName: .gatewayEvent,
object: nil,
queue: .main
) { note in
guard let obj = note.userInfo as? [String: Any],
let event = obj["event"] as? String else { return }
switch event {
case "agent":
if let payload = obj["payload"] as? [String: Any],
let runId = payload["runId"] as? String,
let seq = payload["seq"] as? Int,
let stream = payload["stream"] as? String,
let ts = payload["ts"] as? Double,
let dataDict = payload["data"] as? [String: Any]
{
let wrapped = dataDict.mapValues { AnyCodable($0) }
AgentEventStore.shared.append(ControlAgentEvent(runId: runId, seq: seq, stream: stream, ts: ts, data: wrapped))
}
case "presence":
// InstancesStore listens separately via notification
break
case "shutdown":
self.state = .degraded("gateway shutdown")
default:
break
}
}
let tick = NotificationCenter.default.addObserver(
forName: .gatewaySnapshot,
object: nil,
queue: .main
) { _ in
self.state = .connected
}
eventTokens = [ev, tick]
}
}

View File

@@ -0,0 +1,221 @@
import Foundation
import OSLog
struct GatewayEvent: Codable {
let type: String
let event: String?
let payload: AnyCodable?
let seq: Int?
}
extension Notification.Name {
static let gatewaySnapshot = Notification.Name("clawdis.gateway.snapshot")
static let gatewayEvent = Notification.Name("clawdis.gateway.event")
static let gatewaySeqGap = Notification.Name("clawdis.gateway.seqgap")
}
private actor GatewayChannelActor {
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway")
private var task: URLSessionWebSocketTask?
private var pending: [String: CheckedContinuation<Data, Error>] = [:]
private var connected = false
private var url: URL
private var token: String?
private let session = URLSession(configuration: .default)
private var backoffMs: Double = 500
private var shouldReconnect = true
private var lastSeq: Int?
init(url: URL, token: String?) {
self.url = url
self.token = token
}
func connect() async throws {
if connected, task?.state == .running { return }
task?.cancel(with: .goingAway, reason: nil)
task = session.webSocketTask(with: url)
task?.resume()
try await sendHello()
listen()
connected = true
backoffMs = 500
lastSeq = nil
}
private func sendHello() async throws {
let hello: [String: Any] = [
"type": "hello",
"minProtocol": 1,
"maxProtocol": 1,
"client": [
"name": "clawdis-mac",
"version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev",
"platform": "macos",
"mode": "app",
"instanceId": Host.current().localizedName ?? UUID().uuidString,
],
"caps": [],
"auth": token != nil ? ["token": token!] : [:],
]
let data = try JSONSerialization.data(withJSONObject: hello)
try await task?.send(.data(data))
// wait for hello-ok
if let msg = try await task?.receive() {
if try await handleHelloResponse(msg) { return }
}
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "hello failed"])
}
private func handleHelloResponse(_ msg: URLSessionWebSocketTask.Message) async throws -> Bool {
let data: Data?
switch msg {
case .data(let d): data = d
case .string(let s): data = s.data(using: .utf8)
@unknown default: data = nil
}
guard let data else { return false }
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = obj["type"] as? String else { return false }
if type == "hello-ok" {
NotificationCenter.default.post(name: .gatewaySnapshot, object: nil, userInfo: obj)
return true
}
return false
}
private func listen() {
task?.receive { [weak self] result in
guard let self else { return }
switch result {
case .failure(let err):
self.logger.error("gateway ws receive failed \(err.localizedDescription, privacy: .public)")
self.connected = false
self.scheduleReconnect()
case .success(let msg):
Task { await self.handle(msg) }
self.listen()
}
}
}
private func handle(_ msg: URLSessionWebSocketTask.Message) async {
let data: Data?
switch msg {
case .data(let d): data = d
case .string(let s): data = s.data(using: .utf8)
@unknown default: data = nil
}
guard let data else { return }
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = obj["type"] as? String else { return }
switch type {
case "res":
if let id = obj["id"] as? String, let waiter = pending.removeValue(forKey: id) {
waiter.resume(returning: data)
}
case "event":
if let seq = obj["seq"] as? Int {
if let last = lastSeq, seq > last + 1 {
NotificationCenter.default.post(
name: .gatewaySeqGap,
object: nil,
userInfo: ["expected": last + 1, "received": seq]
)
}
lastSeq = seq
}
NotificationCenter.default.post(name: .gatewayEvent, object: nil, userInfo: obj)
case "hello-ok":
NotificationCenter.default.post(name: .gatewaySnapshot, object: nil, userInfo: obj)
default:
break
}
}
private func scheduleReconnect() {
guard shouldReconnect else { return }
let delay = backoffMs / 1000
backoffMs = min(backoffMs * 2, 30_000)
Task.detached { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard let self else { return }
do {
try await self.connect()
} catch {
self.logger.error("gateway reconnect failed \(error.localizedDescription, privacy: .public)")
self.scheduleReconnect()
}
}
}
func request(method: String, params: [String: Any]?) async throws -> Data {
try await connect()
let id = UUID().uuidString
let frame: [String: Any] = [
"type": "req",
"id": id,
"method": method,
"params": params ?? [:],
]
let data = try JSONSerialization.data(withJSONObject: frame)
let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Data, Error>) in
pending[id] = cont
Task {
do {
try await task?.send(.data(data))
} catch {
pending.removeValue(forKey: id)
cont.resume(throwing: error)
}
}
}
return response
}
}
actor GatewayChannel {
private var inner: GatewayChannelActor?
func configure(url: URL, token: String?) {
inner = GatewayChannelActor(url: url, token: token)
}
func request(method: String, params: [String: Any]?) async throws -> Data {
guard let inner else {
throw NSError(domain: "Gateway", code: 0, userInfo: [NSLocalizedDescriptionKey: "not configured"])
}
return try await inner.request(method: method, params: params)
}
}
struct AnyCodable: Codable {
let value: Any
init(_ value: Any) { self.value = value }
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 }
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
if container.decodeNil() { self.value = NSNull(); return }
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
if let array = try? container.decode([AnyCodable].self) { self.value = array; return }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let intVal as Int: try container.encode(intVal)
case let doubleVal as Double: try container.encode(doubleVal)
case let boolVal as Bool: try container.encode(boolVal)
case let stringVal as String: try container.encode(stringVal)
case is NSNull: try container.encodeNil()
case let dict as [String: AnyCodable]: try container.encode(dict)
case let array as [AnyCodable]: try container.encode(array)
default:
let ctx = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")
throw EncodingError.invalidValue(self.value, ctx)
}
}
}

View File

@@ -36,9 +36,11 @@ final class InstancesStore: ObservableObject {
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "instances")
private var task: Task<Void, Never>?
private let interval: TimeInterval = 30
private var observers: [NSObjectProtocol] = []
func start() {
guard self.task == nil else { return }
self.observeGatewayEvents()
self.task = Task.detached { [weak self] in
guard let self else { return }
await self.refresh()
@@ -52,6 +54,45 @@ final class InstancesStore: ObservableObject {
func stop() {
self.task?.cancel()
self.task = nil
for token in observers {
NotificationCenter.default.removeObserver(token)
}
observers.removeAll()
}
private func observeGatewayEvents() {
let ev = NotificationCenter.default.addObserver(
forName: .gatewayEvent,
object: nil,
queue: .main
) { [weak self] note in
guard let self,
let obj = note.userInfo as? [String: Any],
let event = obj["event"] as? String else { return }
if event == "presence", let payload = obj["payload"] as? [String: Any] {
self.handlePresencePayload(payload)
}
}
let gap = NotificationCenter.default.addObserver(
forName: .gatewaySeqGap,
object: nil,
queue: .main
) { [weak self] _ in
guard let self else { return }
Task { await self.refresh() }
}
let snap = NotificationCenter.default.addObserver(
forName: .gatewaySnapshot,
object: nil,
queue: .main
) { [weak self] note in
guard let self,
let obj = note.userInfo as? [String: Any],
let snapshot = obj["snapshot"] as? [String: Any],
let presence = snapshot["presence"] else { return }
self.decodeAndApplyPresence(presence: presence)
}
observers = [ev, snap, gap]
}
func refresh() async {
@@ -213,4 +254,35 @@ final class InstancesStore: ObservableObject {
}
}
}
private func handlePresencePayload(_ payload: [String: Any]) {
if let presence = payload["presence"] {
self.decodeAndApplyPresence(presence: presence)
}
}
private func decodeAndApplyPresence(presence: Any) {
guard let data = try? JSONSerialization.data(withJSONObject: presence) else { return }
do {
let decoded = try JSONDecoder().decode([InstanceInfo].self, from: data)
let withIDs = decoded.map { entry -> InstanceInfo in
let key = entry.host ?? entry.ip ?? entry.text
return InstanceInfo(
id: key,
host: entry.host,
ip: entry.ip,
version: entry.version,
lastInputSeconds: entry.lastInputSeconds,
mode: entry.mode,
reason: entry.reason,
text: entry.text,
ts: entry.ts)
}
self.instances = withIDs
self.statusMessage = nil
self.lastError = nil
} catch {
self.logger.error("presence decode from event failed: \(error.localizedDescription, privacy: .public)")
}
}
}

View File

@@ -89,12 +89,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate
RelayProcessManager.shared.setActive(!state.isPaused)
}
Task {
let controlMode: ControlChannel.Mode = AppStateStore.shared.connectionMode == .remote
? .remote(target: AppStateStore.shared.remoteTarget, identity: AppStateStore.shared.remoteIdentity)
: .local
try? await ControlChannel.shared.configure(mode: controlMode)
try? await AgentRPC.shared.start()
_ = await AgentRPC.shared.setHeartbeatsEnabled(AppStateStore.shared.heartbeatsEnabled)
try? await ControlChannel.shared.configure()
PresenceReporter.shared.start()
}
Task { await HealthStore.shared.refresh(onDemand: true) }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,895 @@
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let clawdisGateway = try ClawdisGateway(json)
import Foundation
/// Handshake, request/response, and event frames for the Gateway WebSocket.
// MARK: - ClawdisGateway
struct ClawdisGateway: Codable {
let auth: Auth?
let caps: [String]?
let client: Client?
let locale: String?
let maxProtocol, minProtocol: Int?
let type: TypeEnum
let userAgent: String?
let features: Features?
let policy: Policy?
let clawdisGatewayProtocol: Int?
let server: Server?
let snapshot: Snapshot?
let expectedProtocol: Int?
let minClient, reason, id, method: String?
let params: JSONAny?
let error: Error?
let ok: Bool?
let payload: JSONAny?
let event: String?
let seq: Int?
let stateVersion: ClawdisGatewayStateVersion?
enum CodingKeys: String, CodingKey {
case auth, caps, client, locale, maxProtocol, minProtocol, type, userAgent, features, policy
case clawdisGatewayProtocol = "protocol"
case server, snapshot, expectedProtocol, minClient, reason, id, method, params, error, ok, payload, event, seq, stateVersion
}
}
// MARK: ClawdisGateway convenience initializers and mutators
extension ClawdisGateway {
init(data: Data) throws {
self = try newJSONDecoder().decode(ClawdisGateway.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
auth: Auth?? = nil,
caps: [String]?? = nil,
client: Client?? = nil,
locale: String?? = nil,
maxProtocol: Int?? = nil,
minProtocol: Int?? = nil,
type: TypeEnum? = nil,
userAgent: String?? = nil,
features: Features?? = nil,
policy: Policy?? = nil,
clawdisGatewayProtocol: Int?? = nil,
server: Server?? = nil,
snapshot: Snapshot?? = nil,
expectedProtocol: Int?? = nil,
minClient: String?? = nil,
reason: String?? = nil,
id: String?? = nil,
method: String?? = nil,
params: JSONAny?? = nil,
error: Error?? = nil,
ok: Bool?? = nil,
payload: JSONAny?? = nil,
event: String?? = nil,
seq: Int?? = nil,
stateVersion: ClawdisGatewayStateVersion?? = nil
) -> ClawdisGateway {
return ClawdisGateway(
auth: auth ?? self.auth,
caps: caps ?? self.caps,
client: client ?? self.client,
locale: locale ?? self.locale,
maxProtocol: maxProtocol ?? self.maxProtocol,
minProtocol: minProtocol ?? self.minProtocol,
type: type ?? self.type,
userAgent: userAgent ?? self.userAgent,
features: features ?? self.features,
policy: policy ?? self.policy,
clawdisGatewayProtocol: clawdisGatewayProtocol ?? self.clawdisGatewayProtocol,
server: server ?? self.server,
snapshot: snapshot ?? self.snapshot,
expectedProtocol: expectedProtocol ?? self.expectedProtocol,
minClient: minClient ?? self.minClient,
reason: reason ?? self.reason,
id: id ?? self.id,
method: method ?? self.method,
params: params ?? self.params,
error: error ?? self.error,
ok: ok ?? self.ok,
payload: payload ?? self.payload,
event: event ?? self.event,
seq: seq ?? self.seq,
stateVersion: stateVersion ?? self.stateVersion
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Auth
struct Auth: Codable {
let token: String?
}
// MARK: Auth convenience initializers and mutators
extension Auth {
init(data: Data) throws {
self = try newJSONDecoder().decode(Auth.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
token: String?? = nil
) -> Auth {
return Auth(
token: token ?? self.token
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Client
struct Client: Codable {
let instanceID: String?
let mode, name, platform, version: String
enum CodingKeys: String, CodingKey {
case instanceID = "instanceId"
case mode, name, platform, version
}
}
// MARK: Client convenience initializers and mutators
extension Client {
init(data: Data) throws {
self = try newJSONDecoder().decode(Client.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
instanceID: String?? = nil,
mode: String? = nil,
name: String? = nil,
platform: String? = nil,
version: String? = nil
) -> Client {
return Client(
instanceID: instanceID ?? self.instanceID,
mode: mode ?? self.mode,
name: name ?? self.name,
platform: platform ?? self.platform,
version: version ?? self.version
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Error
struct Error: Codable {
let code: String
let details: JSONAny?
let message: String
let retryable: Bool?
let retryAfterMS: Int?
enum CodingKeys: String, CodingKey {
case code, details, message, retryable
case retryAfterMS = "retryAfterMs"
}
}
// MARK: Error convenience initializers and mutators
extension Error {
init(data: Data) throws {
self = try newJSONDecoder().decode(Error.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
code: String? = nil,
details: JSONAny?? = nil,
message: String? = nil,
retryable: Bool?? = nil,
retryAfterMS: Int?? = nil
) -> Error {
return Error(
code: code ?? self.code,
details: details ?? self.details,
message: message ?? self.message,
retryable: retryable ?? self.retryable,
retryAfterMS: retryAfterMS ?? self.retryAfterMS
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Features
struct Features: Codable {
let events, methods: [String]
}
// MARK: Features convenience initializers and mutators
extension Features {
init(data: Data) throws {
self = try newJSONDecoder().decode(Features.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
events: [String]? = nil,
methods: [String]? = nil
) -> Features {
return Features(
events: events ?? self.events,
methods: methods ?? self.methods
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Policy
struct Policy: Codable {
let maxBufferedBytes, maxPayload, tickIntervalMS: Int
enum CodingKeys: String, CodingKey {
case maxBufferedBytes, maxPayload
case tickIntervalMS = "tickIntervalMs"
}
}
// MARK: Policy convenience initializers and mutators
extension Policy {
init(data: Data) throws {
self = try newJSONDecoder().decode(Policy.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
maxBufferedBytes: Int? = nil,
maxPayload: Int? = nil,
tickIntervalMS: Int? = nil
) -> Policy {
return Policy(
maxBufferedBytes: maxBufferedBytes ?? self.maxBufferedBytes,
maxPayload: maxPayload ?? self.maxPayload,
tickIntervalMS: tickIntervalMS ?? self.tickIntervalMS
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Server
struct Server: Codable {
let commit: String?
let connID: String
let host: String?
let version: String
enum CodingKeys: String, CodingKey {
case commit
case connID = "connId"
case host, version
}
}
// MARK: Server convenience initializers and mutators
extension Server {
init(data: Data) throws {
self = try newJSONDecoder().decode(Server.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
commit: String?? = nil,
connID: String? = nil,
host: String?? = nil,
version: String? = nil
) -> Server {
return Server(
commit: commit ?? self.commit,
connID: connID ?? self.connID,
host: host ?? self.host,
version: version ?? self.version
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Snapshot
struct Snapshot: Codable {
let health: JSONAny
let presence: [Presence]
let stateVersion: SnapshotStateVersion
let uptimeMS: Int
enum CodingKeys: String, CodingKey {
case health, presence, stateVersion
case uptimeMS = "uptimeMs"
}
}
// MARK: Snapshot convenience initializers and mutators
extension Snapshot {
init(data: Data) throws {
self = try newJSONDecoder().decode(Snapshot.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
health: JSONAny? = nil,
presence: [Presence]? = nil,
stateVersion: SnapshotStateVersion? = nil,
uptimeMS: Int? = nil
) -> Snapshot {
return Snapshot(
health: health ?? self.health,
presence: presence ?? self.presence,
stateVersion: stateVersion ?? self.stateVersion,
uptimeMS: uptimeMS ?? self.uptimeMS
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Presence
struct Presence: Codable {
let host, instanceID, ip: String?
let lastInputSeconds: Int?
let mode, reason: String?
let tags: [String]?
let text: String?
let ts: Int
let version: String?
enum CodingKeys: String, CodingKey {
case host
case instanceID = "instanceId"
case ip, lastInputSeconds, mode, reason, tags, text, ts, version
}
}
// MARK: Presence convenience initializers and mutators
extension Presence {
init(data: Data) throws {
self = try newJSONDecoder().decode(Presence.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
host: String?? = nil,
instanceID: String?? = nil,
ip: String?? = nil,
lastInputSeconds: Int?? = nil,
mode: String?? = nil,
reason: String?? = nil,
tags: [String]?? = nil,
text: String?? = nil,
ts: Int? = nil,
version: String?? = nil
) -> Presence {
return Presence(
host: host ?? self.host,
instanceID: instanceID ?? self.instanceID,
ip: ip ?? self.ip,
lastInputSeconds: lastInputSeconds ?? self.lastInputSeconds,
mode: mode ?? self.mode,
reason: reason ?? self.reason,
tags: tags ?? self.tags,
text: text ?? self.text,
ts: ts ?? self.ts,
version: version ?? self.version
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - SnapshotStateVersion
struct SnapshotStateVersion: Codable {
let health, presence: Int
}
// MARK: SnapshotStateVersion convenience initializers and mutators
extension SnapshotStateVersion {
init(data: Data) throws {
self = try newJSONDecoder().decode(SnapshotStateVersion.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
health: Int? = nil,
presence: Int? = nil
) -> SnapshotStateVersion {
return SnapshotStateVersion(
health: health ?? self.health,
presence: presence ?? self.presence
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - ClawdisGatewayStateVersion
struct ClawdisGatewayStateVersion: Codable {
let health, presence: Int
}
// MARK: ClawdisGatewayStateVersion convenience initializers and mutators
extension ClawdisGatewayStateVersion {
init(data: Data) throws {
self = try newJSONDecoder().decode(ClawdisGatewayStateVersion.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func with(
health: Int? = nil,
presence: Int? = nil
) -> ClawdisGatewayStateVersion {
return ClawdisGatewayStateVersion(
health: health ?? self.health,
presence: presence ?? self.presence
)
}
func jsonData() throws -> Data {
return try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
enum TypeEnum: String, Codable {
case event = "event"
case hello = "hello"
case helloError = "hello-error"
case helloOk = "hello-ok"
case req = "req"
case res = "res"
}
// MARK: - Helper functions for creating encoders and decoders
func newJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
decoder.dateDecodingStrategy = .iso8601
}
return decoder
}
func newJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
encoder.dateEncodingStrategy = .iso8601
}
return encoder
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
class JSONCodingKey: CodingKey {
let key: String
required init?(intValue: Int) {
return nil
}
required init?(stringValue: String) {
key = stringValue
}
var intValue: Int? {
return nil
}
var stringValue: String {
return key
}
}
class JSONAny: Codable {
let value: Any
static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
return DecodingError.typeMismatch(JSONAny.self, context)
}
static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
return EncodingError.invalidValue(value, context)
}
static func decode(from container: SingleValueDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if container.decodeNil() {
return JSONNull()
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if let value = try? container.decodeNil() {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer() {
return try decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) {
return try decodeDictionary(from: &container)
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decode(from container: inout KeyedDecodingContainer<JSONCodingKey>, forKey key: JSONCodingKey) throws -> Any {
if let value = try? container.decode(Bool.self, forKey: key) {
return value
}
if let value = try? container.decode(Int64.self, forKey: key) {
return value
}
if let value = try? container.decode(Double.self, forKey: key) {
return value
}
if let value = try? container.decode(String.self, forKey: key) {
return value
}
if let value = try? container.decodeNil(forKey: key) {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer(forKey: key) {
return try decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) {
return try decodeDictionary(from: &container)
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
var arr: [Any] = []
while !container.isAtEnd {
let value = try decode(from: &container)
arr.append(value)
}
return arr
}
static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] {
var dict = [String: Any]()
for key in container.allKeys {
let value = try decode(from: &container, forKey: key)
dict[key.stringValue] = value
}
return dict
}
static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
for value in array {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer()
try encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self)
try encode(to: &container, dictionary: value)
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws {
for (key, value) in dictionary {
let key = JSONCodingKey(stringValue: key)!
if let value = value as? Bool {
try container.encode(value, forKey: key)
} else if let value = value as? Int64 {
try container.encode(value, forKey: key)
} else if let value = value as? Double {
try container.encode(value, forKey: key)
} else if let value = value as? String {
try container.encode(value, forKey: key)
} else if value is JSONNull {
try container.encodeNil(forKey: key)
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer(forKey: key)
try encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key)
try encode(to: &container, dictionary: value)
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
public required init(from decoder: Decoder) throws {
if var arrayContainer = try? decoder.unkeyedContainer() {
self.value = try JSONAny.decodeArray(from: &arrayContainer)
} else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) {
self.value = try JSONAny.decodeDictionary(from: &container)
} else {
let container = try decoder.singleValueContainer()
self.value = try JSONAny.decode(from: container)
}
}
public func encode(to encoder: Encoder) throws {
if let arr = self.value as? [Any] {
var container = encoder.unkeyedContainer()
try JSONAny.encode(to: &container, array: arr)
} else if let dict = self.value as? [String: Any] {
var container = encoder.container(keyedBy: JSONCodingKey.self)
try JSONAny.encode(to: &container, dictionary: dict)
} else {
var container = encoder.singleValueContainer()
try JSONAny.encode(to: &container, value: self.value)
}
}
}