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
+94
View File
@@ -468,4 +468,98 @@ struct ConversationStoreTests {
store.append(makeMessage(id: "m3", timestamp: 3), to: b)
#expect(bWillChangeCount > 0)
}
// MARK: - Public timelines (mesh/geohash, ex-PublicTimelineStore behavior)
@Test("geohash conversations are separated by geohash and from mesh")
@MainActor
func geohashConversationSeparation() {
let store = ConversationStore()
store.append(makeMessage(id: "mesh-1", timestamp: 1), to: .mesh)
store.append(makeMessage(id: "geo-a-1", timestamp: 2), to: .geohash("u4pruyd"))
store.append(makeMessage(id: "geo-b-1", timestamp: 3), to: .geohash("9q8yy"))
#expect(store.conversation(for: .mesh).messages.map(\.id) == ["mesh-1"])
#expect(store.conversation(for: .geohash("u4pruyd")).messages.map(\.id) == ["geo-a-1"])
#expect(store.conversation(for: .geohash("9q8yy")).messages.map(\.id) == ["geo-b-1"])
}
@Test("geohash append dedups by ID and reports duplicates")
@MainActor
func geohashAppendIfAbsentContract() {
let store = ConversationStore()
let message = makeMessage(id: "geo-1", timestamp: 1)
#expect(store.append(message, to: .geohash("u4pruyd")))
#expect(!store.append(message, to: .geohash("u4pruyd")))
// The same ID is still fresh in a different geohash.
#expect(store.append(message, to: .geohash("9q8yy")))
}
@Test("removePublicMessage searches mesh and geohash conversations only")
@MainActor
func removePublicMessageSearchesPublicConversations() {
let store = ConversationStore()
let direct = makeDirectConversationID("aa")
store.append(makeMessage(id: "mesh-1", timestamp: 1), to: .mesh)
store.append(makeMessage(id: "geo-1", timestamp: 2), to: .geohash("u4pruyd"))
store.append(makeMessage(id: "dm-1", timestamp: 3, isPrivate: true), to: direct)
#expect(store.removePublicMessage(withID: "geo-1")?.id == "geo-1")
#expect(store.conversation(for: .geohash("u4pruyd")).messages.isEmpty)
#expect(store.removePublicMessage(withID: "mesh-1")?.id == "mesh-1")
#expect(store.conversation(for: .mesh).messages.isEmpty)
// Direct conversations are never touched.
#expect(store.removePublicMessage(withID: "dm-1") == nil)
#expect(store.conversation(for: direct).messages.map(\.id) == ["dm-1"])
}
@Test("removeMessages(from:where:) purges matches and emits per removal")
@MainActor
func removeMessagesByPredicate() {
let store = ConversationStore()
let id = ConversationID.geohash("u4pruyd")
store.append(makeMessage(id: "keep-1", timestamp: 1), to: id)
store.append(makeMessage(id: "drop-1", timestamp: 2, content: "purge me"), to: id)
store.append(makeMessage(id: "drop-2", timestamp: 3, content: "purge me"), to: id)
store.append(makeMessage(id: "keep-2", timestamp: 4), to: id)
var removedIDs: [String] = []
var cancellables = Set<AnyCancellable>()
store.changes
.sink { change in
if case .messageRemoved(_, let messageID) = change {
removedIDs.append(messageID)
}
}
.store(in: &cancellables)
store.removeMessages(from: id, where: { $0.content == "purge me" })
#expect(store.conversation(for: id).messages.map(\.id) == ["keep-1", "keep-2"])
#expect(removedIDs == ["drop-1", "drop-2"])
// The ID index survives the purge: dedup and removal still work.
#expect(!store.append(makeMessage(id: "keep-2", timestamp: 4), to: id))
#expect(store.removeMessage(withID: "keep-1", from: id) != nil)
#expect(store.conversation(for: id).messages.map(\.id) == ["keep-2"])
}
@Test("trimmed public message IDs can return after falling off the cap")
@MainActor
func trimmedMessageIDsCanReturn() {
let store = ConversationStore()
let id = ConversationID.geohash("u4pruyd")
let conversation = store.conversation(for: id)
let first = makeMessage(id: "one", timestamp: 1)
store.append(first, to: id)
for index in 0..<conversation.cap {
store.append(makeMessage(id: "filler-\(index)", timestamp: 2 + TimeInterval(index)), to: id)
}
// "one" was trimmed by the cap, so its ID is free again.
#expect(!conversation.containsMessage(withID: "one"))
#expect(store.append(makeMessage(id: "one", timestamp: 2000), to: id))
}
}