mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 00:05:18 +00:00
Revert "BLE background presence: pending-connect wake-on-proximity + wake-window maintenance (#1395)"
This reverts commit b043324035.
This commit is contained in:
@@ -1,53 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// Remembers recently seen bitchat peripherals (fresh discoveries and dropped
|
|
||||||
/// links) so the service can arm pending background connections against them
|
|
||||||
/// when the app leaves the foreground. Generic over the peripheral type so
|
|
||||||
/// the eviction/expiry logic is testable without CoreBluetooth.
|
|
||||||
final class BLERecentPeripheralCache<Peripheral> {
|
|
||||||
private struct Entry {
|
|
||||||
let peripheral: Peripheral
|
|
||||||
var lastSeen: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private var entries: [String: Entry] = [:]
|
|
||||||
private let capacity: Int
|
|
||||||
private let maxAge: TimeInterval
|
|
||||||
|
|
||||||
init(
|
|
||||||
capacity: Int = TransportConfig.bleRecentPeripheralCacheCap,
|
|
||||||
maxAge: TimeInterval = TransportConfig.bleRecentPeripheralMaxAgeSeconds
|
|
||||||
) {
|
|
||||||
self.capacity = capacity
|
|
||||||
self.maxAge = maxAge
|
|
||||||
}
|
|
||||||
|
|
||||||
var count: Int { entries.count }
|
|
||||||
|
|
||||||
func record(_ peripheral: Peripheral, peripheralID: String, at now: Date) {
|
|
||||||
entries[peripheralID] = Entry(peripheral: peripheral, lastSeen: now)
|
|
||||||
guard entries.count > capacity else { return }
|
|
||||||
// Inserts overshoot capacity by at most one; evict the stalest entry
|
|
||||||
if let stalest = entries.min(by: { $0.value.lastSeen < $1.value.lastSeen }) {
|
|
||||||
entries.removeValue(forKey: stalest.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Most-recently-seen peripherals eligible for a pending background
|
|
||||||
/// connect, freshest first, capped at `limit`. Expired entries are
|
|
||||||
/// pruned as a side effect.
|
|
||||||
func reconnectTargets(
|
|
||||||
now: Date,
|
|
||||||
limit: Int,
|
|
||||||
excluding: (String) -> Bool
|
|
||||||
) -> [(peripheralID: String, peripheral: Peripheral)] {
|
|
||||||
let cutoff = now.addingTimeInterval(-maxAge)
|
|
||||||
entries = entries.filter { $0.value.lastSeen >= cutoff }
|
|
||||||
guard limit > 0 else { return [] }
|
|
||||||
return entries
|
|
||||||
.filter { !excluding($0.key) }
|
|
||||||
.sorted { $0.value.lastSeen > $1.value.lastSeen }
|
|
||||||
.prefix(limit)
|
|
||||||
.map { (peripheralID: $0.key, peripheral: $0.value.peripheral) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -197,7 +197,6 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
|
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
|
||||||
private var maintenanceCounter = 0 // Track maintenance cycles
|
private var maintenanceCounter = 0 // Track maintenance cycles
|
||||||
private var lastMaintenanceAt = Date.distantPast // bleQueue-confined; drives background-wake catch-up passes
|
|
||||||
/// Whether real CoreBluetooth managers were initialized. When false (unit
|
/// Whether real CoreBluetooth managers were initialized. When false (unit
|
||||||
/// tests), periodic mesh background work is not started — the maintenance
|
/// tests), periodic mesh background work is not started — the maintenance
|
||||||
/// timer and the gossip-sync timers only drain BLE writes/notifications,
|
/// timer and the gossip-sync timers only drain BLE writes/notifications,
|
||||||
@@ -208,9 +207,6 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// MARK: - Connection budget & scheduling (central role)
|
// MARK: - Connection budget & scheduling (central role)
|
||||||
private var connectionScheduler = BLEConnectionScheduler<CBPeripheral>()
|
private var connectionScheduler = BLEConnectionScheduler<CBPeripheral>()
|
||||||
// Recently seen peripherals retained for background wake-on-proximity
|
|
||||||
// connects (bleQueue-confined, like the link state store)
|
|
||||||
private let recentPeripheralCache = BLERecentPeripheralCache<CBPeripheral>()
|
|
||||||
|
|
||||||
// MARK: - Adaptive scanning duty-cycle
|
// MARK: - Adaptive scanning duty-cycle
|
||||||
private var scanDutyTimer: DispatchSourceTimer?
|
private var scanDutyTimer: DispatchSourceTimer?
|
||||||
@@ -1613,9 +1609,6 @@ extension BLEService: CBCentralManagerDelegate {
|
|||||||
isConnectable: isConnectable,
|
isConnectable: isConnectable,
|
||||||
discoveredAt: Date()
|
discoveredAt: Date()
|
||||||
)
|
)
|
||||||
if isConnectable {
|
|
||||||
recentPeripheralCache.record(peripheral, peripheralID: peripheralID, at: candidate.discoveredAt)
|
|
||||||
}
|
|
||||||
let existingState = linkStateStore.state(forPeripheralID: peripheralID).map(BLEExistingConnectionState.init)
|
let existingState = linkStateStore.state(forPeripheralID: peripheralID).map(BLEExistingConnectionState.init)
|
||||||
|
|
||||||
switch connectionScheduler.handleDiscovery(
|
switch connectionScheduler.handleDiscovery(
|
||||||
@@ -1643,14 +1636,6 @@ extension BLEService: CBCentralManagerDelegate {
|
|||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
let peripheralID = peripheral.identifier.uuidString
|
let peripheralID = peripheral.identifier.uuidString
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
// A connect completing while backgrounded is the wake-on-proximity
|
|
||||||
// path doing its job — worth an info line for field verification.
|
|
||||||
if !isAppActive {
|
|
||||||
SecureLogger.info("🌙 Background wake: connected to \(peripheral.name ?? peripheralID) while backgrounded", category: .session)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Update state to connected
|
// Update state to connected
|
||||||
linkStateStore.markConnected(peripheral)
|
linkStateStore.markConnected(peripheral)
|
||||||
|
|
||||||
@@ -1676,23 +1661,6 @@ extension BLEService: CBCentralManagerDelegate {
|
|||||||
connectionScheduler.recordDisconnectError(peripheralID: peripheralID, at: Date())
|
connectionScheduler.recordDisconnectError(peripheralID: peripheralID, at: Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retain the handle: a dropped link is the best wake-on-proximity
|
|
||||||
// candidate if the app backgrounds before the peer returns.
|
|
||||||
recentPeripheralCache.record(peripheral, peripheralID: peripheralID, at: Date())
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
// Link lost while backgrounded (peer walked away): re-arm a pending
|
|
||||||
// connect during this wake window so the peer's return wakes us again.
|
|
||||||
// Delayed past the disconnect-settle window to avoid reconnect thrash
|
|
||||||
// at range edge.
|
|
||||||
if !isAppActive {
|
|
||||||
bleQueue.asyncAfter(deadline: .now() + TransportConfig.bleDisconnectDiscoveryIgnoreSeconds) { [weak self] in
|
|
||||||
guard let self, !self.isAppActive else { return }
|
|
||||||
self.armPendingBackgroundConnects()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Clean up references and peer mappings
|
// Clean up references and peer mappings
|
||||||
_ = linkStateStore.removePeripheral(peripheralID)
|
_ = linkStateStore.removePeripheral(peripheralID)
|
||||||
if let peerID {
|
if let peerID {
|
||||||
@@ -1820,18 +1788,6 @@ extension BLEService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
if !self.isAppActive {
|
|
||||||
// Backgrounded: leave the connect pending. iOS never expires
|
|
||||||
// it — the controller completes it whenever the peer comes
|
|
||||||
// back into range, waking the app (state restoration relaunches
|
|
||||||
// us if we were terminated). Foreground return cancels stale
|
|
||||||
// pendings via cancelStalePendingConnects().
|
|
||||||
SecureLogger.info("🌙 Connect timeout deferred while backgrounded, left pending for wake-on-proximity: \(candidate.name)", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SecureLogger.debug("⏱️ Timeout: \(candidate.name)", category: .session)
|
SecureLogger.debug("⏱️ Timeout: \(candidate.name)", category: .session)
|
||||||
central.cancelPeripheralConnection(peripheral)
|
central.cancelPeripheralConnection(peripheral)
|
||||||
_ = self.linkStateStore.removePeripheral(peripheralID)
|
_ = self.linkStateStore.removePeripheral(peripheralID)
|
||||||
@@ -3451,7 +3407,6 @@ extension BLEService {
|
|||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
startScanning()
|
startScanning()
|
||||||
}
|
}
|
||||||
cancelStalePendingConnects()
|
|
||||||
logBluetoothStatus("became-active")
|
logBluetoothStatus("became-active")
|
||||||
scheduleBluetoothStatusSample(after: 5.0, context: "active-5s")
|
scheduleBluetoothStatusSample(after: 5.0, context: "active-5s")
|
||||||
// No Local Name; nothing to refresh for advertising policy
|
// No Local Name; nothing to refresh for advertising policy
|
||||||
@@ -3464,7 +3419,6 @@ extension BLEService {
|
|||||||
centralManager?.stopScan()
|
centralManager?.stopScan()
|
||||||
startScanning()
|
startScanning()
|
||||||
}
|
}
|
||||||
armPendingBackgroundConnects()
|
|
||||||
// Backgrounding may precede a kill; flush the public-history archive
|
// Backgrounding may precede a kill; flush the public-history archive
|
||||||
// outside its 30s maintenance cadence.
|
// outside its 30s maintenance cadence.
|
||||||
gossipSyncManager?.persistNow()
|
gossipSyncManager?.persistNow()
|
||||||
@@ -3472,76 +3426,6 @@ extension BLEService {
|
|||||||
scheduleBluetoothStatusSample(after: 15.0, context: "background-15s")
|
scheduleBluetoothStatusSample(after: 15.0, context: "background-15s")
|
||||||
// No Local Name; nothing to refresh for advertising policy
|
// No Local Name; nothing to refresh for advertising policy
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Issue indefinite `connect()` requests to recently seen peripherals on
|
|
||||||
/// backgrounding. Pending connects live in the Bluetooth controller's
|
|
||||||
/// allowlist — no scanning and no app CPU — and complete whenever a peer
|
|
||||||
/// comes into range, waking (or relaunching) the app. A couple of central
|
|
||||||
/// slots stay reserved for connects driven by live background discovery.
|
|
||||||
private func armPendingBackgroundConnects() {
|
|
||||||
bleQueue.async { [weak self] in
|
|
||||||
guard let self, let central = self.centralManager, central.state == .poweredOn else { return }
|
|
||||||
let budget = TransportConfig.bleMaxCentralLinks
|
|
||||||
- TransportConfig.bleBackgroundPendingConnectSlotReserve
|
|
||||||
- self.linkStateStore.connectedOrConnectingPeripheralCount
|
|
||||||
let now = Date()
|
|
||||||
let targets = self.recentPeripheralCache.reconnectTargets(now: now, limit: budget) { peripheralID in
|
|
||||||
let state = self.linkStateStore.state(forPeripheralID: peripheralID)
|
|
||||||
return state?.isConnected == true || state?.isConnecting == true
|
|
||||||
}
|
|
||||||
guard !targets.isEmpty else { return }
|
|
||||||
for target in targets {
|
|
||||||
// lastConnectionAttempt stays nil: an indefinite pending connect
|
|
||||||
// has no attempt clock, and nil marks it always-stale so
|
|
||||||
// cancelStalePendingConnects() reclaims it on foreground even
|
|
||||||
// after a quick background→foreground bounce.
|
|
||||||
self.linkStateStore.setPeripheralState(
|
|
||||||
BLEPeripheralLinkState(
|
|
||||||
peripheral: target.peripheral,
|
|
||||||
characteristic: nil,
|
|
||||||
peerID: nil,
|
|
||||||
isConnecting: true,
|
|
||||||
isConnected: false,
|
|
||||||
lastConnectionAttempt: nil,
|
|
||||||
assembler: NotificationStreamAssembler()
|
|
||||||
),
|
|
||||||
for: target.peripheralID
|
|
||||||
)
|
|
||||||
target.peripheral.delegate = self
|
|
||||||
central.connect(target.peripheral, options: [
|
|
||||||
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
||||||
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
||||||
CBConnectPeripheralOptionNotifyOnNotificationKey: true
|
|
||||||
])
|
|
||||||
}
|
|
||||||
SecureLogger.info("🌙 Armed \(targets.count) pending background connect(s) for wake-on-proximity", category: .session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Foreground restores normal connection management: pending connects
|
|
||||||
/// older than the connect timeout (including ones rebuilt by state
|
|
||||||
/// restoration after a relaunch) are cancelled so live scanning and the
|
|
||||||
/// scheduler take over. Anything still nearby is rediscovered within
|
|
||||||
/// seconds by the allow-duplicates foreground scan.
|
|
||||||
private func cancelStalePendingConnects() {
|
|
||||||
bleQueue.async { [weak self] in
|
|
||||||
guard let self, let central = self.centralManager else { return }
|
|
||||||
let now = Date()
|
|
||||||
var cancelled = 0
|
|
||||||
for state in self.linkStateStore.peripheralStates where state.isConnecting && !state.isConnected {
|
|
||||||
let age = state.lastConnectionAttempt.map { now.timeIntervalSince($0) } ?? .infinity
|
|
||||||
guard age > TransportConfig.bleConnectTimeoutSeconds else { continue }
|
|
||||||
let peripheralID = state.peripheral.identifier.uuidString
|
|
||||||
central.cancelPeripheralConnection(state.peripheral)
|
|
||||||
_ = self.linkStateStore.removePeripheral(peripheralID)
|
|
||||||
cancelled += 1
|
|
||||||
}
|
|
||||||
if cancelled > 0 {
|
|
||||||
SecureLogger.info("🌅 Cancelled \(cancelled) stale pending connect(s) on foreground", category: .session)
|
|
||||||
self.tryConnectFromQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: Private Message Handling
|
// MARK: Private Message Handling
|
||||||
@@ -3890,16 +3774,6 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
// The maintenance timer is suspended with the app, so a packet arriving
|
|
||||||
// while backgrounded means the radio woke us — use the wake window to
|
|
||||||
// run the announce/flush/drain pass the timer would have run.
|
|
||||||
if !isAppActive {
|
|
||||||
bleQueue.async { [weak self] in self?.performBackgroundWakeMaintenanceIfStale() }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
// Process by type
|
// Process by type
|
||||||
switch context.messageType {
|
switch context.messageType {
|
||||||
case .announce:
|
case .announce:
|
||||||
@@ -4396,7 +4270,6 @@ extension BLEService {
|
|||||||
|
|
||||||
private func performMaintenance() {
|
private func performMaintenance() {
|
||||||
maintenanceCounter += 1
|
maintenanceCounter += 1
|
||||||
lastMaintenanceAt = Date()
|
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let connectedCount = collectionsQueue.sync { peerRegistry.connectedCount }
|
let connectedCount = collectionsQueue.sync { peerRegistry.connectedCount }
|
||||||
@@ -4461,18 +4334,6 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
/// Catch-up maintenance for background wake windows (bleQueue-confined).
|
|
||||||
/// Rate-limited to the normal maintenance cadence so a burst of inbound
|
|
||||||
/// packets during one wake still runs at most one extra pass.
|
|
||||||
private func performBackgroundWakeMaintenanceIfStale() {
|
|
||||||
guard meshBackgroundEnabled,
|
|
||||||
!isAppActive,
|
|
||||||
Date().timeIntervalSince(lastMaintenanceAt) >= TransportConfig.bleMaintenanceInterval else { return }
|
|
||||||
performMaintenance()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private func checkPeerConnectivity() {
|
private func checkPeerConnectivity() {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let peerIDsForLinkState: [PeerID] = collectionsQueue.sync { peerRegistry.peerIDs }
|
let peerIDsForLinkState: [PeerID] = collectionsQueue.sync { peerRegistry.peerIDs }
|
||||||
|
|||||||
@@ -242,16 +242,6 @@ enum TransportConfig {
|
|||||||
static let bleDisconnectNotifyDebounceSeconds: TimeInterval = 0.9
|
static let bleDisconnectNotifyDebounceSeconds: TimeInterval = 0.9
|
||||||
static let bleReconnectLogDebounceSeconds: TimeInterval = 2.0
|
static let bleReconnectLogDebounceSeconds: TimeInterval = 2.0
|
||||||
|
|
||||||
// Background wake-on-proximity (iOS). Pending connects issued on
|
|
||||||
// backgrounding never expire at the OS level: the Bluetooth controller
|
|
||||||
// completes them whenever the peer reappears in range and relaunches the
|
|
||||||
// app via state restoration. Entries older than the BLE address-rotation
|
|
||||||
// window no longer map to a reachable address, so the cache prunes them.
|
|
||||||
static let bleRecentPeripheralCacheCap: Int = 16
|
|
||||||
static let bleRecentPeripheralMaxAgeSeconds: TimeInterval = 15 * 60
|
|
||||||
// Central slots kept free for connects driven by live background discovery
|
|
||||||
static let bleBackgroundPendingConnectSlotReserve: Int = 2
|
|
||||||
|
|
||||||
// Weak-link cooldown after connection timeouts
|
// Weak-link cooldown after connection timeouts
|
||||||
static let bleWeakLinkCooldownSeconds: TimeInterval = 30.0
|
static let bleWeakLinkCooldownSeconds: TimeInterval = 30.0
|
||||||
static let bleWeakLinkRSSICutoff: Int = -90
|
static let bleWeakLinkRSSICutoff: Int = -90
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
//
|
|
||||||
// BLERecentPeripheralCacheTests.swift
|
|
||||||
// bitchatTests
|
|
||||||
//
|
|
||||||
// Eviction, expiry, and reconnect-target selection for the background
|
|
||||||
// wake-on-proximity peripheral cache.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Testing
|
|
||||||
import Foundation
|
|
||||||
@testable import bitchat
|
|
||||||
|
|
||||||
struct BLERecentPeripheralCacheTests {
|
|
||||||
|
|
||||||
private let base = Date(timeIntervalSince1970: 1_700_000_000)
|
|
||||||
|
|
||||||
private func makeCache(capacity: Int = 4, maxAge: TimeInterval = 900) -> BLERecentPeripheralCache<String> {
|
|
||||||
BLERecentPeripheralCache<String>(capacity: capacity, maxAge: maxAge)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func recordUpsertsByPeripheralID() {
|
|
||||||
let cache = makeCache()
|
|
||||||
cache.record("p1", peripheralID: "A", at: base)
|
|
||||||
cache.record("p1-updated", peripheralID: "A", at: base.addingTimeInterval(10))
|
|
||||||
|
|
||||||
#expect(cache.count == 1)
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(11), limit: 10) { _ in false }
|
|
||||||
#expect(targets.map(\.peripheral) == ["p1-updated"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func overCapacityEvictsStalestEntry() {
|
|
||||||
let cache = makeCache(capacity: 2)
|
|
||||||
cache.record("p1", peripheralID: "A", at: base)
|
|
||||||
cache.record("p2", peripheralID: "B", at: base.addingTimeInterval(1))
|
|
||||||
cache.record("p3", peripheralID: "C", at: base.addingTimeInterval(2))
|
|
||||||
|
|
||||||
#expect(cache.count == 2)
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(3), limit: 10) { _ in false }
|
|
||||||
#expect(targets.map(\.peripheralID) == ["C", "B"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func refreshingAnEntryProtectsItFromEviction() {
|
|
||||||
let cache = makeCache(capacity: 2)
|
|
||||||
cache.record("p1", peripheralID: "A", at: base)
|
|
||||||
cache.record("p2", peripheralID: "B", at: base.addingTimeInterval(1))
|
|
||||||
// A becomes the freshest again; adding C must evict B, not A
|
|
||||||
cache.record("p1", peripheralID: "A", at: base.addingTimeInterval(2))
|
|
||||||
cache.record("p3", peripheralID: "C", at: base.addingTimeInterval(3))
|
|
||||||
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(4), limit: 10) { _ in false }
|
|
||||||
#expect(targets.map(\.peripheralID) == ["C", "A"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func expiredEntriesArePruned() {
|
|
||||||
let cache = makeCache(maxAge: 100)
|
|
||||||
cache.record("p1", peripheralID: "A", at: base)
|
|
||||||
cache.record("p2", peripheralID: "B", at: base.addingTimeInterval(50))
|
|
||||||
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(120), limit: 10) { _ in false }
|
|
||||||
#expect(targets.map(\.peripheralID) == ["B"])
|
|
||||||
#expect(cache.count == 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func targetsAreFreshestFirstAndCappedAtLimit() {
|
|
||||||
let cache = makeCache(capacity: 8)
|
|
||||||
for (index, id) in ["A", "B", "C", "D"].enumerated() {
|
|
||||||
cache.record("p\(id)", peripheralID: id, at: base.addingTimeInterval(TimeInterval(index)))
|
|
||||||
}
|
|
||||||
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(10), limit: 2) { _ in false }
|
|
||||||
#expect(targets.map(\.peripheralID) == ["D", "C"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func excludedPeripheralsAreSkippedWithoutConsumingTheLimit() {
|
|
||||||
let cache = makeCache(capacity: 8)
|
|
||||||
for (index, id) in ["A", "B", "C"].enumerated() {
|
|
||||||
cache.record("p\(id)", peripheralID: id, at: base.addingTimeInterval(TimeInterval(index)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// C (freshest) is already connected; the two slots go to B and A
|
|
||||||
let targets = cache.reconnectTargets(now: base.addingTimeInterval(10), limit: 2) { $0 == "C" }
|
|
||||||
#expect(targets.map(\.peripheralID) == ["B", "A"])
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func nonPositiveLimitReturnsNothing() {
|
|
||||||
let cache = makeCache()
|
|
||||||
cache.record("p1", peripheralID: "A", at: base)
|
|
||||||
|
|
||||||
#expect(cache.reconnectTargets(now: base, limit: 0) { _ in false }.isEmpty)
|
|
||||||
#expect(cache.reconnectTargets(now: base, limit: -3) { _ in false }.isEmpty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user