Cut public message path over to ConversationStore; delete PublicTimelineStore

Mesh and geohash timelines are now store conversations. All public
mutation sites flow through store intents; PublicMessagePipeline keeps
its 80ms UI batching but commits batches via store appends with each
buffered entry carrying its destination conversation (a mid-batch
channel switch now flushes instead of dropping the buffer).
ChatViewModel.messages becomes a cached get-only view of the active
conversation, invalidated through the change subject. The mesh
late-insert threshold is consciously removed: it only ever ordered the
non-rendered messages copy, so strict timestamp insertion makes the
working set agree with rendered order. PublicTimelineStore and the
per-message full-array legacy sync are deleted; the coalescing bridge
mirrors public conversations for the remaining legacy readers.

pipeline.publicIngest: 6.6k -> 9.5k msg/s (+45%); private steady;
store.append 237k/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-11 13:03:07 +02:00
co-authored by Claude Fable 5
parent 879d8cba12
commit 99d1d1dccd
26 changed files with 713 additions and 875 deletions
@@ -52,23 +52,17 @@ private final class MockChatMediaTransferContext: ChatMediaTransferContext {
return true
}
var meshTimeline: [BitchatMessage] = []
private(set) var refreshedChannels: [ChannelID?] = []
private(set) var trimCount = 0
private(set) var appendedPublicMessages: [(message: BitchatMessage, conversationID: ConversationID)] = []
private(set) var removedMessages: [(messageID: String, cleanupFile: Bool)] = []
private(set) var systemMessages: [String] = []
private(set) var notifyUIChangedCount = 0
func appendTimelineMessage(_ message: BitchatMessage, to channel: ChannelID) {
meshTimeline.append(message)
@discardableResult
func appendPublicMessage(_ message: BitchatMessage, to conversationID: ConversationID) -> Bool {
appendedPublicMessages.append((message, conversationID))
return true
}
func refreshVisibleMessages(from channel: ChannelID?) {
refreshedChannels.append(channel)
}
func trimMessagesIfNeeded() { trimCount += 1 }
func removeMessage(withID messageID: String, cleanupFile: Bool) {
removedMessages.append((messageID, cleanupFile))
}
@@ -129,22 +123,21 @@ struct ChatMediaTransferCoordinatorContextTests {
#expect(message.senderPeerID == context.myPeerID)
#expect(message.deliveryStatus == .sending)
#expect(context.recordedContentKeys == ["[voice] note.m4a"])
#expect(context.trimCount == 1)
#expect(context.notifyUIChangedCount == 1)
#expect(context.meshTimeline.isEmpty)
#expect(context.appendedPublicMessages.isEmpty)
}
@Test @MainActor
func enqueueMediaMessage_publicAppendsToTimelineAndRefreshes() async {
func enqueueMediaMessage_publicAppendsToActiveConversation() async {
let context = MockChatMediaTransferContext()
let coordinator = ChatMediaTransferCoordinator(context: context)
let message = coordinator.enqueueMediaMessage(content: "[image] pic.jpg", targetPeer: nil)
#expect(context.meshTimeline.map(\.id) == [message.id])
#expect(context.appendedPublicMessages.map(\.message.id) == [message.id])
#expect(context.appendedPublicMessages.first?.conversationID == .mesh)
#expect(!message.isPrivate)
#expect(message.sender == "me")
#expect(context.refreshedChannels.count == 1)
#expect(context.privateChats.isEmpty)
#expect(context.notifyUIChangedCount == 1)
}
@@ -210,7 +203,7 @@ struct ChatMediaTransferCoordinatorContextTests {
#expect(!FileManager.default.fileExists(atPath: url.path))
#expect(context.systemMessages == ["Voice notes are only available in mesh chats."])
#expect(context.privateChats.isEmpty)
#expect(context.meshTimeline.isEmpty)
#expect(context.appendedPublicMessages.isEmpty)
#expect(coordinator.transferIdToMessageIDs.isEmpty)
}
}