Merge 929a86ce2ba882d59209783335291b832e5dd153 into 9b84b361225facd8e623f25d76f889d3dc54a879

This commit is contained in:
krish rathi 2026-08-29 23:01:53 +05:30 committed by GitHub
commit 05d6f51885
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 112 additions and 8 deletions

View File

@ -8,6 +8,7 @@ struct BLEPeripheralLinkState {
var isConnecting: Bool
var isConnected: Bool
var lastConnectionAttempt: Date?
var attemptToken: UInt64 = 0
/// When didConnect last fired for this link. Nil for links restored
/// already-connected (their connect predates this process), which is
/// exactly the signal redundant-link consolidation needs: a restored
@ -17,6 +18,23 @@ struct BLEPeripheralLinkState {
var assembler: NotificationStreamAssembler
}
struct BLEConnectTimeoutPolicy {
/// Pure decision: given a captured attempt token and current link state attributes,
/// returns true if the timeout closure should act on this peripheral attempt.
static func shouldExecuteConnectTimeout(
capturedAttemptToken: UInt64,
isConnecting: Bool,
isConnected: Bool,
currentAttemptToken: UInt64,
isPeripheralConnected: Bool
) -> Bool {
guard isConnecting && !isConnected else { return false }
guard !isPeripheralConnected else { return false }
guard currentAttemptToken == capturedAttemptToken else { return false }
return true
}
}
struct BLEDirectLinkState: Equatable {
let hasPeripheral: Bool
let hasCentral: Bool
@ -104,7 +122,10 @@ final class BLELinkStateStore {
return state
}
func beginConnecting(to peripheral: CBPeripheral, at date: Date) {
@discardableResult
func beginConnecting(to peripheral: CBPeripheral, at date: Date = Date()) -> UInt64 {
let peripheralID = peripheral.identifier.uuidString
let nextAttemptToken = (peripherals[peripheralID]?.attemptToken ?? 0) + 1
setPeripheralState(
BLEPeripheralLinkState(
peripheral: peripheral,
@ -112,10 +133,12 @@ final class BLELinkStateStore {
isConnecting: true,
isConnected: false,
lastConnectionAttempt: date,
attemptToken: nextAttemptToken,
assembler: NotificationStreamAssembler()
),
for: peripheral.identifier.uuidString
for: peripheralID
)
return nextAttemptToken
}
func markConnected(_ peripheral: CBPeripheral, at now: Date = Date()) {

View File

@ -231,7 +231,9 @@ final class BLERadioController {
guard delegate?.radioIsPanicSuspended() == false else { return }
let peripheral = candidate.peripheral
let peripheralID = candidate.peripheralID
linkStateStore.beginConnecting(to: peripheral, at: Date())
// Captured monotonically increasing attempt token so the timeout closure
// below can tell this specific attempt apart from a later one.
let attemptToken = linkStateStore.beginConnecting(to: peripheral, at: Date())
peripheral.delegate = peripheralDelegate
let options: [String: Any] = [
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
@ -243,12 +245,25 @@ final class BLERadioController {
SecureLogger.debug("\(logPrefix): \(candidate.name) [RSSI:\(candidate.rssi)]", category: .session)
queue.asyncAfter(deadline: .now() + TransportConfig.bleConnectTimeoutSeconds) { [weak self] in
guard let self,
let state = self.linkStateStore.state(forPeripheralID: peripheralID),
state.isConnecting && !state.isConnected else { return }
guard let self else { return }
let state = self.linkStateStore.state(forPeripheralID: peripheralID)
guard peripheral.state != .connected else {
SecureLogger.debug("⏱️ Timeout fired but peripheral already connected: \(candidate.name)", category: .session)
// A disconnect + immediate reconnect between this attempt and now
// starts a new attempt with an incremented attemptToken. Without
// this check, this now-stale timeout would cancel that newer,
// still-live attempt out from under it and apply a
// connection-timeout penalty to a peer that never actually timed out.
guard let state,
BLEConnectTimeoutPolicy.shouldExecuteConnectTimeout(
capturedAttemptToken: attemptToken,
isConnecting: state.isConnecting,
isConnected: state.isConnected,
currentAttemptToken: state.attemptToken,
isPeripheralConnected: peripheral.state == .connected
) else {
if let state, state.attemptToken != attemptToken {
SecureLogger.debug("⏱️ Timeout fired for a superseded connection attempt, ignoring: \(candidate.name)", category: .session)
}
return
}

View File

@ -0,0 +1,66 @@
import Foundation
import Testing
@testable import bitchat
struct BLEConnectTimeoutPolicyTests {
@Test
func supersededAttemptTokenIgnoresTimeout() {
let capturedToken: UInt64 = 1
let currentToken: UInt64 = 2
let result = BLEConnectTimeoutPolicy.shouldExecuteConnectTimeout(
capturedAttemptToken: capturedToken,
isConnecting: true,
isConnected: false,
currentAttemptToken: currentToken,
isPeripheralConnected: false
)
#expect(!result)
}
@Test
func matchingAttemptTokenExecutesTimeout() {
let token: UInt64 = 5
let result = BLEConnectTimeoutPolicy.shouldExecuteConnectTimeout(
capturedAttemptToken: token,
isConnecting: true,
isConnected: false,
currentAttemptToken: token,
isPeripheralConnected: false
)
#expect(result)
}
@Test
func connectedStateIgnoresTimeout() {
let token: UInt64 = 5
let result = BLEConnectTimeoutPolicy.shouldExecuteConnectTimeout(
capturedAttemptToken: token,
isConnecting: false,
isConnected: true,
currentAttemptToken: token,
isPeripheralConnected: true
)
#expect(!result)
}
@Test
func peripheralConnectedStateIgnoresTimeout() {
let token: UInt64 = 5
let result = BLEConnectTimeoutPolicy.shouldExecuteConnectTimeout(
capturedAttemptToken: token,
isConnecting: true,
isConnected: false,
currentAttemptToken: token,
isPeripheralConnected: true
)
#expect(!result)
}
}