From be722aa170384d4ed00db5a0e64a11280283ab9a Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Sep 2025 21:20:49 +0200 Subject: [PATCH] Fix critical routing and buffer issues\n\n- Preserve unsent items in MessageRouter outbox (no data loss)\n- Drain and bound Nostr send queue; flush on relay connect\n- Cap BLE pending write buffers per peripheral to avoid OOM\n- Enable Nostr fallback for short peer IDs via favorites mapping\n- Route favorite notifications via MessageRouter (mesh/Nostr) --- bitchat/Nostr/NostrRelayManager.swift | 66 ++++++++++++++++++++--- bitchat/Services/BLEService.swift | 25 ++++++++- bitchat/Services/MessageRouter.swift | 31 ++++++++--- bitchat/Services/UnifiedPeerService.swift | 10 +++- bitchat/ViewModels/ChatViewModel.swift | 2 + 5 files changed, 115 insertions(+), 19 deletions(-) diff --git a/bitchat/Nostr/NostrRelayManager.swift b/bitchat/Nostr/NostrRelayManager.swift index 3c894d4e..2f5f66d6 100644 --- a/bitchat/Nostr/NostrRelayManager.swift +++ b/bitchat/Nostr/NostrRelayManager.swift @@ -44,7 +44,12 @@ class NostrRelayManager: ObservableObject { private var cancellables = Set() // Message queue for reliability - private var messageQueue: [(event: NostrEvent, relayUrls: [String])] = [] + // Pending sends held only for relays that are not yet connected. + private struct PendingSend { + var event: NostrEvent + var pendingRelays: Set + } + private var messageQueue: [PendingSend] = [] private let messageQueueLock = NSLock() // Exponential backoff configuration @@ -95,16 +100,57 @@ class NostrRelayManager: ObservableObject { func sendEvent(_ event: NostrEvent, to relayUrls: [String]? = nil) { let targetRelays = relayUrls ?? Self.defaultRelays ensureConnections(to: targetRelays) - - // Add to queue for reliability - messageQueueLock.lock() - messageQueue.append((event, targetRelays)) - messageQueueLock.unlock() - - // Attempt immediate send + + // Attempt immediate send to relays with active connections; queue the rest + var stillPending = Set() for relayUrl in targetRelays { if let connection = connections[relayUrl] { sendToRelay(event: event, connection: connection, relayUrl: relayUrl) + } else { + stillPending.insert(relayUrl) + } + } + if !stillPending.isEmpty { + messageQueueLock.lock() + messageQueue.append(PendingSend(event: event, pendingRelays: stillPending)) + messageQueueLock.unlock() + } + } + + /// Try to flush any queued messages for relays that are now connected. + private func flushMessageQueue(for relayUrl: String? = nil) { + messageQueueLock.lock() + defer { messageQueueLock.unlock() } + guard !messageQueue.isEmpty else { return } + if let target = relayUrl { + // Flush only for a specific relay + for i in (0.. capBytes { + SecureLogger.log("⚠️ Dropping oversized write chunk (\(newSize)B) for peripheral \(uuid)", + category: SecureLogger.session, level: .warning) + } else { + // Append and trim from the front to respect cap + var total = queue.reduce(0) { $0 + $1.count } + queue.append(data) + total += newSize + if total > capBytes { + var removedBytes = 0 + while total > capBytes && !queue.isEmpty { + let removed = queue.removeFirst() + removedBytes += removed.count + total -= removed.count + } + SecureLogger.log("📉 Trimmed pending write buffer for \(uuid) by \(removedBytes)B to \(total)B", + category: SecureLogger.session, level: .warning) + } + self.pendingPeripheralWrites[uuid] = queue.isEmpty ? nil : queue + } } } } diff --git a/bitchat/Services/MessageRouter.swift b/bitchat/Services/MessageRouter.swift index 5937bdec..cc0472e5 100644 --- a/bitchat/Services/MessageRouter.swift +++ b/bitchat/Services/MessageRouter.swift @@ -89,10 +89,19 @@ final class MessageRouter { // MARK: - Outbox Management private func canSendViaNostr(peerID: String) -> Bool { - guard let noiseKey = Data(hexString: peerID) else { return false } - if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), - fav.peerNostrPublicKey != nil { - return true + // Two forms are supported: + // - 64-hex Noise public key (32 bytes) + // - 16-hex short peer ID (derived from Noise pubkey) + if peerID.count == 64, let noiseKey = Data(hexString: peerID) { + if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey), + fav.peerNostrPublicKey != nil { + return true + } + } else if peerID.count == 16 { + if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID), + fav.peerNostrPublicKey != nil { + return true + } } return false } @@ -101,6 +110,7 @@ final class MessageRouter { guard let queued = outbox[peerID], !queued.isEmpty else { return } SecureLogger.log("Flushing outbox for \(peerID.prefix(8))… count=\(queued.count)", category: SecureLogger.session, level: .debug) + var remaining: [(content: String, nickname: String, messageID: String)] = [] // Prefer mesh if connected; else try Nostr if mapping exists for (content, nickname, messageID) in queued { if mesh.isPeerReachable(peerID) { @@ -112,14 +122,19 @@ final class MessageRouter { category: SecureLogger.session, level: .debug) nostr.sendPrivateMessage(content, to: peerID, recipientNickname: nickname, messageID: messageID) } else { - continue + // Keep unsent items queued + remaining.append((content, nickname, messageID)) } } - // Remove all flushed items (remaining ones, if any, will be re-queued on next call) - outbox[peerID]?.removeAll() + // Persist only items we could not send + if remaining.isEmpty { + outbox.removeValue(forKey: peerID) + } else { + outbox[peerID] = remaining + } } func flushAllOutbox() { - for key in outbox.keys { flushOutbox(for: key) } + for key in Array(outbox.keys) { flushOutbox(for: key) } } } diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index 9675a4aa..8bd3d7dd 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -27,6 +27,7 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { private var peerIndex: [String: BitchatPeer] = [:] private var fingerprintCache: [String: String] = [:] // peerID -> fingerprint private let meshService: Transport + weak var messageRouter: MessageRouter? private let favoritesService = FavoritesPersistenceService.shared private var cancellables = Set() @@ -330,8 +331,13 @@ class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate { SecureLogger.log("⭐️ Toggled favorite for '\(finalNickname)' (peerID: \(peerID), was: \(wasFavorite), now: \(!wasFavorite))", category: SecureLogger.session, level: .debug) - // Send favorite notification to the peer - meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite) + // Send favorite notification to the peer via router (mesh or Nostr) + if let router = messageRouter { + router.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite) + } else { + // Fallback to mesh-only if router not yet wired + meshService.sendFavoriteNotification(to: peerID, isFavorite: !wasFavorite) + } // Force update of peers to reflect the change updatePeers() diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 8a1d121d..1363739f 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -492,6 +492,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate { self.messageRouter = MessageRouter(mesh: meshService, nostr: nostrTransport) // Route receipts from PrivateChatManager through MessageRouter self.privateChatManager.messageRouter = self.messageRouter + // Allow UnifiedPeerService to route favorite notifications via mesh/Nostr + self.unifiedPeerService.messageRouter = self.messageRouter self.autocompleteService = AutocompleteService() // Wire up dependencies