mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 16:05:19 +00:00
Nostr: parse inbound messages off-main, then update state on main; BLE: debounce peer snapshot publishing to reduce churn
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1990,6 +1990,28 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
private func performMaintenance() {
|
private func performMaintenance() {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user