diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index 078e2f31..525487e5 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -335,7 +335,7 @@ class PeerManager { // Remove stale peer IDs stalePeerIDs.forEach { stalePeerID -> - removePeer(stalePeerID, notifyDelegate = false) + removePeer(stalePeerID, notifyPeerList = false) } // Check if this is a new peer announcement @@ -371,7 +371,7 @@ class PeerManager { /** * Remove peer */ - fun removePeer(peerID: String, notifyDelegate: Boolean = true) { + fun removePeer(peerID: String, notifyPeerList: Boolean = true) { val removed = peers.remove(peerID) peerRSSI.remove(peerID) announcedPeers.remove(peerID) @@ -380,10 +380,13 @@ class PeerManager { // Also remove fingerprint mappings fingerprintManager.removePeer(peerID) - if (notifyDelegate && removed != null) { - // Notify specific removal event then list update + if (removed != null) { + // Lifecycle cleanup must always run. Callers may suppress only the + // intermediate peer-list update while atomically replacing a peer. try { delegate?.onPeerRemoved(peerID) } catch (_: Exception) {} - notifyPeerListUpdate() + if (notifyPeerList) { + notifyPeerListUpdate() + } } } diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt index c2849b01..785c249d 100644 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt +++ b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt @@ -67,6 +67,11 @@ class MeshGraphService private constructor() { nicknames.remove(peerID) announcements.remove(peerID) lastUpdate.remove(peerID) + announcements.keys.toList().forEach { originPeerID -> + announcements.computeIfPresent(originPeerID) { _, neighbors -> + neighbors - peerID + } + } publishSnapshot() } } diff --git a/app/src/test/kotlin/com/bitchat/PeerManagerTest.kt b/app/src/test/kotlin/com/bitchat/PeerManagerTest.kt index 90021c52..2a726ad0 100644 --- a/app/src/test/kotlin/com/bitchat/PeerManagerTest.kt +++ b/app/src/test/kotlin/com/bitchat/PeerManagerTest.kt @@ -1,6 +1,7 @@ package com.bitchat import com.bitchat.android.mesh.PeerManager +import com.bitchat.android.mesh.PeerManagerDelegate import com.bitchat.android.model.PeerCapabilities import junit.framework.TestCase.assertEquals import org.junit.Test @@ -167,6 +168,28 @@ class PeerManagerTest { assertEquals(testUsers.size - 3, numberOfAllPeers) } + @Test + fun suppressing_peer_list_update_still_notifies_peer_removal() { + val removedPeers = mutableListOf() + var peerListUpdates = 0 + peerManager.delegate = object : PeerManagerDelegate { + override fun onPeerListUpdated(peerIDs: List) { + peerListUpdates++ + } + + override fun onPeerRemoved(peerID: String) { + removedPeers.add(peerID) + } + } + peerManager.addOrUpdatePeer("peer-stale", "alice") + peerListUpdates = 0 + + peerManager.removePeer("peer-stale", notifyPeerList = false) + + assertEquals(listOf("peer-stale"), removedPeers) + assertEquals(0, peerListUpdates) + } + @Test fun last_seen_updated_correctly() { testUsers.forEach { peerID, _ -> diff --git a/app/src/test/kotlin/com/bitchat/android/services/meshgraph/MeshGraphServiceTest.kt b/app/src/test/kotlin/com/bitchat/android/services/meshgraph/MeshGraphServiceTest.kt index dc27ff99..feecea6a 100644 --- a/app/src/test/kotlin/com/bitchat/android/services/meshgraph/MeshGraphServiceTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/services/meshgraph/MeshGraphServiceTest.kt @@ -123,4 +123,19 @@ class MeshGraphServiceTest { assertFalse(snapshot.edges.isEmpty()) assertNotNull(snapshot.edges.find { (it.a == "PeerA" && it.b == "PeerB") || (it.a == "PeerB" && it.b == "PeerA") }) } + + @Test + fun removePeer_RemovesInboundAndOutboundGraphReferences() { + service.updateFromAnnouncement("PeerA", "Alice", listOf("PeerB"), 100UL) + service.updateFromAnnouncement("PeerB", "Bob", listOf("PeerA", "PeerC"), 100UL) + service.updateFromAnnouncement("PeerC", "Carol", listOf("PeerB"), 100UL) + + service.removePeer("PeerB") + + val snapshot = service.graphState.value + assertFalse(snapshot.nodes.any { it.peerID == "PeerB" }) + assertFalse(snapshot.edges.any { it.a == "PeerB" || it.b == "PeerB" }) + assertTrue(snapshot.nodes.any { it.peerID == "PeerA" }) + assertTrue(snapshot.nodes.any { it.peerID == "PeerC" }) + } }