chore: centralize location + geohash constants (filters, lookback, relay count) and adopt in LocationChannelManager/ChatViewModel

This commit is contained in:
jack
2025-08-25 20:17:09 +02:00
parent 43e159df0e
commit c39df29b25
5 changed files with 47 additions and 28 deletions
+10 -8
View File
@@ -346,7 +346,9 @@ final class BLEService: NSObject {
// Single maintenance timer for all periodic tasks (dispatch-based for determinism) // Single maintenance timer for all periodic tasks (dispatch-based for determinism)
let timer = DispatchSource.makeTimerSource(queue: bleQueue) let timer = DispatchSource.makeTimerSource(queue: bleQueue)
timer.schedule(deadline: .now() + 10.0, repeating: 10.0, leeway: .seconds(1)) timer.schedule(deadline: .now() + TransportConfig.bleMaintenanceInterval,
repeating: TransportConfig.bleMaintenanceInterval,
leeway: .seconds(TransportConfig.bleMaintenanceLeewaySeconds))
timer.setEventHandler { [weak self] in timer.setEventHandler { [weak self] in
self?.performMaintenance() self?.performMaintenance()
} }
@@ -1912,10 +1914,10 @@ final class BLEService: NSObject {
if lastIsolatedAt == nil { lastIsolatedAt = Date() } if lastIsolatedAt == nil { lastIsolatedAt = Date() }
let iso = lastIsolatedAt ?? Date() let iso = lastIsolatedAt ?? Date()
let elapsed = Date().timeIntervalSince(iso) let elapsed = Date().timeIntervalSince(iso)
if elapsed > 60 { if elapsed > TransportConfig.bleIsolationRelaxThresholdSeconds {
dynamicRSSIThreshold = -92 dynamicRSSIThreshold = TransportConfig.bleRSSIIsolatedRelaxed
} else { } else {
dynamicRSSIThreshold = -90 dynamicRSSIThreshold = TransportConfig.bleRSSIIsolatedBase
} }
return return
} }
@@ -1925,12 +1927,12 @@ final class BLEService: NSObject {
// If we're at budget or queue is large, prefer closer peers // If we're at budget or queue is large, prefer closer peers
let linkCount = peripherals.values.filter { $0.isConnected || $0.isConnecting }.count let linkCount = peripherals.values.filter { $0.isConnected || $0.isConnecting }.count
if linkCount >= maxCentralLinks || connectionCandidates.count > TransportConfig.bleConnectionCandidatesMax { if linkCount >= maxCentralLinks || connectionCandidates.count > TransportConfig.bleConnectionCandidatesMax {
threshold = -85 threshold = TransportConfig.bleRSSIConnectedThreshold
} }
// If we have many recent timeouts, raise further // If we have many recent timeouts, raise further
let recentTimeouts = recentConnectTimeouts.filter { Date().timeIntervalSince($0.value) < 60 }.count let recentTimeouts = recentConnectTimeouts.filter { Date().timeIntervalSince($0.value) < TransportConfig.bleRecentTimeoutWindowSeconds }.count
if recentTimeouts >= 3 { if recentTimeouts >= TransportConfig.bleRecentTimeoutCountThreshold {
threshold = max(threshold, -80) threshold = max(threshold, TransportConfig.bleRSSIHighTimeoutThreshold)
} }
dynamicRSSIThreshold = threshold dynamicRSSIThreshold = threshold
} }
@@ -39,7 +39,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
super.init() super.init()
cl.delegate = self cl.delegate = self
cl.desiredAccuracy = kCLLocationAccuracyHundredMeters cl.desiredAccuracy = kCLLocationAccuracyHundredMeters
cl.distanceFilter = 1000 // meters; we're not tracking continuously cl.distanceFilter = TransportConfig.locationDistanceFilterMeters // meters; we're not tracking continuously
// Load selection // Load selection
if let data = UserDefaults.standard.data(forKey: userDefaultsKey), if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
let channel = try? JSONDecoder().decode(ChannelID.self, from: data) { let channel = try? JSONDecoder().decode(ChannelID.self, from: data) {
@@ -93,7 +93,7 @@ final class LocationChannelManager: NSObject, CLLocationManagerDelegate, Observa
} }
/// Begin periodic one-shot location refreshes while a selector UI is visible. /// Begin periodic one-shot location refreshes while a selector UI is visible.
func beginLiveRefresh(interval: TimeInterval = 5.0) { func beginLiveRefresh(interval: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
guard permissionState == .authorized else { return } guard permissionState == .authorized else { return }
// Switch to a lightweight periodic one-shot request (polling) while the sheet is open // Switch to a lightweight periodic one-shot request (polling) while the sheet is open
refreshTimer?.invalidate() refreshTimer?.invalidate()
+1 -9
View File
@@ -177,15 +177,7 @@ final class NostrTransport: Transport {
func sendBroadcastAnnounce() { /* no-op for Nostr */ } func sendBroadcastAnnounce() { /* no-op for Nostr */ }
func sendDeliveryAck(for messageID: String, to peerID: String) { func sendDeliveryAck(for messageID: String, to peerID: String) {
Task { @MainActor in Task { @MainActor in
var recipientNostrPubkey: String? guard let recipientNpub = resolveRecipientNpub(for: peerID) else { return }
if let noiseKey = Data(hexString: peerID),
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey) {
recipientNostrPubkey = fav.peerNostrPublicKey
}
if recipientNostrPubkey == nil, peerID.count == 16 {
recipientNostrPubkey = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID)?.peerNostrPublicKey
}
guard let recipientNpub = recipientNostrPubkey else { return }
guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return } guard let senderIdentity = try? NostrIdentityBridge.getCurrentNostrIdentity() else { return }
SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))", SecureLogger.log("NostrTransport: preparing DELIVERED ack for id=\(messageID.prefix(8))… to \(recipientNpub.prefix(16))",
category: SecureLogger.session, level: .debug) category: SecureLogger.session, level: .debug)
+20
View File
@@ -37,4 +37,24 @@ enum TransportConfig {
// UI thresholds // UI thresholds
static let uiLateInsertThreshold: TimeInterval = 15.0 static let uiLateInsertThreshold: TimeInterval = 15.0
static let uiProcessedNostrEventsCap: Int = 2000 static let uiProcessedNostrEventsCap: Int = 2000
// BLE maintenance & thresholds
static let bleMaintenanceInterval: TimeInterval = 10.0
static let bleMaintenanceLeewaySeconds: Int = 1
static let bleIsolationRelaxThresholdSeconds: TimeInterval = 60
static let bleRecentTimeoutWindowSeconds: TimeInterval = 60
static let bleRecentTimeoutCountThreshold: Int = 3
static let bleRSSIIsolatedBase: Int = -90
static let bleRSSIIsolatedRelaxed: Int = -92
static let bleRSSIConnectedThreshold: Int = -85
static let bleRSSIHighTimeoutThreshold: Int = -80
// Location
static let locationDistanceFilterMeters: Double = 1000
static let locationLiveRefreshInterval: TimeInterval = 5.0
// Nostr geohash
static let nostrGeohashInitialLookbackSeconds: TimeInterval = 3600
static let nostrGeohashInitialLimit: Int = 200
static let nostrGeoRelayCount: Int = 5
} }
+14 -9
View File
@@ -707,8 +707,15 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
startGeoParticipantsTimer() startGeoParticipantsTimer()
// Unsubscribe + resubscribe // Unsubscribe + resubscribe
NostrRelayManager.shared.unsubscribe(id: subID) NostrRelayManager.shared.unsubscribe(id: subID)
let filter = NostrFilter.geohashEphemeral(ch.geohash, since: Date().addingTimeInterval(-3600), limit: 200) let filter = NostrFilter.geohashEphemeral(
let subRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) ch.geohash,
since: Date().addingTimeInterval(-TransportConfig.nostrGeohashInitialLookbackSeconds),
limit: TransportConfig.nostrGeohashInitialLimit
)
let subRelays = GeoRelayDirectory.shared.closestRelays(
toGeohash: ch.geohash,
count: TransportConfig.nostrGeoRelayCount
)
NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in NostrRelayManager.shared.subscribe(filter: filter, id: subID, relayUrls: subRelays) { [weak self] event in
guard let self = self else { return } guard let self = self else { return }
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return } guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
@@ -1264,7 +1271,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
nickname: self.nickname, nickname: self.nickname,
teleported: LocationChannelManager.shared.teleported teleported: LocationChannelManager.shared.teleported
) )
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) let targetRelays = GeoRelayDirectory.shared.closestRelays(
toGeohash: ch.geohash,
count: TransportConfig.nostrGeoRelayCount
)
if targetRelays.isEmpty { if targetRelays.isEmpty {
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
} else { } else {
@@ -2450,12 +2460,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
) )
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5)
if targetRelays.isEmpty { if targetRelays.isEmpty {
let targetRelays = GeoRelayDirectory.shared.closestRelays(toGeohash: ch.geohash, count: 5) SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
if targetRelays.isEmpty {
SecureLogger.log("Geo: no geohash relays available for \(ch.geohash); not sending", category: SecureLogger.session, level: .warning)
} else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
}
} else { } else {
NostrRelayManager.shared.sendEvent(event, to: targetRelays) NostrRelayManager.shared.sendEvent(event, to: targetRelays)
} }