Luminate/Sources/LuminateCore/WebSocketClient.swift

111 lines
3.4 KiB
Swift

//
// WebSocketClient.swift
// LuminateCore
//
// Created by Brendan Szymanski on 6/5/26.
//
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"))
}
}
}