Cut private message path over to ConversationStore

All private-message mutations now flow through store intents:
coordinators, PrivateChatManager (its @Published dicts deleted - now
read-only views over the store), outbound sends, delivery status, and
chat migration. The O(1) store dedup replaces the full-scan duplicate
check; insertion order is maintained by the store so sanitizeChat's
re-sort is a documented no-op. Both bootstrapper Combine bridges and
the Task.yield store synchronization are deleted.

ChatViewModel.privateChats/unreadPrivateMessages become get-only derived
views (measured: naive rebuild equals a change-invalidated cache within
noise, so the simpler form stays). Feature models still read the legacy
store, fed by a coalescing LegacyConversationStoreBridge (one mirror
per burst, marked for step-5 deletion).

pipeline.privateIngest: 9.6k -> 14.7k msg/s (+53%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-11 12:19:11 +02:00
co-authored by Claude Fable 5
parent ac3a2f2d34
commit 879d8cba12
33 changed files with 1293 additions and 545 deletions
@@ -54,7 +54,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .read(by: "Peer", at: Date())
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
// Action: try to downgrade to .delivered
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .delivered(to: "Peer", at: Date()))
@@ -85,7 +85,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
// Action: upgrade to .delivered
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .delivered(to: "Peer", at: Date()))
@@ -114,7 +114,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
let didUpdate = viewModel.deliveryCoordinator.updateMessageDeliveryStatus(messageID, status: .sent)
@@ -140,7 +140,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .delivered(to: "Peer", at: Date().addingTimeInterval(-60))
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
// Action: upgrade to .read
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .read(by: "Peer", at: Date()))
@@ -173,7 +173,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
// Action: receive read receipt
let receipt = ReadReceipt(
@@ -207,7 +207,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
viewModel.sentReadReceipts = ["keep-receipt", "drop-receipt"]
viewModel.isStartupPhase = false
@@ -265,7 +265,7 @@ struct ChatViewModelDeliveryStatusTests {
deliveryStatus: .sent
)
]
viewModel.privateChats[firstPeerID] = [
viewModel.seedPrivateChat([
BitchatMessage(
id: messageID,
sender: viewModel.nickname,
@@ -277,8 +277,8 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
]
viewModel.privateChats[secondPeerID] = [
], for: firstPeerID)
viewModel.seedPrivateChat([
BitchatMessage(
id: messageID,
sender: viewModel.nickname,
@@ -290,7 +290,7 @@ struct ChatViewModelDeliveryStatusTests {
senderPeerID: transport.myPeerID,
deliveryStatus: .sent
)
]
], for: secondPeerID)
let didUpdate = viewModel.deliveryCoordinator.updateMessageDeliveryStatus(
messageID,
@@ -331,10 +331,15 @@ struct ChatViewModelDeliveryStatusTests {
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [targetMessage, olderMessage]
viewModel.seedPrivateChat([targetMessage], for: peerID)
#expect(isSent(viewModel.deliveryCoordinator.deliveryStatus(for: messageID)))
viewModel.privateChats[peerID] = [olderMessage, targetMessage]
// A late arrival with an older timestamp is inserted before the
// target by the store, shifting its position and invalidating the
// delivery coordinator's location index.
viewModel.seedPrivateChat([olderMessage], for: peerID)
#expect(viewModel.privateChats[peerID]?.map(\.id) == ["older-msg", messageID])
let didUpdate = viewModel.deliveryCoordinator.updateMessageDeliveryStatus(
messageID,
status: .read(by: "Peer", at: Date())
@@ -401,6 +406,20 @@ private final class MockChatDeliveryContext: ChatDeliveryContext {
func markMessageDelivered(_ messageID: String) {
markedDeliveredMessageIDs.append(messageID)
}
@discardableResult
func setPrivateDeliveryStatus(_ status: DeliveryStatus, forMessageID messageID: String, peerID: PeerID) -> Bool {
guard var chat = privateChats[peerID],
let index = chat.firstIndex(where: { $0.id == messageID }) else {
return false
}
if Conversation.shouldSkipStatusUpdate(current: chat[index].deliveryStatus, new: status) {
return false
}
chat[index].deliveryStatus = status
privateChats[peerID] = chat
return true
}
}
@MainActor