Fix peer discovery stability and connection cycling issues

- Added debouncing for peer list updates (500ms) to prevent UI spam
- Fixed race conditions in activePeers management by adding proper locking
- Prevent duplicate key exchange processing with tracking set
- Only notify delegate when peer list actually changes
- Fixed thread safety issues in disconnect/unsubscribe handlers
- Clear processed key exchanges periodically with bloom filter

This resolves the rapid connect/disconnect cycles and excessive peer list updates.
This commit is contained in:
jack
2025-07-04 16:19:52 +02:00
parent 9ef9ef8056
commit 87d4455c25
+66 -26
View File
@@ -45,6 +45,7 @@ class BluetoothMeshService: NSObject {
private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery
private var announcedToPeers = Set<String>() // Track which peers we've announced to
private var announcedPeers = Set<String>() // Track peers who have already been announced
private var processedKeyExchanges = Set<String>() // Track processed key exchanges to prevent duplicates
// Store-and-forward message cache
private struct StoredMessage {
@@ -72,6 +73,10 @@ class BluetoothMeshService: NSObject {
private var batteryMonitorTimer: Timer?
private var currentBatteryLevel: Float = 1.0 // Default to full battery
// Peer list update debouncing
private var peerListUpdateTimer: Timer?
private let peerListUpdateDebounceInterval: TimeInterval = 0.5 // 500ms debounce
// Cover traffic for privacy
private var coverTrafficTimer: Timer?
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
@@ -276,6 +281,7 @@ class BluetoothMeshService: NSObject {
self?.messageQueue.async(flags: .barrier) {
self?.messageBloomFilter.reset()
self?.processedMessages.removeAll()
self?.processedKeyExchanges.removeAll()
}
}
@@ -776,6 +782,24 @@ class BluetoothMeshService: NSObject {
return Array(validPeers).sorted()
}
// Debounced peer list update notification
private func notifyPeerListUpdate() {
// Cancel any pending update
peerListUpdateTimer?.invalidate()
// Schedule a new update after debounce interval
peerListUpdateTimer = Timer.scheduledTimer(withTimeInterval: peerListUpdateDebounceInterval, repeats: false) { [weak self] _ in
guard let self = self else { return }
let connectedPeerIDs = self.getAllConnectedPeerIDs()
print("[DEBUG] Notifying peer list update after debounce: \(connectedPeerIDs.count) peers")
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(connectedPeerIDs)
}
}
}
// MARK: - Store-and-Forward Methods
private func cacheMessage(_ packet: BitchatPacket, messageID: String) {
@@ -1294,6 +1318,18 @@ class BluetoothMeshService: NSObject {
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
if packet.payload.count > 0 {
let publicKeyData = packet.payload
// Create a unique key for this exchange
let exchangeKey = "\(senderID)-\(publicKeyData.hexEncodedString().prefix(16))"
// Check if we've already processed this key exchange
if processedKeyExchanges.contains(exchangeKey) {
print("[DEBUG] Ignoring duplicate key exchange from \(senderID)")
return
}
// Mark this key exchange as processed
processedKeyExchanges.insert(exchangeKey)
do {
try encryptionService.addPeerPublicKey(senderID, publicKeyData: publicKeyData)
// Added public key
@@ -1334,12 +1370,18 @@ class BluetoothMeshService: NSObject {
}
}
// Add to active peers immediately on key exchange
// Add to active peers with proper locking
activePeersLock.lock()
let wasNewPeer = !activePeers.contains(senderID)
if wasNewPeer {
activePeers.insert(senderID)
print("[DEBUG] Added peer \(senderID) to active peers via key exchange")
let connectedPeerIDs = self.getAllConnectedPeerIDs()
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(connectedPeerIDs)
}
activePeersLock.unlock()
// Only notify if this was actually a new peer
if wasNewPeer {
self.notifyPeerListUpdate()
}
}
@@ -1394,8 +1436,10 @@ class BluetoothMeshService: NSObject {
announcedPeers.insert(senderID)
DispatchQueue.main.async {
self.delegate?.didConnectToPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
DispatchQueue.main.async {
// Check if this is a favorite peer and send notification
// Note: This might not work immediately if key exchange hasn't happened yet
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
@@ -1416,9 +1460,7 @@ class BluetoothMeshService: NSObject {
}
} else {
// Just update the peer list
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
}
} else {
}
@@ -1445,15 +1487,18 @@ class BluetoothMeshService: NSObject {
// Peer is leaving
// Remove from active peers
// Remove from active peers with proper locking
activePeersLock.lock()
activePeers.remove(senderID)
activePeersLock.unlock()
announcedPeers.remove(senderID)
// Show leave message
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
// Clean up peer data
peerNicknamesLock.lock()
@@ -1792,8 +1837,11 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
connectedPeripherals.removeValue(forKey: peerID)
peripheralCharacteristics.removeValue(forKey: peripheral)
// Remove from active peers
// Remove from active peers with proper locking
activePeersLock.lock()
activePeers.remove(peerID)
activePeersLock.unlock()
announcedPeers.remove(peerID)
announcedToPeers.remove(peerID)
@@ -1809,13 +1857,9 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
if let nickname = nickname, nickname != peerID {
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
} else {
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
self.notifyPeerListUpdate()
}
// Keep in pool but remove from discovered list
@@ -1967,7 +2011,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
// It's a real peer ID, store it
self.peerRSSI[peerID] = RSSI
// Force UI update when we have a real peer ID
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
self.notifyPeerListUpdate()
}
}
@@ -2050,9 +2094,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
}
}
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
}
handleReceivedPacket(packet, from: peerID)
@@ -2082,9 +2124,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
}
// Update peer list to show we're connected (even without peer ID yet)
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
}
}
@@ -2094,6 +2134,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
// If no more centrals are subscribed, clear all central-connected peers
if subscribedCentrals.isEmpty {
// Find and remove peers that were connected as centrals only
activePeersLock.lock()
let peersToRemove = activePeers.filter { peerID in
!connectedPeripherals.keys.contains(peerID)
}
@@ -2111,10 +2152,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
}
}
}
activePeersLock.unlock()
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
self.notifyPeerListUpdate()
}
// Ensure advertising continues for reconnection