import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif @MainActor final class OrderRealtimeTracker { private var pollingTask: Task? = nil private var socketClient: OrderSocketClient? = nil private var activeOrderId: String? = nil var onOrderUpdated: ((PublicOrderResult) -> Void)? func start(orderId: String, jwt: String?) { stop() activeOrderId = orderId pollingTask = Task { @MainActor [weak self] in guard let self else { return } await self.runPollingLoop(orderId: orderId) } guard let jwt, jwt.isEmpty == false else { return } let socket = OrderSocketClient() socket.onOrderUpdate = { [weak self] update in guard let self else { return } guard update.id == orderId else { return } Task { @MainActor [weak self] in guard let self else { return } await self.fetchLatest(orderId: orderId) } } socket.connect(jwt: jwt) socketClient = socket } func stop() { pollingTask?.cancel() pollingTask = nil socketClient?.disconnect() socketClient = nil activeOrderId = nil } private func runPollingLoop(orderId: String) async { var elapsedSeconds = 0 while Task.isCancelled == false { if activeOrderId != orderId { return } let fetched = await fetchLatest(orderId: orderId) if fetched?.isFinalStatus == true { return } let delay = pollingDelay(for: elapsedSeconds) elapsedSeconds += delay do { try await Task.sleep(nanoseconds: UInt64(delay) * 1_000_000_000) } catch { return } } } private func pollingDelay(for elapsedSeconds: Int) -> Int { if elapsedSeconds < 60 { return 3 } if elapsedSeconds < 180 { return 5 } return 10 } @discardableResult private func fetchLatest(orderId: String) async -> PublicOrderResult? { do { let response = try await ApiService().publicOrder(orderId: orderId) guard response.error == false, let order = response.result else { return nil } SessionStateStore.saveTrackedOrder(order) onOrderUpdated?(order) return order } catch { return nil } } } final class OrderSocketClient: @unchecked Sendable { var onOrderUpdate: ((OrderRealtimeUpdate) -> Void)? #if os(iOS) || os(macOS) private var task: URLSessionWebSocketTask? = nil private let session = URLSession(configuration: .default) private var isConnected = false private var pendingJWT: String? = nil #endif func connect(jwt: String) { #if os(iOS) || os(macOS) disconnect() guard let url = makeSocketURL() else { return } let wsTask = session.webSocketTask(with: url) wsTask.resume() task = wsTask pendingJWT = jwt receiveLoop() #else _ = jwt #endif } func disconnect() { #if os(iOS) || os(macOS) isConnected = false pendingJWT = nil task?.cancel(with: .goingAway, reason: nil) task = nil #endif } #if os(iOS) || os(macOS) private func receiveLoop() { guard let task else { return } task.receive { [weak self] result in guard let self else { return } switch result { case .failure: self.disconnect() case .success(let message): self.handleMessage(message) self.receiveLoop() } } } private func handleMessage(_ message: URLSessionWebSocketTask.Message) { let text: String switch message { case .string(let value): text = value case .data(let data): text = String(data: data, encoding: .utf8) ?? "" @unknown default: return } guard text.isEmpty == false else { return } if text == "2" { task?.send(.string("3")) { _ in } return } if text.hasPrefix("0"), let jwt = pendingJWT { let authPacket = "40{\"token\":\"Bearer \(jwt)\"}" task?.send(.string(authPacket)) { _ in } pendingJWT = nil return } if text.hasPrefix("40") { isConnected = true return } guard text.hasPrefix("42") else { return } let eventPayload = String(text.dropFirst(2)) guard let data = eventPayload.data(using: .utf8) else { return } if let rawArray = try? JSONSerialization.jsonObject(with: data) as? [Any], rawArray.count >= 2, let eventName = rawArray[0] as? String, eventName == "order_update" { let payloadAny = rawArray[1] guard JSONSerialization.isValidJSONObject(payloadAny), let payloadData = try? JSONSerialization.data(withJSONObject: payloadAny), let update = try? JSONDecoder().decode(OrderRealtimeUpdate.self, from: payloadData) else { return } onOrderUpdate?(update) return } // Compat: alguns servidores podem encapsular o evento como objeto. if let rawObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let eventName = (rawObject["event"] as? String)?.lowercased(), eventName == "order_update", let payloadAny = rawObject["data"], JSONSerialization.isValidJSONObject(payloadAny), let payloadData = try? JSONSerialization.data(withJSONObject: payloadAny), let update = try? JSONDecoder().decode(OrderRealtimeUpdate.self, from: payloadData) { onOrderUpdate?(update) } } private func makeSocketURL() -> URL? { guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else { return nil } if components.scheme == "https" { components.scheme = "wss" } else { components.scheme = "ws" } components.path = "/socket.io/" components.queryItems = [ URLQueryItem(name: "EIO", value: "4"), URLQueryItem(name: "transport", value: "websocket") ] return components.url } #endif }