mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:05:19 +00:00
Range/robustness: store-and-forward for directed packets (15s) with flush on new links + periodic; announces: no subset + afterglow re-announce on first-seen; adaptive scanning: force ON when <=2 neighbors or recent traffic
This commit is contained in:
@@ -124,6 +124,9 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// Backpressure-aware write queue per peripheral
|
// Backpressure-aware write queue per peripheral
|
||||||
private var pendingPeripheralWrites: [String: [Data]] = [:]
|
private var pendingPeripheralWrites: [String: [Data]] = [:]
|
||||||
|
// Store-and-forward for directed messages when we have no links
|
||||||
|
// Keyed by recipient short peerID -> messageID -> (packet, enqueuedAt)
|
||||||
|
private var pendingDirectedRelays: [String: [String: (packet: BitchatPacket, enqueuedAt: Date)]] = [:]
|
||||||
|
|
||||||
// MARK: - Maintenance Timer
|
// MARK: - Maintenance Timer
|
||||||
|
|
||||||
@@ -1061,15 +1064,23 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For broadcast (no directed peer) and non-fragment, choose a subset deterministically
|
// For broadcast (no directed peer) and non-fragment, choose a subset deterministically
|
||||||
|
// Special-case announces: do NOT subset to maximize reach for presence
|
||||||
var selectedPeripheralIDs = Set(allowedPeripheralIDs)
|
var selectedPeripheralIDs = Set(allowedPeripheralIDs)
|
||||||
var selectedCentralIDs = Set(allowedCentralIDs)
|
var selectedCentralIDs = Set(allowedCentralIDs)
|
||||||
if directedOnlyPeer == nil && packet.type != MessageType.fragment.rawValue {
|
if directedOnlyPeer == nil && packet.type != MessageType.fragment.rawValue && packet.type != MessageType.announce.rawValue {
|
||||||
let kp = subsetSizeForFanout(allowedPeripheralIDs.count)
|
let kp = subsetSizeForFanout(allowedPeripheralIDs.count)
|
||||||
let kc = subsetSizeForFanout(allowedCentralIDs.count)
|
let kc = subsetSizeForFanout(allowedCentralIDs.count)
|
||||||
selectedPeripheralIDs = selectDeterministicSubset(ids: allowedPeripheralIDs, k: kp, seed: messageID)
|
selectedPeripheralIDs = selectDeterministicSubset(ids: allowedPeripheralIDs, k: kp, seed: messageID)
|
||||||
selectedCentralIDs = selectDeterministicSubset(ids: allowedCentralIDs, k: kc, seed: messageID)
|
selectedCentralIDs = selectDeterministicSubset(ids: allowedCentralIDs, k: kc, seed: messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If directed and we currently have no links to forward on, spool for a short window
|
||||||
|
if let only = directedOnlyPeer,
|
||||||
|
selectedPeripheralIDs.isEmpty && selectedCentralIDs.isEmpty,
|
||||||
|
(packet.type == MessageType.noiseEncrypted.rawValue || packet.type == MessageType.noiseHandshake.rawValue) {
|
||||||
|
spoolDirectedPacket(packet, recipientPeerID: only)
|
||||||
|
}
|
||||||
|
|
||||||
// Writes to selected connected peripherals
|
// Writes to selected connected peripherals
|
||||||
for s in states where s.isConnected {
|
for s in states where s.isConnected {
|
||||||
let pid = s.peripheral.identifier.uuidString
|
let pid = s.peripheral.identifier.uuidString
|
||||||
@@ -1087,6 +1098,43 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Directed store-and-forward
|
||||||
|
private func spoolDirectedPacket(_ packet: BitchatPacket, recipientPeerID: String) {
|
||||||
|
let msgID = makeMessageID(for: packet)
|
||||||
|
collectionsQueue.async(flags: .barrier) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
var byMsg = self.pendingDirectedRelays[recipientPeerID] ?? [:]
|
||||||
|
if byMsg[msgID] == nil {
|
||||||
|
byMsg[msgID] = (packet: packet, enqueuedAt: Date())
|
||||||
|
self.pendingDirectedRelays[recipientPeerID] = byMsg
|
||||||
|
SecureLogger.log("🧳 Spooling directed packet for \(recipientPeerID) mid=\(msgID.prefix(8))…", category: SecureLogger.session, level: .debug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushDirectedSpool() {
|
||||||
|
// Move items out and attempt broadcast; if still no links, they'll be re-spooled
|
||||||
|
let toSend: [(String, BitchatPacket)] = collectionsQueue.sync(flags: .barrier) {
|
||||||
|
var out: [(String, BitchatPacket)] = []
|
||||||
|
let now = Date()
|
||||||
|
for (recipient, dict) in pendingDirectedRelays {
|
||||||
|
var remaining: [String: (packet: BitchatPacket, enqueuedAt: Date)] = [:]
|
||||||
|
for (msgID, entry) in dict {
|
||||||
|
if now.timeIntervalSince(entry.enqueuedAt) <= TransportConfig.bleDirectedSpoolWindowSeconds {
|
||||||
|
out.append((recipient, entry.packet))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear recipient bucket; will be re-spooled if still no links
|
||||||
|
pendingDirectedRelays[recipient] = remaining
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
guard !toSend.isEmpty else { return }
|
||||||
|
for (_, packet) in toSend {
|
||||||
|
messageQueue.async { [weak self] in self?.broadcastPacket(packet) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func sendData(_ data: Data, to peripheral: CBPeripheral) {
|
private func sendData(_ data: Data, to peripheral: CBPeripheral) {
|
||||||
// Fire-and-forget: Simple send without complex fallback logic
|
// Fire-and-forget: Simple send without complex fallback logic
|
||||||
guard peripheral.state == .connected else { return }
|
guard peripheral.state == .connected else { return }
|
||||||
@@ -1479,6 +1527,14 @@ final class BLEService: NSObject {
|
|||||||
// Force send to ensure the peer receives our announce
|
// Force send to ensure the peer receives our announce
|
||||||
sendAnnounce(forceSend: true)
|
sendAnnounce(forceSend: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Afterglow: on first-seen peers, schedule a short re-announce to push presence one more hop
|
||||||
|
if isNewPeer {
|
||||||
|
let delay = Double.random(in: 0.3...0.6)
|
||||||
|
messageQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
|
self?.sendAnnounce(forceSend: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mention parsing moved to ChatViewModel
|
// Mention parsing moved to ChatViewModel
|
||||||
@@ -1892,6 +1948,11 @@ final class BLEService: NSObject {
|
|||||||
performCleanup()
|
performCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempt to flush any spooled directed messages periodically (~every 5 seconds)
|
||||||
|
if maintenanceCounter % 2 == 1 {
|
||||||
|
flushDirectedSpool()
|
||||||
|
}
|
||||||
|
|
||||||
// No rotating alias: nothing to refresh
|
// No rotating alias: nothing to refresh
|
||||||
|
|
||||||
// Reset counter to prevent overflow (every 60 seconds)
|
// Reset counter to prevent overflow (every 60 seconds)
|
||||||
@@ -1989,6 +2050,15 @@ final class BLEService: NSObject {
|
|||||||
if !self.ingressByMessageID.isEmpty {
|
if !self.ingressByMessageID.isEmpty {
|
||||||
self.ingressByMessageID = self.ingressByMessageID.filter { $0.value.timestamp >= cutoff }
|
self.ingressByMessageID = self.ingressByMessageID.filter { $0.value.timestamp >= cutoff }
|
||||||
}
|
}
|
||||||
|
// Clean expired directed spooled items
|
||||||
|
if !self.pendingDirectedRelays.isEmpty {
|
||||||
|
var cleaned: [String: [String: (packet: BitchatPacket, enqueuedAt: Date)]] = [:]
|
||||||
|
for (recipient, dict) in self.pendingDirectedRelays {
|
||||||
|
let pruned = dict.filter { now.timeIntervalSince($0.value.enqueuedAt) <= TransportConfig.bleDirectedSpoolWindowSeconds }
|
||||||
|
if !pruned.isEmpty { cleaned[recipient] = pruned }
|
||||||
|
}
|
||||||
|
self.pendingDirectedRelays = cleaned
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2000,7 +2070,13 @@ final class BLEService: NSObject {
|
|||||||
#else
|
#else
|
||||||
let active = true
|
let active = true
|
||||||
#endif
|
#endif
|
||||||
let shouldDuty = dutyEnabled && active && connectedCount > 0
|
// Force full-time scanning if we have very few neighbors or very recent traffic
|
||||||
|
let hasRecentTraffic: Bool = collectionsQueue.sync {
|
||||||
|
let cutoff = Date().addingTimeInterval(-TransportConfig.bleRecentTrafficForceScanSeconds)
|
||||||
|
return recentPacketTimestamps.contains(where: { $0 >= cutoff })
|
||||||
|
}
|
||||||
|
let forceScanOn = (connectedCount <= 2) || hasRecentTraffic
|
||||||
|
let shouldDuty = dutyEnabled && active && connectedCount > 0 && !forceScanOn
|
||||||
if shouldDuty {
|
if shouldDuty {
|
||||||
if scanDutyTimer == nil {
|
if scanDutyTimer == nil {
|
||||||
// Start timer to toggle scanning on/off
|
// Start timer to toggle scanning on/off
|
||||||
@@ -2495,6 +2571,8 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
// Send announce after subscription is confirmed (force send for new connection)
|
// Send announce after subscription is confirmed (force send for new connection)
|
||||||
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in
|
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostSubscribeAnnounceDelaySeconds) { [weak self] in
|
||||||
self?.sendAnnounce(forceSend: true)
|
self?.sendAnnounce(forceSend: true)
|
||||||
|
// Try flushing any spooled directed packets now that we have a link
|
||||||
|
self?.flushDirectedSpool()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning)
|
SecureLogger.log("⚠️ Characteristic does not support notifications", category: SecureLogger.session, level: .warning)
|
||||||
@@ -2657,6 +2735,8 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
// Send announce to the newly subscribed central after a small delay to avoid overwhelming
|
// Send announce to the newly subscribed central after a small delay to avoid overwhelming
|
||||||
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
|
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
|
||||||
self?.sendAnnounce(forceSend: true)
|
self?.sendAnnounce(forceSend: true)
|
||||||
|
// Flush any spooled directed packets now that we have a central subscribed
|
||||||
|
self?.flushDirectedSpool()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ enum TransportConfig {
|
|||||||
static let bleConnectTimeoutBackoffWindowSeconds: TimeInterval = 120.0
|
static let bleConnectTimeoutBackoffWindowSeconds: TimeInterval = 120.0
|
||||||
static let bleRecentPacketWindowSeconds: TimeInterval = 30.0
|
static let bleRecentPacketWindowSeconds: TimeInterval = 30.0
|
||||||
static let bleRecentPacketWindowMaxCount: Int = 100
|
static let bleRecentPacketWindowMaxCount: Int = 100
|
||||||
|
// Keep scanning fully ON when we saw traffic very recently
|
||||||
|
static let bleRecentTrafficForceScanSeconds: TimeInterval = 10.0
|
||||||
static let bleThreadSleepWriteShortDelaySeconds: TimeInterval = 0.05
|
static let bleThreadSleepWriteShortDelaySeconds: TimeInterval = 0.05
|
||||||
static let bleExpectedWritePerFragmentMs: Int = 8
|
static let bleExpectedWritePerFragmentMs: Int = 8
|
||||||
static let bleExpectedWriteMaxMs: Int = 2000
|
static let bleExpectedWriteMaxMs: Int = 2000
|
||||||
@@ -136,6 +138,9 @@ enum TransportConfig {
|
|||||||
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
||||||
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
|
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
|
||||||
|
|
||||||
|
// Store-and-forward for directed packets at relays
|
||||||
|
static let bleDirectedSpoolWindowSeconds: TimeInterval = 15.0
|
||||||
|
|
||||||
// Content hashing / formatting
|
// Content hashing / formatting
|
||||||
static let contentKeyPrefixLength: Int = 256
|
static let contentKeyPrefixLength: Int = 256
|
||||||
static let uiLongMessageLengthThreshold: Int = 2000
|
static let uiLongMessageLengthThreshold: Int = 2000
|
||||||
|
|||||||
Reference in New Issue
Block a user