mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Rebuild message location index on non-append growth
The incremental index refresh assumed growth meant appended messages, but PublicMessagePipeline inserts out-of-order arrivals by timestamp: the count grows while the tail ID stays put, so the inserted message never entered the index and later delivery updates for it silently no-op'd (and, with retain-until-ack routing, left it queued for resend). Detect non-append growth by checking the previously indexed tail kept its position, and rebuild when it hasn't. Same check on the per-peer private chat arrays, which re-sort by timestamp. Found by Codex review on #1331. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -238,6 +238,15 @@ private extension ChatDeliveryCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if context.messages.count > indexedPublicMessageCount {
|
if context.messages.count > indexedPublicMessageCount {
|
||||||
|
// Growth is only a pure append if the previously indexed tail kept
|
||||||
|
// its position; a middle insertion (out-of-order timestamp arrival)
|
||||||
|
// shifts it and invalidates every indexed location after the
|
||||||
|
// insertion point.
|
||||||
|
if indexedPublicMessageCount > 0,
|
||||||
|
context.messages[indexedPublicMessageCount - 1].id != indexedPublicTailMessageID {
|
||||||
|
rebuildMessageLocationIndex()
|
||||||
|
return true
|
||||||
|
}
|
||||||
for index in indexedPublicMessageCount..<context.messages.count {
|
for index in indexedPublicMessageCount..<context.messages.count {
|
||||||
add(.publicTimeline(index: index), for: context.messages[index].id)
|
add(.publicTimeline(index: index), for: context.messages[index].id)
|
||||||
}
|
}
|
||||||
@@ -265,6 +274,12 @@ private extension ChatDeliveryCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard messages.count > indexedCount else { continue }
|
guard messages.count > indexedCount else { continue }
|
||||||
|
// Same append-only check as the public timeline above.
|
||||||
|
if indexedCount > 0,
|
||||||
|
messages[indexedCount - 1].id != indexedPrivateTailMessageIDs[peerID] {
|
||||||
|
rebuildMessageLocationIndex()
|
||||||
|
return true
|
||||||
|
}
|
||||||
for index in indexedCount..<messages.count {
|
for index in indexedCount..<messages.count {
|
||||||
add(.privateChat(peerID: peerID, index: index), for: messages[index].id)
|
add(.privateChat(peerID: peerID, index: index), for: messages[index].id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,6 +483,59 @@ struct ChatDeliveryCoordinatorContextTests {
|
|||||||
#expect(context.notifyUIChangedCount == 1)
|
#expect(context.notifyUIChangedCount == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test @MainActor
|
||||||
|
func middleInsertedMessage_isFoundAfterIndexWasBuilt() async {
|
||||||
|
let context = MockChatDeliveryContext()
|
||||||
|
let coordinator = ChatDeliveryCoordinator(context: context)
|
||||||
|
let makePublic = { (id: String) in
|
||||||
|
BitchatMessage(
|
||||||
|
id: id,
|
||||||
|
sender: "me",
|
||||||
|
content: "Public message",
|
||||||
|
timestamp: Date(),
|
||||||
|
isRelay: false,
|
||||||
|
isPrivate: false,
|
||||||
|
deliveryStatus: .sending
|
||||||
|
)
|
||||||
|
}
|
||||||
|
context.messages = [makePublic("public-a"), makePublic("public-b")]
|
||||||
|
|
||||||
|
// Build the incremental index.
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("public-a", status: .sent))
|
||||||
|
|
||||||
|
// Out-of-order arrival: PublicMessagePipeline inserts by timestamp,
|
||||||
|
// so the count grows while the tail ID stays the same.
|
||||||
|
context.messages.insert(makePublic("public-mid"), at: 1)
|
||||||
|
|
||||||
|
// The inserted message must be locatable, and the shifted tail must
|
||||||
|
// not be updated through a stale index entry.
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("public-mid", status: .sent))
|
||||||
|
#expect(isSent(context.messages[1].deliveryStatus))
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("public-b", status: .sent))
|
||||||
|
#expect(isSent(context.messages[2].deliveryStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor
|
||||||
|
func middleInsertedPrivateMessage_isFoundAfterIndexWasBuilt() async {
|
||||||
|
let context = MockChatDeliveryContext()
|
||||||
|
let coordinator = ChatDeliveryCoordinator(context: context)
|
||||||
|
let peerID = PeerID(str: "0102030405060708")
|
||||||
|
context.privateChats[peerID] = [
|
||||||
|
makePrivateMessage(id: "pm-a", status: .sending),
|
||||||
|
makePrivateMessage(id: "pm-b", status: .sending)
|
||||||
|
]
|
||||||
|
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("pm-a", status: .sent))
|
||||||
|
|
||||||
|
// Timestamp re-sort (sanitizeChat) can place a late arrival mid-array.
|
||||||
|
context.privateChats[peerID]?.insert(makePrivateMessage(id: "pm-mid", status: .sending), at: 1)
|
||||||
|
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("pm-mid", status: .sent))
|
||||||
|
#expect(isSent(context.privateChats[peerID]?[1].deliveryStatus))
|
||||||
|
#expect(coordinator.updateMessageDeliveryStatus("pm-b", status: .sent))
|
||||||
|
#expect(isSent(context.privateChats[peerID]?[2].deliveryStatus))
|
||||||
|
}
|
||||||
|
|
||||||
@Test @MainActor
|
@Test @MainActor
|
||||||
func cleanupOldReadReceipts_prunesReceiptsAgainstMockContext() async {
|
func cleanupOldReadReceipts_prunesReceiptsAgainstMockContext() async {
|
||||||
let context = MockChatDeliveryContext()
|
let context = MockChatDeliveryContext()
|
||||||
|
|||||||
Reference in New Issue
Block a user