From edb30d6c80a0b0798d4f2cb55fe828e71b577bef Mon Sep 17 00:00:00 2001 From: Nelson Campos Date: Mon, 7 Jul 2025 23:42:33 -0300 Subject: [PATCH] fix: ensure messages maintain correct order during sync - Sort retry queue by original timestamp when adding and processing messages - Add 50ms delay between retry messages to prevent out-of-order delivery - Messages now sync in the exact order they were sent (ping 1, ping 2, ping 3) --- bitchat/Services/MessageRetryService.swift | 211 ++++++++++++--------- 1 file changed, 117 insertions(+), 94 deletions(-) diff --git a/bitchat/Services/MessageRetryService.swift b/bitchat/Services/MessageRetryService.swift index 6cce8046..43370f58 100644 --- a/bitchat/Services/MessageRetryService.swift +++ b/bitchat/Services/MessageRetryService.swift @@ -87,6 +87,13 @@ class MessageRetryService { ) retryQueue.append(retryMessage) + + // Sort the queue by original timestamp to maintain message order + retryQueue.sort { (msg1, msg2) in + let time1 = msg1.originalTimestamp ?? Date.distantPast + let time2 = msg2.originalTimestamp ?? Date.distantPast + return time1 < time2 + } } private func processRetryQueue() { @@ -106,107 +113,123 @@ class MessageRetryService { retryQueue = updatedQueue - for message in messagesToRetry { + // Sort messages by original timestamp to maintain order + messagesToRetry.sort { (msg1, msg2) in + let time1 = msg1.originalTimestamp ?? Date.distantPast + let time2 = msg2.originalTimestamp ?? Date.distantPast + return time1 < time2 + } + + // Send messages with delay to maintain order + for (index, message) in messagesToRetry.enumerated() { // Check if we should still retry if message.retryCount >= message.maxRetries { continue } - // Check connectivity before retrying - let viewModel = meshService.delegate as? ChatViewModel - let connectedPeers = viewModel?.connectedPeers ?? [] + // Add delay between messages to ensure proper ordering + let delay = Double(index) * 0.05 // 50ms between messages - if message.isPrivate { - // For private messages, check if recipient is connected - if let recipientID = message.recipientPeerID, - connectedPeers.contains(recipientID) { - // Retry private message - meshService.sendPrivateMessage( - message.content, - to: recipientID, - recipientNickname: message.recipientNickname ?? "unknown", - messageID: message.originalMessageID - ) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self = self, + let meshService = self.meshService else { return } + + // Check connectivity before retrying + let viewModel = meshService.delegate as? ChatViewModel + let connectedPeers = viewModel?.connectedPeers ?? [] + + if message.isPrivate { + // For private messages, check if recipient is connected + if let recipientID = message.recipientPeerID, + connectedPeers.contains(recipientID) { + // Retry private message + meshService.sendPrivateMessage( + message.content, + to: recipientID, + recipientNickname: message.recipientNickname ?? "unknown", + messageID: message.originalMessageID + ) + } else { + // Recipient not connected, keep in queue with updated retry time + var updatedMessage = message + updatedMessage = RetryableMessage( + id: message.id, + originalMessageID: message.originalMessageID, + originalTimestamp: message.originalTimestamp, + content: message.content, + mentions: message.mentions, + channel: message.channel, + isPrivate: message.isPrivate, + recipientPeerID: message.recipientPeerID, + recipientNickname: message.recipientNickname, + channelKey: message.channelKey, + retryCount: message.retryCount + 1, + nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2)) + ) + self.retryQueue.append(updatedMessage) + } + } else if let channel = message.channel, let channelKeyData = message.channelKey { + // For channel messages, check if we have peers in the channel + if !connectedPeers.isEmpty { + // Recreate SymmetricKey from data + let channelKey = SymmetricKey(data: channelKeyData) + meshService.sendEncryptedChannelMessage( + message.content, + mentions: message.mentions ?? [], + channel: channel, + channelKey: channelKey, + messageID: message.originalMessageID, + timestamp: message.originalTimestamp + ) + } else { + // No peers connected, keep in queue + var updatedMessage = message + updatedMessage = RetryableMessage( + id: message.id, + originalMessageID: message.originalMessageID, + originalTimestamp: message.originalTimestamp, + content: message.content, + mentions: message.mentions, + channel: message.channel, + isPrivate: message.isPrivate, + recipientPeerID: message.recipientPeerID, + recipientNickname: message.recipientNickname, + channelKey: message.channelKey, + retryCount: message.retryCount + 1, + nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2)) + ) + self.retryQueue.append(updatedMessage) + } } else { - // Recipient not connected, keep in queue with updated retry time - var updatedMessage = message - updatedMessage = RetryableMessage( - id: message.id, - originalMessageID: message.originalMessageID, - originalTimestamp: message.originalTimestamp, - content: message.content, - mentions: message.mentions, - channel: message.channel, - isPrivate: message.isPrivate, - recipientPeerID: message.recipientPeerID, - recipientNickname: message.recipientNickname, - channelKey: message.channelKey, - retryCount: message.retryCount + 1, - nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2)) - ) - retryQueue.append(updatedMessage) - } - } else if let channel = message.channel, let channelKeyData = message.channelKey { - // For channel messages, check if we have peers in the channel - if !connectedPeers.isEmpty { - // Recreate SymmetricKey from data - let channelKey = SymmetricKey(data: channelKeyData) - meshService.sendEncryptedChannelMessage( - message.content, - mentions: message.mentions ?? [], - channel: channel, - channelKey: channelKey, - messageID: message.originalMessageID, - timestamp: message.originalTimestamp - ) - } else { - // No peers connected, keep in queue - var updatedMessage = message - updatedMessage = RetryableMessage( - id: message.id, - originalMessageID: message.originalMessageID, - originalTimestamp: message.originalTimestamp, - content: message.content, - mentions: message.mentions, - channel: message.channel, - isPrivate: message.isPrivate, - recipientPeerID: message.recipientPeerID, - recipientNickname: message.recipientNickname, - channelKey: message.channelKey, - retryCount: message.retryCount + 1, - nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2)) - ) - retryQueue.append(updatedMessage) - } - } else { - // Regular message - if !connectedPeers.isEmpty { - meshService.sendMessage( - message.content, - mentions: message.mentions ?? [], - channel: message.channel, - to: nil, - messageID: message.originalMessageID, - timestamp: message.originalTimestamp - ) - } else { - // No peers connected, keep in queue - var updatedMessage = message - updatedMessage = RetryableMessage( - id: message.id, - originalMessageID: message.originalMessageID, - originalTimestamp: message.originalTimestamp, - content: message.content, - mentions: message.mentions, - channel: message.channel, - isPrivate: message.isPrivate, - recipientPeerID: message.recipientPeerID, - recipientNickname: message.recipientNickname, - channelKey: message.channelKey, - retryCount: message.retryCount + 1, - nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2)) - ) - retryQueue.append(updatedMessage) + // Regular message + if !connectedPeers.isEmpty { + meshService.sendMessage( + message.content, + mentions: message.mentions ?? [], + channel: message.channel, + to: nil, + messageID: message.originalMessageID, + timestamp: message.originalTimestamp + ) + } else { + // No peers connected, keep in queue + var updatedMessage = message + updatedMessage = RetryableMessage( + id: message.id, + originalMessageID: message.originalMessageID, + originalTimestamp: message.originalTimestamp, + content: message.content, + mentions: message.mentions, + channel: message.channel, + isPrivate: message.isPrivate, + recipientPeerID: message.recipientPeerID, + recipientNickname: message.recipientNickname, + channelKey: message.channelKey, + retryCount: message.retryCount + 1, + nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2)) + ) + self.retryQueue.append(updatedMessage) + } } } }