Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.6 KiB
Swift
135 lines
4.6 KiB
Swift
import Foundation
|
|
|
|
final class ActionCableClient: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
|
|
protocol Listener: AnyObject {
|
|
func onConnected()
|
|
func onDisconnected()
|
|
func onChannelMessage(_ payload: [String: Any])
|
|
}
|
|
|
|
private let cableUrl: URL
|
|
private let accessToken: String
|
|
private var webSocket: URLSessionWebSocketTask?
|
|
private var channelIdentifier: String?
|
|
private weak var listener: Listener?
|
|
private let session: URLSession
|
|
|
|
init(cableUrl: String, accessToken: String) {
|
|
self.cableUrl = URL(string: cableUrl)!
|
|
self.accessToken = accessToken
|
|
let config = URLSessionConfiguration.default
|
|
self.session = URLSession(configuration: config)
|
|
super.init()
|
|
}
|
|
|
|
func connect(channel: String, params: [String: String], listener: Listener) {
|
|
disconnect()
|
|
self.listener = listener
|
|
var identifier: [String: Any] = ["channel": channel]
|
|
params.forEach { identifier[$0.key] = $0.value }
|
|
channelIdentifier = jsonString(identifier)
|
|
|
|
var request = URLRequest(url: cableUrl)
|
|
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
webSocket = session.webSocketTask(with: request)
|
|
webSocket?.resume()
|
|
receiveLoop()
|
|
sendSubscribe()
|
|
}
|
|
|
|
func performReceive(payload: [String: Any]) {
|
|
guard let identifier = channelIdentifier else { return }
|
|
var data = payload
|
|
data["action"] = "receive"
|
|
let envelope: [String: Any] = [
|
|
"command": "message",
|
|
"identifier": identifier,
|
|
"data": jsonString(data) ?? "{}",
|
|
]
|
|
send(json: envelope)
|
|
}
|
|
|
|
func disconnect() {
|
|
webSocket?.cancel(with: .goingAway, reason: nil)
|
|
webSocket = nil
|
|
channelIdentifier = nil
|
|
listener?.onDisconnected()
|
|
}
|
|
|
|
private func sendSubscribe() {
|
|
guard let identifier = channelIdentifier else { return }
|
|
send(json: ["command": "subscribe", "identifier": identifier])
|
|
}
|
|
|
|
private func receiveLoop() {
|
|
webSocket?.receive { [weak self] result in
|
|
guard let self else { return }
|
|
switch result {
|
|
case .success(let message):
|
|
switch message {
|
|
case .string(let text):
|
|
self.handleIncoming(text)
|
|
case .data(let data):
|
|
if let text = String(data: data, encoding: .utf8) { self.handleIncoming(text) }
|
|
@unknown default:
|
|
break
|
|
}
|
|
self.receiveLoop()
|
|
case .failure:
|
|
self.listener?.onDisconnected()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleIncoming(_ text: String) {
|
|
guard let data = text.data(using: .utf8),
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
|
|
|
|
if let type = json["type"] as? String, type == "ping" {
|
|
let pong: [String: Any] = ["type": "pong", "message": json["message"] ?? Date().timeIntervalSince1970]
|
|
send(json: pong)
|
|
return
|
|
}
|
|
|
|
if json["type"] as? String == "confirm_subscription" {
|
|
listener?.onConnected()
|
|
return
|
|
}
|
|
|
|
if json["type"] as? String == "disconnect" {
|
|
listener?.onDisconnected()
|
|
return
|
|
}
|
|
|
|
if let message = json["message"] {
|
|
var payload: [String: Any]?
|
|
if let dict = message as? [String: Any] {
|
|
if let nested = dict["data"] as? [String: Any] {
|
|
payload = nested
|
|
} else if dict["method"] as? String == "receive_message",
|
|
let nested = dict["data"] as? [String: Any] {
|
|
payload = nested
|
|
} else {
|
|
payload = dict
|
|
}
|
|
} else if let str = message as? String,
|
|
let d = str.data(using: .utf8),
|
|
let dict = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
|
|
payload = dict
|
|
}
|
|
if let payload { listener?.onChannelMessage(payload) }
|
|
}
|
|
}
|
|
|
|
private func send(json: [String: Any]) {
|
|
guard let text = jsonString(json) else { return }
|
|
webSocket?.send(.string(text)) { _ in }
|
|
}
|
|
|
|
private func jsonString(_ object: Any) -> String? {
|
|
guard let data = try? JSONSerialization.data(withJSONObject: object),
|
|
let text = String(data: data, encoding: .utf8) else { return nil }
|
|
return text
|
|
}
|
|
}
|