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
@@ -174,4 +174,73 @@ struct LegacyConversationStoreBridgeTests {
#expect(legacy.directMessagesByPeerID()[oldPeerID] ?? [] == [])
#expect(legacy.directMessagesByPeerID()[newPeerID]?.map(\.id) == ["coord-mig-1"])
}
// MARK: - Public mirroring (migration step 3)
@Test("public messages mirror into Legacy via the coalesced flush")
@MainActor
func publicMessagesMirrorIntoLegacy() async {
let (viewModel, store, legacy, _) = makeBridgedFixture()
viewModel.appendPublicMessage(
BitchatMessage(
id: "bridge-pub-1",
sender: "alice",
content: "hello mesh",
timestamp: Date(),
isRelay: false
),
to: .mesh
)
viewModel.appendGeohashMessageIfAbsent(
BitchatMessage(
id: "bridge-geo-1",
sender: "bob#abcd",
content: "hello geohash",
timestamp: Date(),
isRelay: false
),
toGeohash: "U4PRUYD"
)
// The new store is synchronously authoritative (geohash keys are
// normalized to lowercase).
#expect(store.conversation(for: .mesh).messages.map(\.id) == ["bridge-pub-1"])
#expect(store.conversation(for: .geohash("u4pruyd")).messages.map(\.id) == ["bridge-geo-1"])
// Legacy catches up within one coalesced flush.
let mirrored = await TestHelpers.waitUntil(
{
legacy.messages(for: .mesh).map(\.id) == ["bridge-pub-1"]
&& legacy.messages(for: .geohash("u4pruyd")).map(\.id) == ["bridge-geo-1"]
},
timeout: 1.0
)
#expect(mirrored)
}
@Test("removing a public conversation empties its Legacy mirror immediately")
@MainActor
func publicConversationRemovalClearsLegacy() async {
let (viewModel, store, legacy, _) = makeBridgedFixture()
viewModel.appendPublicMessage(
BitchatMessage(
id: "bridge-pub-2",
sender: "alice",
content: "soon gone",
timestamp: Date(),
isRelay: false
),
to: .mesh
)
let mirrored = await TestHelpers.waitUntil(
{ legacy.messages(for: .mesh).map(\.id) == ["bridge-pub-2"] },
timeout: 1.0
)
#expect(mirrored)
// Panic-style removal: Legacy must never show stale public messages.
store.removeConversation(.mesh)
#expect(legacy.messages(for: .mesh).isEmpty)
}
}