Nostr: parse inbound messages off-main, then update state on main; BLE: debounce peer snapshot publishing to reduce churn

This commit is contained in:
jack
2025-09-04 23:52:05 +02:00
parent d9884eaf87
commit 4e51c83914
2 changed files with 92 additions and 60 deletions
+65 -55
View File
@@ -290,14 +290,21 @@ class NostrRelayManager: ObservableObject {
case .success(let message): case .success(let message):
switch message { switch message {
case .string(let text): case .string(let text):
Task { @MainActor in // Parse off-main to reduce UI jank, then hop back for state updates
self.handleMessage(text, from: relayUrl) Task.detached { [weak self] in
guard let parsed = Self.parseInboundMessage(text) else { return }
await MainActor.run {
self?.handleParsedMessage(parsed, from: relayUrl)
}
} }
case .data(let data): case .data(let data):
if let text = String(data: data, encoding: .utf8) { if let text = String(data: data, encoding: .utf8) {
Task { @MainActor in Task.detached { [weak self] in
self.handleMessage(text, from: relayUrl) guard let parsed = Self.parseInboundMessage(text) else { return }
} await MainActor.run {
self?.handleParsedMessage(parsed, from: relayUrl)
}
}
} }
@unknown default: @unknown default:
break break
@@ -316,79 +323,82 @@ class NostrRelayManager: ObservableObject {
} }
} }
private func handleMessage(_ message: String, from relayUrl: String) { // Parsed inbound message type (off-main)
guard let data = message.data(using: .utf8) else { return } private enum ParsedInbound {
case event(subId: String, event: NostrEvent)
case ok(eventId: String, success: Bool, reason: String)
case eose(subscriptionId: String)
case notice(String)
}
// Off-main JSON parse to avoid UI jank
private static func parseInboundMessage(_ message: String) -> ParsedInbound? {
guard let data = message.data(using: .utf8) else { return nil }
do { do {
// Try to decode as an array first
if let array = try JSONSerialization.jsonObject(with: data) as? [Any], if let array = try JSONSerialization.jsonObject(with: data) as? [Any],
array.count >= 2, array.count >= 2,
let type = array[0] as? String { let type = array[0] as? String {
// Received message from relay
switch type { switch type {
case "EVENT": case "EVENT":
if array.count >= 3, if array.count >= 3,
let subId = array[1] as? String, let subId = array[1] as? String,
let eventDict = array[2] as? [String: Any] { let eventDict = array[2] as? [String: Any] {
let event = try NostrEvent(from: eventDict) let event = try NostrEvent(from: eventDict)
return .event(subId: subId, event: event)
// Only log non-gift-wrap events to reduce noise
if event.kind != 1059 {
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
}
DispatchQueue.main.async {
// Update relay stats
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
self.relays[index].messagesReceived += 1
}
// Call handler
if let handler = self.messageHandlers[subId] {
handler(event)
} else {
SecureLogger.log("⚠️ No handler for subscription \(subId)",
category: SecureLogger.session, level: .warning)
}
}
} }
case "EOSE": case "EOSE":
if array.count >= 2 { if let subId = array[1] as? String { return .eose(subscriptionId: subId) }
// End of stored events
}
case "OK": case "OK":
if array.count >= 3, if array.count >= 3,
let eventId = array[1] as? String, let eventId = array[1] as? String,
let success = array[2] as? Bool { let success = array[2] as? Bool {
let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given" let reason = array.count >= 4 ? (array[3] as? String ?? "no reason given") : "no reason given"
if success { return .ok(eventId: eventId, success: success, reason: reason)
_ = Self.pendingGiftWrapIDs.remove(eventId)
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
} else {
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
}
} }
case "NOTICE": case "NOTICE":
if array.count >= 2 { if array.count >= 2, let msg = array[1] as? String { return .notice(msg) }
// Server notice received
}
default: default:
break // Unknown message type return nil
} }
} }
} catch { } catch {
SecureLogger.log("Failed to parse Nostr message: \(error)", category: SecureLogger.session, level: .error) // Fall through to nil; main will ignore
}
return nil
}
// Handle parsed message on MainActor (state updates and handlers)
private func handleParsedMessage(_ parsed: ParsedInbound, from relayUrl: String) {
switch parsed {
case .event(let subId, let event):
if event.kind != 1059 {
SecureLogger.log("📥 Event kind=\(event.kind) id=\(event.id.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
}
if let index = self.relays.firstIndex(where: { $0.url == relayUrl }) {
self.relays[index].messagesReceived += 1
}
if let handler = self.messageHandlers[subId] {
handler(event)
} else {
SecureLogger.log("⚠️ No handler for subscription \(subId)",
category: SecureLogger.session, level: .warning)
}
case .eose:
// No-op for now
break
case .ok(let eventId, let success, let reason):
if success {
_ = Self.pendingGiftWrapIDs.remove(eventId)
SecureLogger.log("✅ Accepted id=\(eventId.prefix(16))… relay=\(relayUrl)",
category: SecureLogger.session, level: .debug)
} else {
let isGiftWrap = Self.pendingGiftWrapIDs.remove(eventId) != nil
SecureLogger.log("📮 Rejected id=\(eventId.prefix(16))… reason=\(reason)",
category: SecureLogger.session, level: isGiftWrap ? .warning : .error)
}
case .notice:
break
} }
} }
+27 -5
View File
@@ -400,7 +400,7 @@ final class BLEService: NSObject {
maintenanceTimer = timer maintenanceTimer = timer
// Publish initial empty state // Publish initial empty state
publishFullPeerData() requestPeerDataPublish()
} }
func setNickname(_ nickname: String) { func setNickname(_ nickname: String) {
@@ -1595,7 +1595,7 @@ final class BLEService: NSObject {
self.delegate?.didConnectToPeer(peerID) self.delegate?.didConnectToPeer(peerID)
} }
self.publishFullPeerData() self.requestPeerDataPublish()
self.delegate?.didUpdatePeerList(currentPeerIDs) self.delegate?.didUpdatePeerList(currentPeerIDs)
} }
@@ -1989,6 +1989,28 @@ final class BLEService: NSObject {
self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers) self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers)
} }
} }
// Debounced publish to coalesce rapid changes
private var lastPeerPublishAt: Date = .distantPast
private var peerPublishPending: Bool = false
private let peerPublishMinInterval: TimeInterval = 0.1
private func requestPeerDataPublish() {
let now = Date()
let elapsed = now.timeIntervalSince(lastPeerPublishAt)
if elapsed >= peerPublishMinInterval {
lastPeerPublishAt = now
publishFullPeerData()
} else if !peerPublishPending {
peerPublishPending = true
let delay = peerPublishMinInterval - elapsed
messageQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self else { return }
self.lastPeerPublishAt = Date()
self.peerPublishPending = false
self.publishFullPeerData()
}
}
}
// MARK: - Consolidated Maintenance // MARK: - Consolidated Maintenance
@@ -2102,7 +2124,7 @@ final class BLEService: NSObject {
self.delegate?.didDisconnectFromPeer(peerID) self.delegate?.didDisconnectFromPeer(peerID)
} }
// Publish snapshots so UnifiedPeerService updates connection/reachability icons // Publish snapshots so UnifiedPeerService updates connection/reachability icons
self.publishFullPeerData() self.requestPeerDataPublish()
self.delegate?.didUpdatePeerList(currentPeerIDs) self.delegate?.didUpdatePeerList(currentPeerIDs)
} }
} }
@@ -2484,7 +2506,7 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
if let peerID = peerID { if let peerID = peerID {
self.notifyPeerDisconnectedDebounced(peerID) self.notifyPeerDisconnectedDebounced(peerID)
} }
self.publishFullPeerData() self.requestPeerDataPublish()
self.delegate?.didUpdatePeerList(currentPeerIDs) self.delegate?.didUpdatePeerList(currentPeerIDs)
} }
} }
@@ -2885,7 +2907,7 @@ extension BLEService: CBPeripheralManagerDelegate {
self.notifyPeerDisconnectedDebounced(peerID) self.notifyPeerDisconnectedDebounced(peerID)
// Publish snapshots so UnifiedPeerService can refresh icons promptly // Publish snapshots so UnifiedPeerService can refresh icons promptly
self.publishFullPeerData() self.requestPeerDataPublish()
self.delegate?.didUpdatePeerList(currentPeerIDs) self.delegate?.didUpdatePeerList(currentPeerIDs)
} }
} }