import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public actor WebSocketClient { private var task: URLSessionWebSocketTask? private let session: URLSession private let serverURL: URL private let token: String private var isConnected = false public init(serverURL: URL, token: String) { self.serverURL = serverURL self.token = token self.session = URLSession(configuration: .default) } public func connect() { var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)! components.scheme = serverURL.scheme == "https" ? "wss" : "ws" components.path = "/socket" components.queryItems = [.init(name: "api_key", value: token)] guard let url = components.url else { return } task = session.webSocketTask(with: url) task?.resume() isConnected = true receiveMessage() } public func disconnect() { task?.cancel(with: .goingAway, reason: nil) isConnected = false } private func receiveMessage() { task?.receive { [weak self] result in Task { [weak self] in switch result { case .success(let message): await self?.handleMessage(message) await self?.receiveMessage() case .failure: await self?.reconnect() } } } } private func handleMessage(_ message: URLSessionWebSocketTask.Message) { switch message { case .string(let text): guard let data = text.data(using: .utf8), let event = try? JSONDecoder().decode(WebSocketEvent.self, from: data) else { return } Task { @MainActor in NotificationCenter.default.post( name: .init(event.messageType), object: event ) } case .data: break @unknown default: break } } private func reconnect() async { guard isConnected else { return } try? await Task.sleep(nanoseconds: 5_000_000_000) connect() } } public struct WebSocketEvent: Decodable { public let messageType: String public let data: [String: AnyDecodable]? } public struct AnyDecodable: Decodable { public var value: Any public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let intVal = try? container.decode(Int.self) { value = intVal } else if let doubleVal = try? container.decode(Double.self) { value = doubleVal } else if let boolVal = try? container.decode(Bool.self) { value = boolVal } else if let stringVal = try? container.decode(String.self) { value = stringVal } else if let arrayVal = try? container.decode([AnyDecodable].self) { value = arrayVal.map(\.value) } else if let dictVal = try? container.decode([String: AnyDecodable].self) { value = dictVal.mapValues(\.value) } else { throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "AnyDecodable error")) } } }