import Foundation #if canImport(SocketIO) import SocketIO #endif final class SocketService { static let shared = SocketService() private let syncQueue = DispatchQueue(label: "org.yobble.socket.service") private var currentToken: String? private var currentAuthPayload: [String: Any] = [:] #if canImport(SocketIO) private var manager: SocketManager? private var socket: SocketIOClient? #endif private init() {} 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) socket?.connect(withPayload: currentAuthPayload) #else 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") } } socket.on(clientEvent: .disconnect) { data, _ in if AppConfig.DEBUG { print("[SocketService] Disconnected: \(data)") } } socket.on(clientEvent: .error) { data, _ in if AppConfig.DEBUG { print("[SocketService] Error: \(data)") } } self.manager = manager self.socket = socket } private func disconnectInternal() { socket?.disconnect() manager?.disconnect() socket = nil manager = nil } #else private func disconnectInternal() { } #endif }