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
+18 -4
View File
@@ -438,7 +438,7 @@ struct ChatViewModelReceivingTests {
)
let found = await TestHelpers.waitUntil({
viewModel.timelineStore.messages(for: .mesh).contains { $0.content == "Public hello from Bob" }
viewModel.publicMessages(for: .mesh).contains { $0.content == "Public hello from Bob" }
}, timeout: TestConstants.defaultTimeout)
#expect(found)
@@ -709,10 +709,12 @@ struct ChatViewModelPublicConversationTests {
let (viewModel, _) = makeTestableViewModel()
viewModel.addPublicSystemMessage("system refresh test")
viewModel.messages.removeAll()
viewModel.refreshVisibleMessages(from: .mesh)
// The system message lives in the mesh conversation itself, so the
// derived `messages` view still surfaces it after a refresh.
#expect(viewModel.messages.last?.content == "system refresh test")
#expect(viewModel.publicMessages(for: .mesh).last?.content == "system refresh test")
}
@Test @MainActor
@@ -726,6 +728,18 @@ struct ChatViewModelPublicConversationTests {
viewModel.refreshVisibleMessages(from: .mesh)
#expect(viewModel.messages.isEmpty)
#expect(viewModel.publicMessages(for: .mesh).isEmpty)
}
@Test @MainActor
func queuedGeohashSystemMessages_drainOnce() async {
let (viewModel, _) = makeTestableViewModel()
viewModel.queueGeohashSystemMessage("first")
viewModel.queueGeohashSystemMessage("second")
#expect(viewModel.drainPendingGeohashSystemMessages() == ["first", "second"])
#expect(viewModel.drainPendingGeohashSystemMessages().isEmpty)
}
}
@@ -999,7 +1013,7 @@ struct ChatViewModelPanicTests {
// Set up some state
transport.connectedPeers.insert(PeerID(str: "PEER1"))
viewModel.messages = [
viewModel.seedPublicMessages([
BitchatMessage(
id: "panic-1",
sender: "Tester",
@@ -1007,7 +1021,7 @@ struct ChatViewModelPanicTests {
timestamp: Date(),
isRelay: false
)
]
])
viewModel.seedPrivateChat([
BitchatMessage(
id: "pm-1",