diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 2cf5db8f..6bec699f 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -104,6 +104,10 @@ final class BLEService: NSObject { private var pendingMessagesAfterHandshake: [String: [(content: String, messageID: String)]] = [:] // Noise typed payloads (ACKs, read receipts, etc.) pending handshake private var pendingNoisePayloadsAfterHandshake: [String: [Data]] = [:] + // Keep a tiny buffer of the last few unique announces we've seen (by sender) + private var recentAnnounceBySender: [String: BitchatPacket] = [:] + private var recentAnnounceOrder: [String] = [] + private let recentAnnounceBufferCap = 3 // Queue for notifications that failed due to full queue private var pendingNotifications: [(data: Data, centrals: [CBCentral]?)] = [] @@ -1133,6 +1137,21 @@ final class BLEService: NSObject { messageQueue.async { [weak self] in self?.broadcastPacket(packet) } } } + + private func rebroadcastRecentAnnounces() { + // Snapshot sender order to preserve ordering and avoid holding locks while sending + let packets: [BitchatPacket] = collectionsQueue.sync { + recentAnnounceOrder.compactMap { recentAnnounceBySender[$0] } + } + guard !packets.isEmpty else { return } + for (idx, pkt) in packets.enumerated() { + // Stagger slightly to avoid bursts + let delayMs = idx * 20 + messageQueue.asyncAfter(deadline: .now() + .milliseconds(delayMs)) { [weak self] in + self?.broadcastPacket(pkt) + } + } + } private func sendData(_ data: Data, to peripheral: CBPeripheral) { // Fire-and-forget: Simple send without complex fallback logic @@ -1198,7 +1217,8 @@ final class BLEService: NSObject { ttl: packet.ttl ) // Pace fragments with small jitter to avoid bursts - let delayMs = index * TransportConfig.bleFragmentSpacingMs // ~6ms spacing per fragment + let perFragMs = (directedOnlyPeer != nil || packet.recipientID != nil) ? TransportConfig.bleFragmentSpacingDirectedMs : TransportConfig.bleFragmentSpacingMs + let delayMs = index * perFragMs messageQueue.asyncAfter(deadline: .now() + .milliseconds(delayMs)) { [weak self] in self?.broadcastPacket(fragmentPacket) } @@ -1359,6 +1379,7 @@ final class BLEService: NSObject { isDirectedEncrypted: (packet.type == MessageType.noiseEncrypted.rawValue) && (packet.recipientID != nil), isDirectedFragment: packet.type == MessageType.fragment.rawValue && packet.recipientID != nil, isHandshake: packet.type == MessageType.noiseHandshake.rawValue, + isAnnounce: packet.type == MessageType.announce.rawValue, degree: degree, highDegreeThreshold: highDegreeThreshold ) @@ -1498,6 +1519,20 @@ final class BLEService: NSObject { ) } + // Record this announce for lightweight rebroadcast buffer (exclude self) + if peerID != myPeerID { + collectionsQueue.async(flags: .barrier) { [weak self] in + guard let self = self else { return } + self.recentAnnounceBySender[peerID] = packet + if !self.recentAnnounceOrder.contains(peerID) { self.recentAnnounceOrder.append(peerID) } + // Trim to cap, oldest first + while self.recentAnnounceOrder.count > self.recentAnnounceBufferCap { + let victim = self.recentAnnounceOrder.removeFirst() + self.recentAnnounceBySender.removeValue(forKey: victim) + } + } + } + // Notify UI on main thread notifyUI { [weak self] in guard let self = self else { return } @@ -2572,6 +2607,8 @@ extension BLEService: CBPeripheralDelegate { self?.sendAnnounce(forceSend: true) // Try flushing any spooled directed packets now that we have a link self?.flushDirectedSpool() + // Rebroadcast a couple of recent announces to seed the new link + self?.rebroadcastRecentAnnounces() } } else { SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning) @@ -2736,6 +2773,8 @@ extension BLEService: CBPeripheralManagerDelegate { self?.sendAnnounce(forceSend: true) // Flush any spooled directed packets now that we have a central subscribed self?.flushDirectedSpool() + // Rebroadcast a couple of recent announces to seed the new link + self?.rebroadcastRecentAnnounces() } } diff --git a/bitchat/Services/RelayController.swift b/bitchat/Services/RelayController.swift index aec4adb0..45291c66 100644 --- a/bitchat/Services/RelayController.swift +++ b/bitchat/Services/RelayController.swift @@ -15,6 +15,7 @@ struct RelayController { isDirectedEncrypted: Bool, isDirectedFragment: Bool, isHandshake: Bool, + isAnnounce: Bool, degree: Int, highDegreeThreshold: Int) -> RelayDecision { // Suppress obvious non-relays @@ -46,7 +47,11 @@ struct RelayController { // TTL clamping for broadcast // - Dense graphs: keep very low to avoid floods // - Sparse graphs: allow slightly longer reach for multi-hop discovery - let ttlCap: UInt8 = degree >= highDegreeThreshold ? 3 : 6 + // - Announces in sparse graphs get a bit more headroom + let ttlCap: UInt8 = { + if degree >= highDegreeThreshold { return 3 } + return isAnnounce ? 7 : 6 + }() let clamped = max(1, min(ttl, ttlCap)) let newTTL = clamped &- 1 diff --git a/bitchat/Services/TransportConfig.swift b/bitchat/Services/TransportConfig.swift index 3d006e92..ee53806a 100644 --- a/bitchat/Services/TransportConfig.swift +++ b/bitchat/Services/TransportConfig.swift @@ -85,7 +85,9 @@ enum TransportConfig { static let bleThreadSleepWriteShortDelaySeconds: TimeInterval = 0.05 static let bleExpectedWritePerFragmentMs: Int = 8 static let bleExpectedWriteMaxMs: Int = 2000 - static let bleFragmentSpacingMs: Int = 6 + // Faster fragment pacing; use slightly tighter spacing for directed trains + static let bleFragmentSpacingMs: Int = 5 + static let bleFragmentSpacingDirectedMs: Int = 4 static let bleAnnounceIntervalSeconds: TimeInterval = 4.0 static let bleDutyOnDurationDense: TimeInterval = 3.0 static let bleDutyOffDurationDense: TimeInterval = 15.0