vibetunnel/ios/VibeTunnel/Services/ConnectionManager.swift
Igor Tarasenko 9fad6301a0
feat: Add Bonjour/mDNS service discovery for iOS app (#226)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2025-07-05 11:34:36 +01:00

126 lines
4.7 KiB
Swift

import Foundation
import Observation
/// Manages the server connection state and configuration.
///
/// ConnectionManager handles saving and loading server configurations,
/// tracking connection state, and providing a central point for
/// connection-related operations.
@Observable
@MainActor
final class ConnectionManager {
static let shared = ConnectionManager()
// MARK: - Constants
private enum Constants {
static let connectionRestorationWindow: TimeInterval = 3_600 // 1 hour
static let savedServerConfigKey = "savedServerConfig"
static let connectionStateKey = "connectionState"
static let lastConnectionTimeKey = "lastConnectionTime"
}
var isConnected: Bool = false {
didSet {
guard oldValue != isConnected else { return }
storage.set(isConnected, forKey: Constants.connectionStateKey)
}
}
var serverConfig: ServerConfig?
var lastConnectionTime: Date?
private(set) var authenticationService: AuthenticationService?
private let storage: PersistentStorage
private init(storage: PersistentStorage = UserDefaultsStorage()) {
self.storage = storage
loadSavedConnection()
restoreConnectionState()
}
#if DEBUG
/// Test-only factory method for creating instances with mock storage
/// - Parameter storage: Mock storage for testing
/// - Returns: A new ConnectionManager instance for testing
static func createForTesting(storage: PersistentStorage) -> ConnectionManager {
ConnectionManager(storage: storage)
}
#endif
private func loadSavedConnection() {
if let data = storage.data(forKey: Constants.savedServerConfigKey),
let config = try? JSONDecoder().decode(ServerConfig.self, from: data)
{
self.serverConfig = config
// Set up authentication service for restored connection
authenticationService = AuthenticationService(
apiClient: APIClient.shared,
serverConfig: config
)
// Configure API client and WebSocket client with auth service
if let authService = authenticationService {
APIClient.shared.setAuthenticationService(authService)
BufferWebSocketClient.shared.setAuthenticationService(authService)
}
}
}
private func restoreConnectionState() {
// Restore connection state if app was terminated while connected
let wasConnected = storage.bool(forKey: Constants.connectionStateKey)
if let lastConnectionData = storage.object(forKey: Constants.lastConnectionTimeKey) as? Date {
lastConnectionTime = lastConnectionData
// Only restore connection if it was within the last hour
let timeSinceLastConnection = Date().timeIntervalSince(lastConnectionData)
if wasConnected && timeSinceLastConnection < Constants.connectionRestorationWindow && serverConfig != nil {
// Attempt to restore connection
isConnected = true
} else {
// Clear stale connection state
isConnected = false
}
}
}
func saveConnection(_ config: ServerConfig) {
if let data = try? JSONEncoder().encode(config) {
// Create and configure authentication service BEFORE saving config
// This prevents race conditions where other components try to use
// the API client before authentication is properly configured
authenticationService = AuthenticationService(
apiClient: APIClient.shared,
serverConfig: config
)
// Configure API client and WebSocket client with auth service
if let authService = authenticationService {
APIClient.shared.setAuthenticationService(authService)
BufferWebSocketClient.shared.setAuthenticationService(authService)
}
// Now save the config and timestamp after auth is set up
storage.set(data, forKey: Constants.savedServerConfigKey)
self.serverConfig = config
// Save connection timestamp
lastConnectionTime = Date()
storage.set(lastConnectionTime, forKey: Constants.lastConnectionTimeKey)
}
}
func disconnect() async {
isConnected = false
storage.removeObject(forKey: Constants.connectionStateKey)
storage.removeObject(forKey: Constants.lastConnectionTimeKey)
await authenticationService?.logout()
authenticationService = nil
}
var currentServerConfig: ServerConfig? {
serverConfig
}
}