mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 05:45:19 +00:00
Wire gateway/bridge/panic features to capability ports, not BLEService
App wiring discovered mesh-only features by casting the Transport to the concrete BLEService class in nine places. Those surfaces are now three capability protocols — BluetoothStateReporting, PanicResettingTransport, and MeshBridgingTransport — discovered with as? like any optional capability, so the bootstrapper, panic flow, and lifecycle coordinator no longer name the concrete transport at all. A future second mesh transport picks up gateway/bridge wiring and the panic lifecycle by conforming, and the remaining Transport god-protocol requirements can migrate to the same pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import BitFoundation
|
||||
import CoreBluetooth
|
||||
import Foundation
|
||||
|
||||
/// Optional transport capabilities, discovered with `as?` instead of casting
|
||||
/// to a concrete transport class. `Transport` stays the contract every
|
||||
/// transport genuinely implements; a capability protocol here is the
|
||||
/// contract for one mesh-only feature surface, so app wiring depends on the
|
||||
/// feature it needs rather than on `BLEService` itself.
|
||||
|
||||
/// Radio-state reporting for transports backed by a local radio.
|
||||
protocol BluetoothStateReporting: AnyObject {
|
||||
func getCurrentBluetoothState() -> CBManagerState
|
||||
}
|
||||
|
||||
/// Panic-mode lifecycle for transports that own durable identity state.
|
||||
/// A transport implementing this owns its own restart sequencing:
|
||||
/// `completePanicReset` decides whether services come back, so generic
|
||||
/// `startServices()` calls after a panic belong only to transports that
|
||||
/// don't implement it.
|
||||
protocol PanicResettingTransport: AnyObject {
|
||||
/// Quiesces the radio and drains in-flight work ahead of a panic wipe.
|
||||
func suspendForPanicReset()
|
||||
/// Finishes a panic wipe, optionally restarting services.
|
||||
func completePanicReset(restartServices: Bool)
|
||||
/// Rotates the transport identity as part of a panic reset.
|
||||
func resetIdentityForPanic(currentNickname: String, restartServices: Bool)
|
||||
}
|
||||
|
||||
/// Internet-gateway and geohash-bridge wiring surface (BLE mesh today).
|
||||
/// Everything the gateway/bridge/courier services need from the mesh
|
||||
/// transport, so their bootstrap wiring never touches the concrete class.
|
||||
protocol MeshBridgingTransport: AnyObject {
|
||||
// Runtime-advertised capability bits
|
||||
func setLocalCapability(_ capability: PeerCapabilities, enabled: Bool)
|
||||
func setLocalBridgeGeohash(_ cell: String?)
|
||||
func advertisedBridgeGeohash() -> String?
|
||||
|
||||
// Peers currently advertising bridging roles
|
||||
func reachableGatewayPeers() -> [PeerID]
|
||||
func reachableBridgePeers() -> [PeerID]
|
||||
|
||||
// Gateway carrier packets (mesh <-> Nostr uplink/downlink)
|
||||
@discardableResult
|
||||
func sendNostrCarrier(_ payload: Data, to gatewayPeer: PeerID) -> Bool
|
||||
func broadcastNostrCarrier(_ payload: Data)
|
||||
/// Sink for received carrier packets (set once by app wiring; called on
|
||||
/// the main actor after transport-level checks).
|
||||
var onNostrCarrierPacket: (@MainActor (_ payload: Data, _ from: PeerID, _ directedToUs: Bool) -> Void)? { get set }
|
||||
|
||||
// Bridge courier drops (sealed envelopes carried across the bridge)
|
||||
func sealBridgeCourierEnvelope(_ content: String, messageID: String, recipientNoiseKey: Data) -> CourierEnvelope?
|
||||
@discardableResult
|
||||
func openBridgedCourierEnvelope(_ envelope: CourierEnvelope) -> Bool
|
||||
@discardableResult
|
||||
func deliverBridgedEnvelope(_ envelope: CourierEnvelope, to peerID: PeerID) -> Bool
|
||||
func myNoiseStaticPublicKey() -> Data
|
||||
func verifiedPeersWithNoiseKeys() -> [(peerID: PeerID, noiseKey: Data)]
|
||||
/// Fired (off-main) when a signature-verified announce is processed.
|
||||
var onVerifiedPeerAnnounce: ((_ peerID: PeerID) -> Void)? { get set }
|
||||
}
|
||||
@@ -450,3 +450,6 @@ extension BitchatDelegate {
|
||||
}
|
||||
|
||||
extension BLEService: Transport {}
|
||||
extension BLEService: BluetoothStateReporting {}
|
||||
extension BLEService: PanicResettingTransport {}
|
||||
extension BLEService: MeshBridgingTransport {}
|
||||
|
||||
@@ -106,8 +106,8 @@ extension ChatViewModel: ChatLifecycleContext {
|
||||
}
|
||||
|
||||
func refreshBluetoothState() {
|
||||
if let bleService = meshService as? BLEService {
|
||||
updateBluetoothState(bleService.getCurrentBluetoothState())
|
||||
if let radio = meshService as? BluetoothStateReporting {
|
||||
updateBluetoothState(radio.getCurrentBluetoothState())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1564,8 +1564,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
|
||||
// Quiesce the mesh before clearing stores. Identity replacement below
|
||||
// deliberately stays stopped until media deletion and marker commit.
|
||||
if let bleService = meshService as? BLEService {
|
||||
bleService.suspendForPanicReset()
|
||||
if let panicTransport = meshService as? PanicResettingTransport {
|
||||
panicTransport.suspendForPanicReset()
|
||||
} else {
|
||||
meshService.emergencyDisconnectAll()
|
||||
}
|
||||
@@ -1700,8 +1700,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
|
||||
// Replace the BLE identity while keeping the radio stopped. It may
|
||||
// reopen only after the durable panic transaction commits.
|
||||
if let bleService = meshService as? BLEService {
|
||||
bleService.resetIdentityForPanic(
|
||||
if let panicTransport = meshService as? PanicResettingTransport {
|
||||
panicTransport.resetIdentityForPanic(
|
||||
currentNickname: nickname,
|
||||
restartServices: false
|
||||
)
|
||||
@@ -1746,18 +1746,19 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
|
||||
guard panicCompleted else { return false }
|
||||
|
||||
if let bleService = meshService as? BLEService {
|
||||
if let panicTransport = meshService as? PanicResettingTransport {
|
||||
// Startup recovery reopens admission but leaves actual service
|
||||
// start to the bootstrapper immediately after this method.
|
||||
bleService.completePanicReset(
|
||||
panicTransport.completePanicReset(
|
||||
restartServices: restartServices
|
||||
)
|
||||
}
|
||||
|
||||
if restartServices {
|
||||
// All persistent state and media are gone. Bring each service back
|
||||
// only now, under the new identity.
|
||||
if !(meshService is BLEService) {
|
||||
// only now, under the new identity — a panic-resetting transport
|
||||
// owns its own restart sequencing above.
|
||||
if !(meshService is PanicResettingTransport) {
|
||||
meshService.startServices()
|
||||
}
|
||||
|
||||
|
||||
@@ -195,9 +195,8 @@ private extension ChatViewModelBootstrapper {
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak viewModel] in
|
||||
guard let viewModel,
|
||||
let bleService = viewModel.meshService as? BLEService else { return }
|
||||
let state = bleService.getCurrentBluetoothState()
|
||||
viewModel.updateBluetoothState(state)
|
||||
let radio = viewModel.meshService as? BluetoothStateReporting else { return }
|
||||
viewModel.updateBluetoothState(radio.getCurrentBluetoothState())
|
||||
}
|
||||
|
||||
viewModel.nostrRelayManager = NostrRelayManager.shared
|
||||
@@ -331,7 +330,7 @@ private extension ChatViewModelBootstrapper {
|
||||
func configureGateway() {
|
||||
// Gateway mode bridges BLE mesh <-> Nostr; a mock transport (tests)
|
||||
// has no carrier packets to bridge.
|
||||
guard let bleService = viewModel.meshService as? BLEService else { return }
|
||||
guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return }
|
||||
let gateway = GatewayService.shared
|
||||
|
||||
gateway.publishToRelays = { event, geohash in
|
||||
@@ -410,7 +409,7 @@ private extension ChatViewModelBootstrapper {
|
||||
/// transport, the relay manager, location, and the public timeline. Same
|
||||
/// closure-injection style as `configureGateway`.
|
||||
func configureBridge() {
|
||||
guard let bleService = viewModel.meshService as? BLEService else { return }
|
||||
guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return }
|
||||
let bridge = BridgeService.shared
|
||||
let idBridge = viewModel.idBridge
|
||||
|
||||
@@ -545,7 +544,7 @@ private extension ChatViewModelBootstrapper {
|
||||
/// manager, the mesh transport's sealing/opening primitives, the courier
|
||||
/// store, and the message router's deposit path.
|
||||
func configureBridgeCourier() {
|
||||
guard let bleService = viewModel.meshService as? BLEService else { return }
|
||||
guard let bleService = viewModel.meshService as? MeshBridgingTransport else { return }
|
||||
let courier = BridgeCourierService.shared
|
||||
|
||||
courier.bridgeEnabled = { BridgeService.shared.isEnabled }
|
||||
|
||||
Reference in New Issue
Block a user