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
@@ -42,16 +42,13 @@ private final class MockChatNostrContext: ChatNostrContext {
// Public timeline & pipeline
var messages: [BitchatMessage] = []
private(set) var pipelineResetCount = 0
private(set) var pipelineChannelUpdates: [ChannelID] = []
private(set) var pipelineFlushCount = 0
private(set) var refreshedChannels: [ChannelID?] = []
private(set) var publicSystemMessages: [String] = []
var pendingGeohashSystemMessages: [String] = []
private(set) var appendedGeohashMessages: [(message: BitchatMessage, geohash: String)] = []
private(set) var synchronizedGeohashes: [String] = []
func resetPublicMessagePipeline() { pipelineResetCount += 1 }
func updatePublicMessagePipelineChannel(_ channel: ChannelID) { pipelineChannelUpdates.append(channel) }
func flushPublicMessagePipeline() { pipelineFlushCount += 1 }
func refreshVisibleMessages(from channel: ChannelID?) { refreshedChannels.append(channel) }
func addPublicSystemMessage(_ content: String) { publicSystemMessages.append(content) }
@@ -68,8 +65,6 @@ private final class MockChatNostrContext: ChatNostrContext {
return true
}
func synchronizePublicConversationStore(forGeohash geohash: String) { synchronizedGeohashes.append(geohash) }
// Inbound public messages
private(set) var handledPublicMessages: [BitchatMessage] = []
private(set) var mentionCheckedMessageIDs: [String] = []
@@ -441,9 +436,8 @@ struct ChatNostrCoordinatorContextTests {
coordinator.subscriptions.switchLocationChannel(to: .mesh)
#expect(context.pipelineResetCount == 1)
#expect(context.pipelineFlushCount == 1)
#expect(context.activeChannel == .mesh)
#expect(context.pipelineChannelUpdates == [.mesh])
#expect(context.clearProcessedNostrEventsCount == 1)
#expect(context.refreshedChannels == [.mesh])
#expect(context.refreshTimerStopCount == 1)
@@ -578,7 +572,6 @@ struct GeoPresenceTrackerTests {
await drainMainQueue()
#expect(context.appendedGeohashMessages.isEmpty)
#expect(context.lastGeoNotificationAt["9q8yy"] == recent)
#expect(context.synchronizedGeohashes.isEmpty)
}
@Test @MainActor
@@ -605,7 +598,7 @@ struct GeoPresenceTrackerTests {
#expect(context.appendGeohashMessageIfAbsent(placeholder, toGeohash: "9q8yy"))
// Cooldown elapsed: the geohash is re-stamped and the append is
// attempted (and rejected as a duplicate, so no store sync either).
// attempted (and rejected as a duplicate, so no notification either).
let stale = Date().addingTimeInterval(-TransportConfig.uiGeoNotifyCooldownSeconds - 1)
context.lastGeoNotificationAt["9q8yy"] = stale
tracker.cooldownPerGeohash("9q8yy", content: "sampled activity", event: event)
@@ -614,7 +607,6 @@ struct GeoPresenceTrackerTests {
let stamped = try #require(context.lastGeoNotificationAt["9q8yy"])
#expect(stamped > stale)
#expect(context.appendedGeohashMessages.count == 1)
#expect(context.synchronizedGeohashes.isEmpty)
}
@Test @MainActor
func handleFavoriteNotification_persistsFavoriteAndPostsLocalNotification() async throws {
@@ -661,10 +653,9 @@ struct GeoPresenceTrackerTests {
coordinator.presence.cooldownPerGeohash("u4pruyd", content: "hello geohash", event: first)
await drainMainQueue()
// Sampled message recorded, store synced, and notification posted.
// Sampled message recorded in the store and notification posted.
#expect(context.appendedGeohashMessages.map(\.message.id) == [first.id])
#expect(context.appendedGeohashMessages.first?.message.sender == "alice#" + String(first.pubkey.suffix(4)))
#expect(context.synchronizedGeohashes == ["u4pruyd"])
#expect(context.geohashActivityNotifications.count == 1)
#expect(context.geohashActivityNotifications.first?.geohash == "u4pruyd")
#expect(context.geohashActivityNotifications.first?.bodyPreview == "hello geohash")