# How ConversationStore Manages App-Layer Conversation Context in BitChat

> Learn how BitChat's ConversationStore manages app-layer conversation context with its intent API, O(1) lookups, and automated SwiftUI publishing. Discover efficient state management.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-07-28

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/docs/CONVERSATION-STORE-DESIGN.md) at the repository root, while all runtime logic lives in [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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:

```swift
private var conversationIDsByMessageID: [String: Set<ConversationID>] = [:]

```

As implemented in [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/TransportConfig.swift) (`meshTimelineCap`, `geoTimelineCap`, and `privateChatCap`). The private method `trimIfNeeded() -> [String]` in [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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 a `ConversationID` target.
- **`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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.

```swift
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.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) is the single source of truth for all app-layer conversation context in BitChat.
- It consolidates former timeline stores and private-chat managers into one `ObservableObject` with an intent-driven API.
- **O(1) lookups** are provided by `indexByMessageID`, while `conversationIDsByMessageID` enables cross-conversation delivery status updates.
- **Cap-aware trimming** automatically enforces `TransportConfig` limits 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.