mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 23:45:19 +00:00
Cut private message path over to ConversationStore
All private-message mutations now flow through store intents: coordinators, PrivateChatManager (its @Published dicts deleted - now read-only views over the store), outbound sends, delivery status, and chat migration. The O(1) store dedup replaces the full-scan duplicate check; insertion order is maintained by the store so sanitizeChat's re-sort is a documented no-op. Both bootstrapper Combine bridges and the Task.yield store synchronization are deleted. ChatViewModel.privateChats/unreadPrivateMessages become get-only derived views (measured: naive rebuild equals a change-invalidated cache within noise, so the simpler form stays). Feature models still read the legacy store, fed by a coalescing LegacyConversationStoreBridge (one mirror per burst, marked for step-5 deletion). pipeline.privateIngest: 9.6k -> 14.7k msg/s (+53%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// bitchatTests
|
||||
//
|
||||
// Tests for PrivateChatManager read receipt and selection behavior.
|
||||
// Message storage lives in the single-writer ConversationStore; the
|
||||
// manager's privateChats/unreadMessages are derived views over it.
|
||||
//
|
||||
|
||||
import Testing
|
||||
@@ -12,13 +14,20 @@ import BitFoundation
|
||||
|
||||
struct PrivateChatManagerTests {
|
||||
|
||||
@MainActor
|
||||
private static func makeManager(transport: MockTransport) -> (PrivateChatManager, ConversationStore) {
|
||||
let store = ConversationStore()
|
||||
let manager = PrivateChatManager(meshService: transport, conversationStore: store)
|
||||
return (manager, store)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func startChat_setsSelectedAndClearsUnread() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let peerID = PeerID(str: "00000000000000AA")
|
||||
|
||||
manager.privateChats[peerID] = [
|
||||
store.append(
|
||||
BitchatMessage(
|
||||
id: "pm-1",
|
||||
sender: "Peer",
|
||||
@@ -28,9 +37,10 @@ struct PrivateChatManagerTests {
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
)
|
||||
]
|
||||
manager.unreadMessages.insert(peerID)
|
||||
),
|
||||
to: .directPeer(peerID)
|
||||
)
|
||||
store.markUnread(.directPeer(peerID))
|
||||
|
||||
manager.startChat(with: peerID)
|
||||
|
||||
@@ -43,13 +53,13 @@ struct PrivateChatManagerTests {
|
||||
func markAsRead_sendsReadReceiptViaRouter() async {
|
||||
let transport = MockTransport()
|
||||
let router = MessageRouter(transports: [transport])
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
manager.messageRouter = router
|
||||
|
||||
let peerID = PeerID(str: "00000000000000BB")
|
||||
transport.reachablePeers.insert(peerID)
|
||||
|
||||
manager.privateChats[peerID] = [
|
||||
store.append(
|
||||
BitchatMessage(
|
||||
id: "pm-2",
|
||||
sender: "Peer",
|
||||
@@ -59,9 +69,10 @@ struct PrivateChatManagerTests {
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
)
|
||||
]
|
||||
manager.unreadMessages.insert(peerID)
|
||||
),
|
||||
to: .directPeer(peerID)
|
||||
)
|
||||
store.markUnread(.directPeer(peerID))
|
||||
|
||||
manager.markAsRead(from: peerID)
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
@@ -74,10 +85,10 @@ struct PrivateChatManagerTests {
|
||||
@Test @MainActor
|
||||
func markAsRead_withoutRouterFallsBackToTransport() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let peerID = PeerID(str: "00000000000000CC")
|
||||
|
||||
manager.privateChats[peerID] = [
|
||||
store.append(
|
||||
BitchatMessage(
|
||||
id: "pm-fallback",
|
||||
sender: "Peer",
|
||||
@@ -87,8 +98,9 @@ struct PrivateChatManagerTests {
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
)
|
||||
]
|
||||
),
|
||||
to: .directPeer(peerID)
|
||||
)
|
||||
|
||||
manager.markAsRead(from: peerID)
|
||||
|
||||
@@ -99,7 +111,7 @@ struct PrivateChatManagerTests {
|
||||
@Test @MainActor
|
||||
func consolidateMessages_mergesStableNoiseKeyHistoryAndMarksUnread() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let identityManager = MockIdentityManager(MockKeychain())
|
||||
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
||||
let unifiedPeerService = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identityManager)
|
||||
@@ -120,7 +132,7 @@ struct PrivateChatManagerTests {
|
||||
])
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
manager.privateChats[stablePeerID] = [
|
||||
store.append(
|
||||
BitchatMessage(
|
||||
id: "stable-msg",
|
||||
sender: "Alice",
|
||||
@@ -130,9 +142,10 @@ struct PrivateChatManagerTests {
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: stablePeerID
|
||||
)
|
||||
]
|
||||
manager.unreadMessages.insert(stablePeerID)
|
||||
),
|
||||
to: .directPeer(stablePeerID)
|
||||
)
|
||||
store.markUnread(.directPeer(stablePeerID))
|
||||
|
||||
let hadUnread = manager.consolidateMessages(for: peerID, peerNickname: "Alice", persistedReadReceipts: [])
|
||||
|
||||
@@ -146,11 +159,11 @@ struct PrivateChatManagerTests {
|
||||
@Test @MainActor
|
||||
func consolidateMessages_movesTemporaryGeoDMHistoryByNickname() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let peerID = PeerID(str: "0011223344556677")
|
||||
let tempPeerID = PeerID(nostr_: "0000000000000000000000000000000000000000000000000000000000000042")
|
||||
|
||||
manager.privateChats[tempPeerID] = [
|
||||
store.append(
|
||||
BitchatMessage(
|
||||
id: "geo-msg",
|
||||
sender: "Alice",
|
||||
@@ -160,9 +173,10 @@ struct PrivateChatManagerTests {
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: tempPeerID
|
||||
)
|
||||
]
|
||||
manager.unreadMessages.insert(tempPeerID)
|
||||
),
|
||||
to: .directPeer(tempPeerID)
|
||||
)
|
||||
store.markUnread(.directPeer(tempPeerID))
|
||||
|
||||
let hadUnread = manager.consolidateMessages(for: peerID, peerNickname: "alice", persistedReadReceipts: [])
|
||||
|
||||
@@ -177,10 +191,10 @@ struct PrivateChatManagerTests {
|
||||
@Test @MainActor
|
||||
func syncReadReceiptsForSentMessages_onlyCopiesDeliveredAndRead() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let peerID = PeerID(str: "00000000000000DD")
|
||||
|
||||
manager.privateChats[peerID] = [
|
||||
let seeded = [
|
||||
BitchatMessage(
|
||||
id: "sent-read",
|
||||
sender: "Me",
|
||||
@@ -215,6 +229,9 @@ struct PrivateChatManagerTests {
|
||||
deliveryStatus: .failed(reason: "nope")
|
||||
)
|
||||
]
|
||||
for message in seeded {
|
||||
store.append(message, to: .directPeer(peerID))
|
||||
}
|
||||
|
||||
var externalReceipts = Set<String>()
|
||||
manager.syncReadReceiptsForSentMessages(peerID: peerID, nickname: "Me", externalReceipts: &externalReceipts)
|
||||
@@ -223,47 +240,36 @@ struct PrivateChatManagerTests {
|
||||
#expect(manager.sentReadReceipts == Set(["sent-read", "sent-delivered"]))
|
||||
}
|
||||
|
||||
/// The store replaces `sanitizeChat`: inserts keep chronological order,
|
||||
/// duplicate IDs are rejected on append, and `upsertByID` replaces the
|
||||
/// stored message with the latest copy in place.
|
||||
@Test @MainActor
|
||||
func sanitizeChat_sortsChronologicallyAndKeepsLatestDuplicate() async {
|
||||
func store_keepsChronologicalOrderAndDedupsByID() async {
|
||||
let transport = MockTransport()
|
||||
let manager = PrivateChatManager(meshService: transport)
|
||||
let (manager, store) = Self.makeManager(transport: transport)
|
||||
let peerID = PeerID(str: "00000000000000EE")
|
||||
let base = Date(timeIntervalSince1970: 10)
|
||||
|
||||
manager.privateChats[peerID] = [
|
||||
func message(_ id: String, _ content: String, offset: TimeInterval) -> BitchatMessage {
|
||||
BitchatMessage(
|
||||
id: "same",
|
||||
id: id,
|
||||
sender: "Peer",
|
||||
content: "Older",
|
||||
timestamp: base.addingTimeInterval(10),
|
||||
isRelay: false,
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
),
|
||||
BitchatMessage(
|
||||
id: "first",
|
||||
sender: "Peer",
|
||||
content: "First",
|
||||
timestamp: base,
|
||||
isRelay: false,
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
),
|
||||
BitchatMessage(
|
||||
id: "same",
|
||||
sender: "Peer",
|
||||
content: "Newest",
|
||||
timestamp: base.addingTimeInterval(20),
|
||||
content: content,
|
||||
timestamp: base.addingTimeInterval(offset),
|
||||
isRelay: false,
|
||||
isPrivate: true,
|
||||
recipientNickname: "Me",
|
||||
senderPeerID: peerID
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
manager.sanitizeChat(for: peerID)
|
||||
#expect(store.append(message("same", "Older", offset: 10), to: .directPeer(peerID)))
|
||||
// Out-of-order arrival is inserted in timestamp order.
|
||||
#expect(store.append(message("first", "First", offset: 0), to: .directPeer(peerID)))
|
||||
// Duplicate ID is rejected on append…
|
||||
#expect(!store.append(message("same", "Newest", offset: 20), to: .directPeer(peerID)))
|
||||
// …and replaced in place by upsert.
|
||||
store.upsertByID(message("same", "Newest", offset: 20), in: .directPeer(peerID))
|
||||
|
||||
#expect(manager.privateChats[peerID]?.map(\.id) == ["first", "same"])
|
||||
#expect(manager.privateChats[peerID]?.last?.content == "Newest")
|
||||
|
||||
Reference in New Issue
Block a user