import Foundation // ───────────────────────────────────────────────────────────── // Cross-Device Client // ───────────────────────────────────────────────────────────── /// Cross-device client for making requests to other nodes public final class CrossDeviceClient: @unchecked Sendable { private let session: URLSession private let loadBalancer: LoadBalancer private let timeout: TimeInterval public init( loadBalancer: LoadBalancer, timeout: TimeInterval = 30 ) { self.loadBalancer = loadBalancer self.timeout = timeout let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = timeout self.session = URLSession(configuration: config) } /// Send request to cluster public func sendToCluster( endpoint: String, method: String = "POST", body: Data? = nil ) async throws -> CrossDeviceResponse { guard let node = loadBalancer.getNextNode() else { throw CrossDeviceError.noHealthyNodes } return try await sendToNode( node: node, endpoint: endpoint, method: method, body: body ) } /// Send request to specific node public func sendToNode( node: DeviceNode, endpoint: String, method: String = "POST", body: Data? = nil ) async throws -> CrossDeviceResponse { let url = URL(string: "\(node.baseURL)\(endpoint)")! var request = URLRequest(url: url) request.httpMethod = method request.httpBody = body request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.timeoutInterval = timeout let startTime = Date() do { let (data, response) = try await session.data(for: request) let latency = Date().timeIntervalSince(startTime) guard let httpResponse = response as? HTTPURLResponse else { throw CrossDeviceError.invalidResponse } // Update node status loadBalancer.updateNodeStatus( id: node.id, status: httpResponse.statusCode < 500 ? .healthy : .degraded, load: nil ) return CrossDeviceResponse( requestId: UUID().uuidString, statusCode: httpResponse.statusCode, body: data, latency: latency, nodeId: node.id ) } catch { let latency = Date().timeIntervalSince(startTime) // Update node status loadBalancer.updateNodeStatus(id: node.id, status: .unhealthy) throw CrossDeviceError.requestFailed(error) } } /// Broadcast request to all healthy nodes public func broadcastToAll( endpoint: String, method: String = "POST", body: Data? = nil ) async throws -> [CrossDeviceResponse] { let nodes = loadBalancer.getNodes().filter { $0.status == .healthy } return try await withThrowingTaskGroup(of: CrossDeviceResponse.self) { group in for node in nodes { group.addTask { try await self.sendToNode( node: node, endpoint: endpoint, method: method, body: body ) } } var responses: [CrossDeviceResponse] = [] for try await response in group { responses.append(response) } return responses } } } /// Cross-device errors public enum CrossDeviceError: Error, LocalizedError { case noHealthyNodes case invalidResponse case requestFailed(Error) case timeout public var errorDescription: String? { switch self { case .noHealthyNodes: return "No healthy nodes available" case .invalidResponse: return "Invalid response from node" case .requestFailed(let error): return "Request failed: \(error.localizedDescription)" case .timeout: return "Request timed out" } } }