mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 00:25:20 +00:00
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:
@@ -2,7 +2,10 @@
|
||||
// PublicMessagePipelineTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// Tests for PublicMessagePipeline ordering and deduplication.
|
||||
// Tests for PublicMessagePipeline batching, content dedup, and per-message
|
||||
// conversation routing. Ordering and ID dedup live in the ConversationStore
|
||||
// the flush commits into (the old late-insert threshold is gone; see
|
||||
// ConversationStoreTests for ordered-insert coverage).
|
||||
//
|
||||
|
||||
import Testing
|
||||
@@ -13,14 +16,15 @@ import BitFoundation
|
||||
@MainActor
|
||||
private final class TestPipelineDelegate: PublicMessagePipelineDelegate {
|
||||
private let dedupService = MessageDeduplicationService()
|
||||
var messages: [BitchatMessage] = []
|
||||
/// Commits in arrival-at-commit order, per conversation.
|
||||
private(set) var committed: [(message: BitchatMessage, conversationID: ConversationID)] = []
|
||||
/// Message IDs the commit rejects (simulates the store's ID dedup).
|
||||
var rejectedMessageIDs: Set<String> = []
|
||||
private(set) var recordedContentKeys: [String] = []
|
||||
private(set) var batchingStates: [Bool] = []
|
||||
|
||||
func pipelineCurrentMessages(_ pipeline: PublicMessagePipeline) -> [BitchatMessage] {
|
||||
messages
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, setMessages messages: [BitchatMessage]) {
|
||||
self.messages = messages
|
||||
func messages(in conversationID: ConversationID) -> [BitchatMessage] {
|
||||
committed.filter { $0.conversationID == conversationID }.map(\.message)
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String {
|
||||
@@ -33,19 +37,37 @@ private final class TestPipelineDelegate: PublicMessagePipelineDelegate {
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
|
||||
dedupService.recordContentKey(key, timestamp: timestamp)
|
||||
recordedContentKeys.append(key)
|
||||
}
|
||||
|
||||
func pipelineTrimMessages(_ pipeline: PublicMessagePipeline) {}
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
|
||||
guard !rejectedMessageIDs.contains(message.id) else { return false }
|
||||
committed.append((message, conversationID))
|
||||
return true
|
||||
}
|
||||
|
||||
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) {}
|
||||
|
||||
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) {}
|
||||
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) {
|
||||
batchingStates.append(isBatching)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func makeMessage(id: String, content: String, timestamp: Date) -> BitchatMessage {
|
||||
BitchatMessage(
|
||||
id: id,
|
||||
sender: "A",
|
||||
content: content,
|
||||
timestamp: timestamp,
|
||||
isRelay: false
|
||||
)
|
||||
}
|
||||
|
||||
struct PublicMessagePipelineTests {
|
||||
|
||||
@Test @MainActor
|
||||
func flush_sortsByTimestamp() async {
|
||||
func flush_commitsInTimestampOrder() async {
|
||||
let pipeline = PublicMessagePipeline()
|
||||
let delegate = TestPipelineDelegate()
|
||||
pipeline.delegate = delegate
|
||||
@@ -53,26 +75,13 @@ struct PublicMessagePipelineTests {
|
||||
let earlier = Date().addingTimeInterval(-10)
|
||||
let later = Date()
|
||||
|
||||
let messageA = BitchatMessage(
|
||||
id: "a",
|
||||
sender: "A",
|
||||
content: "Later",
|
||||
timestamp: later,
|
||||
isRelay: false
|
||||
)
|
||||
let messageB = BitchatMessage(
|
||||
id: "b",
|
||||
sender: "A",
|
||||
content: "Earlier",
|
||||
timestamp: earlier,
|
||||
isRelay: false
|
||||
)
|
||||
|
||||
pipeline.enqueue(messageA)
|
||||
pipeline.enqueue(messageB)
|
||||
pipeline.enqueue(makeMessage(id: "a", content: "Later", timestamp: later), to: .mesh)
|
||||
pipeline.enqueue(makeMessage(id: "b", content: "Earlier", timestamp: earlier), to: .mesh)
|
||||
pipeline.flushIfNeeded()
|
||||
|
||||
#expect(delegate.messages.map { $0.id } == ["b", "a"])
|
||||
#expect(delegate.messages(in: .mesh).map { $0.id } == ["b", "a"])
|
||||
// Batching state wrapped the flush.
|
||||
#expect(delegate.batchingStates == [true, false])
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -82,86 +91,41 @@ struct PublicMessagePipelineTests {
|
||||
pipeline.delegate = delegate
|
||||
|
||||
let now = Date()
|
||||
let messageA = BitchatMessage(
|
||||
id: "a",
|
||||
sender: "A",
|
||||
content: "Same",
|
||||
timestamp: now,
|
||||
isRelay: false
|
||||
)
|
||||
let messageB = BitchatMessage(
|
||||
id: "b",
|
||||
sender: "A",
|
||||
content: "Same",
|
||||
timestamp: now.addingTimeInterval(0.2),
|
||||
isRelay: false
|
||||
)
|
||||
|
||||
pipeline.enqueue(messageA)
|
||||
pipeline.enqueue(messageB)
|
||||
pipeline.enqueue(makeMessage(id: "a", content: "Same", timestamp: now), to: .mesh)
|
||||
pipeline.enqueue(makeMessage(id: "b", content: "Same", timestamp: now.addingTimeInterval(0.2)), to: .mesh)
|
||||
pipeline.flushIfNeeded()
|
||||
|
||||
#expect(delegate.messages.count == 1)
|
||||
#expect(delegate.messages.first?.content == "Same")
|
||||
#expect(delegate.messages(in: .mesh).count == 1)
|
||||
#expect(delegate.messages(in: .mesh).first?.content == "Same")
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func lateInsert_meshAppendsRecentOlderMessage() async {
|
||||
func flush_routesEachMessageToItsConversation() async {
|
||||
let pipeline = PublicMessagePipeline()
|
||||
let delegate = TestPipelineDelegate()
|
||||
pipeline.delegate = delegate
|
||||
pipeline.updateActiveChannel(.mesh)
|
||||
|
||||
let base = Date()
|
||||
let newer = BitchatMessage(
|
||||
id: "new",
|
||||
sender: "A",
|
||||
content: "New",
|
||||
timestamp: base,
|
||||
isRelay: false
|
||||
)
|
||||
let older = BitchatMessage(
|
||||
id: "old",
|
||||
sender: "A",
|
||||
content: "Old",
|
||||
timestamp: base.addingTimeInterval(-5),
|
||||
isRelay: false
|
||||
)
|
||||
|
||||
delegate.messages = [newer]
|
||||
pipeline.enqueue(older)
|
||||
pipeline.enqueue(makeMessage(id: "mesh-1", content: "mesh hello", timestamp: base), to: .mesh)
|
||||
// A channel switch mid-batch must not misroute already-buffered messages.
|
||||
pipeline.enqueue(makeMessage(id: "geo-1", content: "geo hello", timestamp: base.addingTimeInterval(1)), to: .geohash("u4pruydq"))
|
||||
pipeline.flushIfNeeded()
|
||||
|
||||
#expect(delegate.messages.map { $0.id } == ["new", "old"])
|
||||
#expect(delegate.messages(in: .mesh).map { $0.id } == ["mesh-1"])
|
||||
#expect(delegate.messages(in: .geohash("u4pruydq")).map { $0.id } == ["geo-1"])
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func lateInsert_locationInsertsByTimestamp() async {
|
||||
func flush_rejectedCommitDoesNotRecordContentKey() async {
|
||||
let pipeline = PublicMessagePipeline()
|
||||
let delegate = TestPipelineDelegate()
|
||||
pipeline.delegate = delegate
|
||||
pipeline.updateActiveChannel(.location(GeohashChannel(level: .city, geohash: "u4pruydq")))
|
||||
delegate.rejectedMessageIDs = ["dup"]
|
||||
|
||||
let base = Date()
|
||||
let newer = BitchatMessage(
|
||||
id: "new",
|
||||
sender: "A",
|
||||
content: "New",
|
||||
timestamp: base,
|
||||
isRelay: false
|
||||
)
|
||||
let older = BitchatMessage(
|
||||
id: "old",
|
||||
sender: "A",
|
||||
content: "Old",
|
||||
timestamp: base.addingTimeInterval(-5),
|
||||
isRelay: false
|
||||
)
|
||||
|
||||
delegate.messages = [newer]
|
||||
pipeline.enqueue(older)
|
||||
pipeline.enqueue(makeMessage(id: "dup", content: "already stored", timestamp: Date()), to: .mesh)
|
||||
pipeline.flushIfNeeded()
|
||||
|
||||
#expect(delegate.messages.map { $0.id } == ["old", "new"])
|
||||
#expect(delegate.messages(in: .mesh).isEmpty)
|
||||
#expect(delegate.recordedContentKeys.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user