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
+61 -64
View File
@@ -136,7 +136,7 @@ struct ChatViewModelIdentityTests {
senderPeerID: oldPeerID,
mentions: nil
)
viewModel.privateChats[oldPeerID] = [existingMessage]
viewModel.seedPrivateChat([existingMessage], for: oldPeerID)
viewModel.startPrivateChat(with: oldPeerID)
#expect(viewModel.selectedPrivateChatPeer == oldPeerID)
@@ -345,8 +345,8 @@ struct ChatViewModelServiceLifecycleTests {
mentions: nil
)
viewModel.privateChats[peerID] = [message]
viewModel.unreadPrivateMessages.insert(peerID)
viewModel.seedPrivateChat([message], for: peerID)
viewModel.markPrivateChatUnread(peerID)
viewModel.selectedPrivateChatPeer = peerID
viewModel.handleDidBecomeActive()
@@ -497,7 +497,7 @@ struct ChatViewModelNoisePayloadTests {
mentions: nil,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
viewModel.didReceiveNoisePayload(
from: peerID,
@@ -535,7 +535,7 @@ struct ChatViewModelNoisePayloadTests {
mentions: nil,
deliveryStatus: .sent
)
viewModel.privateChats[peerID] = [message]
viewModel.seedPrivateChat([message], for: peerID)
viewModel.didReceiveNoisePayload(
from: peerID,
@@ -771,7 +771,7 @@ struct ChatViewModelPeerTests {
func didUpdatePeerList_removesStaleUnreadPeerWithoutMessages() async {
let (viewModel, _) = makeTestableViewModel()
let stalePeer = PeerID(str: "00000000000000a2")
viewModel.unreadPrivateMessages = [stalePeer]
viewModel.markPrivateChatUnread(stalePeer)
viewModel.didUpdatePeerList([])
@@ -798,8 +798,8 @@ struct ChatViewModelPeerTests {
senderPeerID: stablePeer,
mentions: nil
)
viewModel.privateChats[stablePeer] = [message]
viewModel.unreadPrivateMessages = [stablePeer]
viewModel.seedPrivateChat([message], for: stablePeer)
viewModel.markPrivateChatUnread(stablePeer)
viewModel.didUpdatePeerList([])
try? await Task.sleep(nanoseconds: 100_000_000)
@@ -876,33 +876,32 @@ struct ChatViewModelPrivateChatSelectionTests {
let older = Date().addingTimeInterval(-120)
let newer = Date().addingTimeInterval(-30)
viewModel.privateChats = [
peerA: [
BitchatMessage(
id: "a-1",
sender: "A",
content: "Old",
timestamp: older,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerA
)
],
peerB: [
BitchatMessage(
id: "b-1",
sender: "B",
content: "New",
timestamp: newer,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerB
)
]
]
viewModel.unreadPrivateMessages = [peerA, peerB]
viewModel.seedPrivateChat([
BitchatMessage(
id: "a-1",
sender: "A",
content: "Old",
timestamp: older,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerA
)
], for: peerA)
viewModel.seedPrivateChat([
BitchatMessage(
id: "b-1",
sender: "B",
content: "New",
timestamp: newer,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerB
)
], for: peerB)
viewModel.markPrivateChatUnread(peerA)
viewModel.markPrivateChatUnread(peerB)
viewModel.openMostRelevantPrivateChat()
@@ -918,32 +917,30 @@ struct ChatViewModelPrivateChatSelectionTests {
let older = Date().addingTimeInterval(-200)
let newer = Date().addingTimeInterval(-20)
viewModel.privateChats = [
peerA: [
BitchatMessage(
id: "a-1",
sender: "A",
content: "Old",
timestamp: older,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerA
)
],
peerB: [
BitchatMessage(
id: "b-1",
sender: "B",
content: "New",
timestamp: newer,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerB
)
]
]
viewModel.seedPrivateChat([
BitchatMessage(
id: "a-1",
sender: "A",
content: "Old",
timestamp: older,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerA
)
], for: peerA)
viewModel.seedPrivateChat([
BitchatMessage(
id: "b-1",
sender: "B",
content: "New",
timestamp: newer,
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: peerB
)
], for: peerB)
viewModel.openMostRelevantPrivateChat()
@@ -1011,7 +1008,7 @@ struct ChatViewModelPanicTests {
isRelay: false
)
]
viewModel.privateChats[PeerID(str: "PEER1")] = [
viewModel.seedPrivateChat([
BitchatMessage(
id: "pm-1",
sender: "Peer",
@@ -1022,8 +1019,8 @@ struct ChatViewModelPanicTests {
recipientNickname: "Me",
senderPeerID: PeerID(str: "PEER1")
)
]
viewModel.unreadPrivateMessages.insert(PeerID(str: "PEER1"))
], for: PeerID(str: "PEER1"))
viewModel.markPrivateChatUnread(PeerID(str: "PEER1"))
viewModel.panicClearAllData()