ios_app_v2/yobble/Services/SocketService.swift
2025-10-21 03:28:39 +03:00

198 lines
6.1 KiB
Swift

import Foundation
import Combine
#if canImport(SocketIO)
import SocketIO
#endif
final class SocketService {
static let shared = SocketService()
enum ConnectionState: Equatable {
case disconnected
case connecting
case connected
}
private let syncQueue = DispatchQueue(label: "org.yobble.socket.service")
private var currentToken: String?
private var currentAuthPayload: [String: Any] = [:]
private let connectionStateSubject = CurrentValueSubject<ConnectionState, Never>(.disconnected)
var connectionStatePublisher: AnyPublisher<ConnectionState, Never> {
connectionStateSubject
.removeDuplicates()
.eraseToAnyPublisher()
}
var currentConnectionState: ConnectionState {
connectionStateSubject.value
}
#if canImport(SocketIO)
private var manager: SocketManager?
private var socket: SocketIOClient?
#endif
private init() {}
private func updateConnectionState(_ state: ConnectionState) {
let sendState: () -> Void = { [weak self] in
guard let self, self.connectionStateSubject.value != state else { return }
self.connectionStateSubject.send(state)
}
if Thread.isMainThread {
sendState()
} else {
DispatchQueue.main.async(execute: sendState)
}
}
func connectForCurrentUser() {
syncQueue.async { [weak self] in
guard let self else { return }
guard let token = self.resolveCurrentAccessToken() else {
if AppConfig.DEBUG { print("[SocketService] No access token available, disconnecting") }
self.currentToken = nil
self.disconnectInternal()
return
}
self.connectInternal(with: token)
}
}
func connect(withToken token: String) {
syncQueue.async { [weak self] in
self?.connectInternal(with: token)
}
}
func disconnect() {
syncQueue.async { [weak self] in
guard let self else { return }
self.currentToken = nil
self.disconnectInternal()
}
}
private func resolveCurrentAccessToken() -> String? {
guard
let login = UserDefaults.standard.string(forKey: "currentUser"),
!login.isEmpty
else {
return nil
}
return KeychainService.shared.get(forKey: "access_token", service: login)
}
private func connectInternal(with token: String) {
#if canImport(SocketIO)
if token == currentToken,
let socket,
socket.status == .connected || socket.status == .connecting {
if AppConfig.DEBUG { print("[SocketService] Already connected with current token") }
return
}
currentToken = token
currentAuthPayload = ["token": token]
setupSocket(with: token)
updateConnectionState(.connecting)
socket?.connect(withPayload: currentAuthPayload)
#else
updateConnectionState(.disconnected)
if AppConfig.DEBUG {
print("[SocketService] SocketIO framework not available; skipping connection")
}
#endif
}
#if canImport(SocketIO)
private func setupSocket(with token: String) {
guard let baseURL = URL(string: AppConfig.API_SERVER) else {
if AppConfig.DEBUG { print("[SocketService] Invalid socket URL: \(AppConfig.API_SERVER)") }
return
}
disconnectInternal()
let configuration: SocketIOClientConfiguration = [
.log(AppConfig.DEBUG),
.compress,
.secure(AppConfig.PROTOCOL.lowercased() == "https"),
.path(AppConfig.SOCKET_PATH),
.reconnects(true),
.reconnectWait(2),
.forceWebsockets(true),
.extraHeaders([
"Authorization": "Bearer \(token)",
"User-Agent": AppConfig.USER_AGENT
]),
.connectParams(["token": token])
]
let manager = SocketManager(socketURL: baseURL, config: configuration)
manager.handleQueue = syncQueue
let socket = manager.defaultSocket
if AppConfig.DEBUG {
socket.onAny { event in
print("[SocketService] onAny event=\(event.event) data=\(event.items ?? [])")
}
}
socket.on(clientEvent: .connect) { _, _ in
if AppConfig.DEBUG { print("[SocketService] Connected") }
self.updateConnectionState(.connected)
}
socket.on(clientEvent: .statusChange) { data, _ in
guard let rawStatus = data.first as? SocketIOStatus else { return }
if rawStatus == .connected {
self.updateConnectionState(.connected)
} else if rawStatus == .connecting {
self.updateConnectionState(.connecting)
} else {
self.updateConnectionState(.disconnected)
}
}
socket.on(clientEvent: .reconnect) { _, _ in
if AppConfig.DEBUG { print("[SocketService] Reconnecting") }
self.updateConnectionState(.connecting)
}
socket.on(clientEvent: .reconnectAttempt) { data, _ in
if AppConfig.DEBUG { print("[SocketService] Reconnect attempt: \(data)") }
self.updateConnectionState(.connecting)
}
socket.on(clientEvent: .disconnect) { data, _ in
if AppConfig.DEBUG { print("[SocketService] Disconnected: \(data)") }
self.updateConnectionState(.disconnected)
}
socket.on(clientEvent: .error) { data, _ in
if AppConfig.DEBUG { print("[SocketService] Error: \(data)") }
self.updateConnectionState(.disconnected)
}
self.manager = manager
self.socket = socket
}
private func disconnectInternal() {
socket?.disconnect()
manager?.disconnect()
socket = nil
manager = nil
updateConnectionState(.disconnected)
}
#else
private func disconnectInternal() { }
#endif
}