fix(macos): harden presence decode
This commit is contained in:
@@ -157,7 +157,6 @@ final class ControlChannel: ObservableObject {
|
||||
return "Gateway request timed out; check the gateway process on localhost:\(GatewayEnvironment.gatewayPort())."
|
||||
}
|
||||
|
||||
let nsError = error as NSError
|
||||
let detail = nsError.localizedDescription.isEmpty ? "unknown gateway error" : nsError.localizedDescription
|
||||
return "Gateway error: \(detail)"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ final class InstancesStore: ObservableObject {
|
||||
private let interval: TimeInterval = 30
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
|
||||
private struct PresenceEventPayload: Codable {
|
||||
let presence: [PresenceEntry]
|
||||
}
|
||||
|
||||
init(isPreview: Bool = false) {
|
||||
self.isPreview = isPreview
|
||||
}
|
||||
@@ -77,11 +81,8 @@ final class InstancesStore: ObservableObject {
|
||||
let frame = note.object as? GatewayFrame else { return }
|
||||
switch frame {
|
||||
case let .event(evt) where evt.event == "presence":
|
||||
if let payload = evt.payload?.value as? [String: Any],
|
||||
let presence = payload["presence"],
|
||||
let presenceData = try? JSONSerialization.data(withJSONObject: presence)
|
||||
{
|
||||
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(presenceData) }
|
||||
if let payload = evt.payload {
|
||||
Task { @MainActor [weak self] in self?.handlePresenceEventPayload(payload) }
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -133,20 +134,8 @@ final class InstancesStore: ObservableObject {
|
||||
await self.probeHealthIfNeeded(reason: "no payload")
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
let decoded = try JSONDecoder().decode([PresenceEntry].self, from: data)
|
||||
let withIDs = self.normalizePresence(decoded)
|
||||
if withIDs.isEmpty {
|
||||
self.instances = [self.localFallbackInstance(reason: "no presence entries")]
|
||||
self.lastError = nil
|
||||
@@ -280,28 +269,47 @@ final class InstancesStore: ObservableObject {
|
||||
|
||||
private func decodeAndApplyPresenceData(_ data: Data) {
|
||||
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
|
||||
let decoded = try JSONDecoder().decode([PresenceEntry].self, from: data)
|
||||
self.applyPresence(decoded)
|
||||
} catch {
|
||||
self.logger.error("presence decode from event failed: \(error.localizedDescription, privacy: .public)")
|
||||
self.lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func handlePresenceEventPayload(_ payload: ClawdisProtocol.AnyCodable) {
|
||||
do {
|
||||
let payloadData = try JSONEncoder().encode(payload)
|
||||
let wrapper = try JSONDecoder().decode(PresenceEventPayload.self, from: payloadData)
|
||||
self.applyPresence(wrapper.presence)
|
||||
} catch {
|
||||
self.logger.error("presence event decode failed: \(error.localizedDescription, privacy: .public)")
|
||||
self.lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizePresence(_ entries: [PresenceEntry]) -> [InstanceInfo] {
|
||||
entries.map { entry -> InstanceInfo in
|
||||
let key = entry.host ?? entry.ip ?? entry.text ?? entry.instanceid ?? "entry-\(entry.ts)"
|
||||
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 ?? "Unnamed node",
|
||||
ts: Double(entry.ts))
|
||||
}
|
||||
}
|
||||
|
||||
private func applyPresence(_ entries: [PresenceEntry]) {
|
||||
let withIDs = self.normalizePresence(entries)
|
||||
self.instances = withIDs
|
||||
self.statusMessage = nil
|
||||
self.lastError = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension InstancesStore {
|
||||
|
||||
36
apps/macos/Tests/ClawdisIPCTests/InstancesStoreTests.swift
Normal file
36
apps/macos/Tests/ClawdisIPCTests/InstancesStoreTests.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
import ClawdisProtocol
|
||||
|
||||
@Suite struct InstancesStoreTests {
|
||||
@Test
|
||||
@MainActor
|
||||
func presenceEventPayloadDecodesViaJSONEncoder() {
|
||||
// Build a payload that mirrors the gateway's presence event shape:
|
||||
// { "presence": [ PresenceEntry ] }
|
||||
let entry: [String: ClawdisProtocol.AnyCodable] = [
|
||||
"host": .init("gw"),
|
||||
"ip": .init("10.0.0.1"),
|
||||
"version": .init("2.0.0"),
|
||||
"mode": .init("gateway"),
|
||||
"lastInputSeconds": .init(5),
|
||||
"reason": .init("test"),
|
||||
"text": .init("Gateway node"),
|
||||
"ts": .init(1_730_000_000),
|
||||
]
|
||||
let payloadMap: [String: ClawdisProtocol.AnyCodable] = [
|
||||
"presence": .init([ClawdisProtocol.AnyCodable(entry)]),
|
||||
]
|
||||
let payload = ClawdisProtocol.AnyCodable(payloadMap)
|
||||
|
||||
let store = InstancesStore(isPreview: true)
|
||||
store.handlePresenceEventPayload(payload)
|
||||
|
||||
#expect(store.instances.count == 1)
|
||||
let instance = store.instances.first
|
||||
#expect(instance?.host == "gw")
|
||||
#expect(instance?.ip == "10.0.0.1")
|
||||
#expect(instance?.mode == "gateway")
|
||||
#expect(instance?.reason == "test")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user