Luminate/Sources/LuminateCore/WebSocketClient.swift

93 lines
3.2 KiB
Swift

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")) }
}
}