How Bitchat Handles Network Partitions and Disconnections: A Technical Deep Dive
Bitchat detects and recovers from network partitions through three independent transport stacks—Nostr, BLE, and Geohash-based relays—each using Combine publishers for real-time state monitoring, thread-safe caching, and automatic retry scheduling with queued message persistence.
The bitchat decentralized messaging protocol must remain functional despite intermittent connectivity, Tor blocking, or Bluetooth mesh fragmentation. This article examines exactly how each transport layer in permissionlesstech/bitchat implements partition detection, state buffering, and seamless recovery.
NostrTransport: Detecting Relay Partitioning
How the DM Relay Connection Is Monitored
The NostrTransport class subscribes to NostrRelayManager.shared.$isDMRelayConnected, a Combine publisher that reports Tor-protected DM relay status. When this flips to false, the transport immediately knows the Internet path is severed.
From [bitchat/Services/NostrTransport.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift), lines 62-66:
// The publisher that drives all Nostr connectivity state
relayConnectivityCancellable = NostrRelayManager.shared.$isDMRelayConnected
.receive(on: queue)
.sink { [weak self] connected in
self?.relaysConnected = connected
self?.reachablePeersDidChange()
}
The relaysConnected flag is cached on a concurrent dispatch queue, allowing synchronous reads from the main actor while updates occur asynchronously.
Message Queuing During Outages
When relaysConnected becomes false, canDeliverPromptly short-circuits and queues outbound messages. The same cancellable (lines 162-166) ensures the instant the relay reconnects, pending traffic resumes without user intervention.
Retry throttling is governed by TransportConfig.uiGeoNotesConnectivityRetrySeconds, defaulting to 3 seconds.
BLEService: Mesh Partition Detection
Periodic Peer Reconciliation
Unlike Nostr's push-based model, BLEService actively polls peer health. Every ~30 seconds, checkPeerConnectivity() queries the underlying BLEEngine for linkStates—a dictionary mapping peer IDs to connection status.
From [bitchat/Services/BLE/BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift), lines 7073-7094:
func reconcileConnectivity(now: Date, linkStates: [PeerID: BLELinkState]) {
registry.pruneStalePeers(asOf: now, linkStates: linkStates)
let changes = registry.computeConnectivityChanges()
// Emit events that trigger UI updates and message re-broadcast
blePeerConnectivitySubject.send(changes)
// Re-advertise presence when mesh health improves
if changes.newReachablePeers.isEmpty == false {
schedulePresenceBroadcast()
}
}
Any peer missing from linkStates or marked inactive is treated as disconnected. The BLEPeerRegistry maintains the source of truth, and stale peers are automatically re-advertised when the mesh recovers, triggering message re-transmission.
GeohashPresenceService: Geographic Relay Recovery
Unified Connectivity Handling
GeohashPresenceService reuses the same relayConnectivity publisher as NostrTransport, calling handleConnectivityChange() on every state transition.
From [bitchat/Services/GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift), lines 184-208:
private func handleConnectivityChange(isConnected: Bool) {
guard isConnected else {
pendingGeohashUpdates.forEach { $0.markWaitingForNetwork() }
return
}
// Connectivity restored—schedule batched retry
scheduleConnectivityRetry(
interval: TransportConfig.uiGeoNotesConnectivityRetrySeconds
) { [weak self] in
self?.flushPendingUpdates()
}
}
This ensures geohash-based location broadcasts survive temporary relay unavailability without duplicating effort or flooding the network.
UI Feedback: ConnectivityStatusBanner
Translating Low-Level State to User Action
The [ConnectivityStatusBanner.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/Components/ConnectivityStatusBanner.swift) component converts technical failures into actionable guidance:
enum ConnectivityIssue {
case bluetoothPoweredOff
case torBlocked
case relayUnreachable
static func resolve(
bluetoothState: CBManagerState,
torBlocked: Bool
) -> ConnectivityIssue? { ... }
}
The banner appears when any transport reports loss of connectivity and dismisses automatically when the respective flag recovers. This gives users immediate visibility while the protocol continues buffering outbound traffic transparently.
Key Architectural Patterns
Bitchat's partition handling rests on four foundational patterns:
- Publish-Subscribe State Propagation —
Combinepublishers eliminate polling overhead and guarantee reactive, instant UI updates - Thread-Safe Caching — Serial/concurrent queues protect
relaysConnected,reachablePeers, andBLEPeerRegistrystate without blocking the main actor - Graceful Degradation with Queuing — Messages become
QueuedAckobjects inNostrTransportrather than failing; automatic retry resumes transmission post-recovery - Centralized Configuration —
TransportConfig(uiGeoNotesConnectivityRetrySeconds,nostrReadAckInterval) provides uniform retry behavior across all transports
Practical Implementation Example
Observe and respond to connectivity changes in your own Bitchat integration:
import Combine
import Bitchat
class ResilientChatController: ObservableObject {
@Published var issue: ConnectivityIssue?
@Published var canSend = true
private var cancellables = Set<AnyCancellable>()
init(transport: NostrTransport) {
// React to Nostr relay partitioning
transport.$relaysConnected
.map { $0 ? nil : .relayUnreachable }
.assign(to: &$issue)
// React to BLE mesh health
BLEService.shared.peerConnectivityPublisher
.map { changes in
changes.disconnectedPeers.isEmpty ? nil : .localMeshDegraded
}
.assign(to: &$issue)
// Drive send button state
$issue
.map { $0 == nil }
.assign(to: &$canSend)
}
}
Summary
- Three independent transports (Nostr, BLE, Geohash) each monitor and recover from partitions autonomously
- Combine publishers provide real-time connectivity state without polling
- Thread-safe caches enable fast synchronous checks while maintaining background update safety
- Automatic message queuing prevents data loss during outages; retries resume seamlessly
- Centralized
TransportConfigcontrols retry intervals uniformly ConnectivityStatusBannersurfaces actionable feedback without interrupting protocol operation
Frequently Asked Questions
How does Bitchat prevent message loss during a network partition?
Outbound messages are converted to QueuedAck objects and held in memory. When relaysConnected or mesh connectivity restores, the retry scheduler automatically re-attempts delivery. No user action is required.
What triggers the connectivity status banner to appear?
The banner renders when ConnectivityIssue.resolve() detects CBManagerState.poweredOff, a Tor block flag, or relay unavailability from any active transport. It dismisses automatically once the underlying publisher reports restoration.
Why does BLEService use polling instead of Combine publishers?
Bluetooth Low Energy link state changes are not uniformly exposable as publisher streams across iOS versions. The checkPeerConnectivity() interval (~30s) balances timely detection with battery impact.
Can retry intervals be configured per-transport?
All transports read from TransportConfig. While there's a single source of truth, you can subclass or modify uiGeoNotesConnectivityRetrySeconds and nostrReadAckInterval to adjust behavior globally.
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 →