How ConversationStore Manages App-Layer Conversation Context in BitChat
BitChat's ConversationStore is a single ObservableObject that acts as the sole source of truth for all app-layer conversation state, funneling every mutation through an intent API while maintaining O(1) message lookups, cap-aware trimming, and automated SwiftUI publishing.
The ConversationStore replaces BitChat's former network of timeline stores and private-chat managers with one centralized, observable model. According to the permissionlesstech/bitchat source code, every message append, delivery status update, and conversation migration flows through this store, eliminating duplicated state and expensive full-dictionary rebuilds. The high-level rationale and migration steps are documented in docs/CONVERSATION-STORE-DESIGN.md at the repository root, while all runtime logic lives in bitchat/App/ConversationStore.swift.
Core Architecture of ConversationStore
The ConversationStore architecture is built around a single writer, multiple readers, and fast index-based access.
Single Source of Truth
In bitchat/App/ConversationStore.swift (lines 30-38), ConversationStore is declared as final class ConversationStore: ObservableObject. This design consolidates every piece of app-layer conversation state into one component. Previously, BitChat relied on separate timeline stores and bridge code; now all mutations go through the store's public intent methods.
Conversation Objects and Incremental Indexing
Each conversation is represented by a final class Conversation: ObservableObject, Identifiable defined in the same file (lines 25-34). Every Conversation maintains its own @Published messages array and tracks isUnread state.
To enable O(1) deduplication and status look-ups, each conversation uses a private dictionary:
private var indexByMessageID: [String: Int] = [:]
This incremental index maps message IDs to logical array positions. Helper methods such as physicalIndex, insert, and upsert leverage this map to avoid linear scans when updating or appending messages in the conversation timeline.
Store-Level Message-ID Mapping
A second dictionary enables delivery-receipt updates without knowing the target conversation upfront:
private var conversationIDsByMessageID: [String: Set<ConversationID>] = [:]
As implemented in bitchat/App/ConversationStore.swift (lines 50-62), the store keeps this map exact at every mutation through registerMessageID and unregisterMessageID. When a message appears in multiple conversations—such as mirrored private chats—the set of ConversationID values ensures the status is fanned out correctly.
Cap-Aware Trimming
Each conversation enforces retention limits defined in bitchat/App/TransportConfig.swift (meshTimelineCap, geoTimelineCap, and privateChatCap). The private method trimIfNeeded() -> [String] in bitchat/App/ConversationStore.swift (lines 92-102) evaluates the current message count against these limits. When a cap is exceeded, the oldest messages are removed and the index offset is adjusted to maintain logical consistency.
Intent API and State Mutations
All writes are funneled through the store's intent API, which enforces invariants and emits ConversationChange events for observers. This pattern centralizes validation and guarantees that every mutation updates both the conversation timelines and the global message-ID index atomically.
Appending and Upserting Messages
The store exposes type-safe methods for adding content:
append(_:to:)– Adds a new message to aConversationIDtarget.upsertByID(_:in:)– Inserts or updates a message by its identifier.
These methods live in the intent API excerpt at lines 90-110 of bitchat/App/ConversationStore.swift. Because writes are centralized, the store can update indexByMessageID, check caps, and publish changes in a single atomic step.
Delivery Status Handling
Delivery receipts are applied per-conversation and never downgraded. The setDeliveryStatus(_:forMessageID:) method fans out updates to every conversation containing the message ID. As shown in the delivery status flow (lines 20-28), the helper applyDeliveryStatus ensures consistency across mirrored private chats while respecting the store's monotonic status constraint.
Conversation Migration
Private-chat hand-offs between ephemeral and stable peer IDs are handled by migrateConversation(from:to:). This method moves messages from a source Conversation into a destination, preserving order, deduplication state, unread flags, and the user's current selection. The implementation in bitchat/App/ConversationStore.swift (lines 77-85) performs the migration atomically so that UI layers observe a single coherent change event.
UI Selection and Observable State
The store owns the UI selection axes and derives the active conversation identity automatically. This eliminates the need for separate view-model state and ensures that channel switches and peer selections propagate through a single observable pipeline.
Active Channel and Private Peer Selection
Published properties in bitchat/App/ConversationStore.swift (lines 44-62) include:
@Published private(set) var activeChannel@Published private(set) var selectedPrivatePeerID@Published private(set) var selectedConversationID
Mutator methods such as setActiveChannel and setSelectedPrivatePeer trigger refreshDerivedSelection(), which recalculates selectedConversationID. This derivation keeps SwiftUI views synchronized without redundant state in view models.
Automatic Change Propagation
Because both ConversationStore and each Conversation conform to ObservableObject, @Published mutations emit changes that SwiftUI consumes directly. The ConversationChange events generated by the intent API give view models fine-grained signals to update lists, badges, and status indicators without full-store rebuilds.
Diagnostics and Invariant Auditing
The store provides auditInvariants() -> [String] to verify runtime consistency. This diagnostic method, defined in bitchat/App/ConversationStore.swift (lines 106-126), walks the entire store and validates structural guarantees across:
- The consistency of the conversation list.
- Per-conversation internal invariants.
- The accuracy of the message-ID mapping.
- The unread set and selection state.
Running this audit during development catches desynchronization between the messages arrays and the indexed lookups before they reach production.
Working with ConversationStore: Practical Examples
The following Swift snippets demonstrate common operations against the store. Each call triggers the appropriate ConversationChange events and updates SwiftUI views automatically.
import BitFoundation
let store = ConversationStore()
// 1️⃣ Append a new message to the mesh (public) conversation
let msg = BitchatMessage(id: UUID().uuidString,
timestamp: Date(),
sender: myPeerID,
payload: .text("Hello world"))
store.append(msg, to: .mesh)
// 2️⃣ Update delivery status across all conversations containing the message
store.setDeliveryStatus(.delivered, forMessageID: msg.id)
// 3️⃣ Open a private chat with a specific peer
store.setSelectedPrivatePeer(otherPeerID)
// 4️⃣ Mark a conversation as read or unread
store.markUnread(.directPeer(otherPeerID))
store.markRead(.directPeer(otherPeerID))
// 5️⃣ Migrate a direct conversation from an ephemeral to a stable peer ID
store.migrateConversation(from: .directPeer(oldPeerID),
to: .directPeer(newPeerID))
Summary
- ConversationStore in
bitchat/App/ConversationStore.swiftis the single source of truth for all app-layer conversation context in BitChat. - It consolidates former timeline stores and private-chat managers into one
ObservableObjectwith an intent-driven API. - O(1) lookups are provided by
indexByMessageID, whileconversationIDsByMessageIDenables cross-conversation delivery status updates. - Cap-aware trimming automatically enforces
TransportConfiglimits per conversation type. - Selection state (
activeChannel,selectedPrivatePeerID) is derived and published directly from the store, keeping SwiftUI reactive without duplicated view-model state. auditInvariants()offers a full consistency check for debugging and development validation.
Frequently Asked Questions
What is the primary role of ConversationStore in BitChat?
ConversationStore serves as the exclusive owner of app-layer conversation state. According to the permissionlesstech/bitchat source code, it replaces multiple fragmented stores with a single ObservableObject that both writes and reads every message timeline. All UI components observe this store or its nested Conversation objects to render chat content.
How does ConversationStore perform message deduplication?
Deduplication relies on private var indexByMessageID: [String: Int] inside each Conversation. When a message is appended or upserted, the store checks this dictionary in O(1) time to determine if the ID already exists. This avoids linear array scans and prevents duplicate entries in the timeline.
What happens when a conversation exceeds its message cap?
The trimIfNeeded() method evaluates the current count against limits such as meshTimelineCap, geoTimelineCap, or privateChatCap. If the limit is exceeded, the oldest messages are removed and the internal index offset is adjusted. The method returns the trimmed message IDs so the store can clean up any global references.
How does ConversationStore support SwiftUI reactivity?
Both ConversationStore and Conversation conform to ObservableObject and expose @Published properties. When the intent API mutates state—whether appending a message, updating delivery status, or changing selection—the underlying publishers emit changes that SwiftUI views observe directly. This eliminates manual reload logic and ensures the UI stays in sync with the app-layer conversation context.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →