// // WebSocketClient.swift // // Copyright 2026 Brendan Szymanski // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later // 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")) } } }