mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:05:21 +00:00
Clear pending retry resolution on peer disconnect
A private-media policy resolution whose completion is dropped by transport teardown (BLEService messageQueue deallocation guard) left its peersResolvingReconnectRetry entry in place, parking every future reconnect-retry resolution for that peer until panic reset. Disconnect now removes the entry; retained records survive and the next reconnect starts a fresh resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -965,6 +965,16 @@ final class ChatMediaTransferCoordinator {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A policy resolution's completion can be dropped entirely when the
|
||||||
|
/// transport tears down mid-flight (BLEService's queue guards on a
|
||||||
|
/// deallocated self), which would leave the pending entry blocking every
|
||||||
|
/// future resolution for this peer. Disconnection invalidates the
|
||||||
|
/// resolution's premise anyway, so drop it; retained records stay and the
|
||||||
|
/// next reconnect starts a fresh resolution.
|
||||||
|
func peerDidDisconnect(_ peerID: PeerID) {
|
||||||
|
peersResolvingReconnectRetry.removeValue(forKey: peerID.toShort())
|
||||||
|
}
|
||||||
|
|
||||||
/// Local fragment completion is not proof that the recipient reconstructed
|
/// Local fragment completion is not proof that the recipient reconstructed
|
||||||
/// the file. Only a remote delivery/read receipt releases retry ownership.
|
/// the file. Only a remote delivery/read receipt releases retry ownership.
|
||||||
func confirmPrivateMediaDelivery(messageID: String) {
|
func confirmPrivateMediaDelivery(messageID: String) {
|
||||||
|
|||||||
@@ -1773,6 +1773,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
|||||||
|
|
||||||
case .peerDisconnected(let peerID):
|
case .peerDisconnected(let peerID):
|
||||||
transportEventCoordinator.didDisconnectFromPeerSynchronously(peerID)
|
transportEventCoordinator.didDisconnectFromPeerSynchronously(peerID)
|
||||||
|
mediaTransferCoordinator.peerDidDisconnect(peerID)
|
||||||
|
|
||||||
case .peerListUpdated(let peers):
|
case .peerListUpdated(let peers):
|
||||||
peerListCoordinator.didUpdatePeerListSynchronously(peers)
|
peerListCoordinator.didUpdatePeerListSynchronously(peers)
|
||||||
@@ -1866,6 +1867,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
|||||||
|
|
||||||
func didDisconnectFromPeer(_ peerID: PeerID) {
|
func didDisconnectFromPeer(_ peerID: PeerID) {
|
||||||
transportEventCoordinator.didDisconnectFromPeer(peerID)
|
transportEventCoordinator.didDisconnectFromPeer(peerID)
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
self?.mediaTransferCoordinator.peerDidDisconnect(peerID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func didUpdatePeerList(_ peers: [PeerID]) {
|
func didUpdatePeerList(_ peers: [PeerID]) {
|
||||||
|
|||||||
@@ -979,6 +979,63 @@ struct ChatMediaTransferCoordinatorContextTests {
|
|||||||
#expect(coordinator.messageIDToTransferId[messageID] == nil)
|
#expect(coordinator.messageIDToTransferId[messageID] == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test @MainActor
|
||||||
|
func disconnectClearsDroppedPolicyResolutionSoRetriesRecover() async throws {
|
||||||
|
let context = MockChatMediaTransferContext()
|
||||||
|
let peerID = PeerID(str: "1122334455667788")
|
||||||
|
context.selectedPrivateChatPeer = peerID
|
||||||
|
context.supportsAuthenticatedPrivateMediaReceipts = true
|
||||||
|
let fileName = "voice_3333444455556666.m4a"
|
||||||
|
let preparer = StaticVoiceNotePreparer(fileName: fileName)
|
||||||
|
let coordinator = ChatMediaTransferCoordinator(
|
||||||
|
context: context,
|
||||||
|
prepareVoiceNotePacket: { url in
|
||||||
|
try preparer.prepare(url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let url = try makeCoordinatorVoiceURL(fileName: fileName)
|
||||||
|
defer {
|
||||||
|
try? FileManager.default.removeItem(
|
||||||
|
at: url.deletingLastPathComponent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinator.sendVoiceNote(at: url)
|
||||||
|
#expect(await TestHelpers.waitUntil(
|
||||||
|
{ context.privateFileSends.count == 1 },
|
||||||
|
timeout: TestConstants.longTimeout
|
||||||
|
))
|
||||||
|
let initial = try #require(context.privateFileSends.first)
|
||||||
|
coordinator.handleTransferEvent(.completed(
|
||||||
|
id: initial.transferId,
|
||||||
|
totalFragments: 1
|
||||||
|
))
|
||||||
|
#expect(coordinator.retainedReconnectRetryCount == 1)
|
||||||
|
|
||||||
|
// The transport accepts this resolution but its completion is never
|
||||||
|
// invoked (BLEService queue teardown drops the closure), so the
|
||||||
|
// pending entry parks every subsequent reconnect resolution.
|
||||||
|
context.resolvesPrivateMediaPolicyImmediately = false
|
||||||
|
coordinator.peerDidReconnect(peerID)
|
||||||
|
#expect(context.privateMediaPolicyResolutionRequests.count == 1)
|
||||||
|
coordinator.peerDidReconnect(peerID)
|
||||||
|
#expect(context.privateMediaPolicyResolutionRequests.count == 1)
|
||||||
|
|
||||||
|
// Disconnection invalidates the dropped resolution; the next
|
||||||
|
// reconnect must start fresh and complete the retry.
|
||||||
|
coordinator.peerDidDisconnect(peerID)
|
||||||
|
context.resolvesPrivateMediaPolicyImmediately = true
|
||||||
|
coordinator.peerDidReconnect(peerID)
|
||||||
|
#expect(context.privateMediaPolicyResolutionRequests.count == 2)
|
||||||
|
#expect(context.privateFileSends.count == 2)
|
||||||
|
let retry = try #require(context.privateFileSends.last)
|
||||||
|
#expect(retry.packet.encode() == initial.packet.encode())
|
||||||
|
#expect(retry.peerID == peerID)
|
||||||
|
#expect(context.privateFileReceiptRetryTransferIDs == [
|
||||||
|
retry.transferId
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
@Test @MainActor
|
@Test @MainActor
|
||||||
func bit8OnlyEncryptedMediaNeverRetainsOrAutomaticallyRetries() async throws {
|
func bit8OnlyEncryptedMediaNeverRetainsOrAutomaticallyRetries() async throws {
|
||||||
let context = MockChatMediaTransferContext()
|
let context = MockChatMediaTransferContext()
|
||||||
|
|||||||
Reference in New Issue
Block a user