import Combine import Foundation import GRDB @MainActor final class ClientsStore: ObservableObject { static let shared = ClientsStore() @Published private(set) var ready = false @Published private(set) var clients: [Client] = [] private let credentialsObservation = ValueObservation.tracking(Credentials.fetchAll) init() { Task { do { for try await creds in credentialsObservation.values(in: Database.shared.dbQueue) { processCredentials(creds) if !ready { ready = true } } } catch {} } } private func processCredentials(_ credentials: [Credentials]) { let existsJids = Set(clients.map { $0.credentials.bareJid }) let credentialsJids = Set(credentials.map { $0.bareJid }) let forAdd = credentials.filter { !existsJids.contains($0.bareJid) } let newClients = forAdd.map { Client(credentials: $0) } let forRemove = clients.filter { !credentialsJids.contains($0.credentials.bareJid) } forRemove.forEach { $0.disconnect() } var updatedClients = clients.filter { credentialsJids.contains($0.credentials.bareJid) } updatedClients.append(contentsOf: newClients) clients = updatedClients } private func client(for credentials: Credentials) -> Client? { clients.first { $0.credentials == credentials } } } extension ClientsStore { func tryLogin(_ jidStr: String, _ pass: String) async throws { // login with fake timeout async let sleep: Void? = try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC) async let request = try await Client.tryLogin(with: .init(bareJid: jidStr, pass: pass, isActive: true)) let client = try await(request, sleep).0 clients.append(client) try? await client.credentials.save() } func reconnectAll() { Task { await withTaskGroup(of: Void.self) { taskGroup in for client in clients { taskGroup.addTask { await client.connect() } } } } } } extension ClientsStore { var actualRosters: [Roster] { get async { var allRosters: [Roster] = [] for client in clients { allRosters.append(contentsOf: await client.rosters) } return allRosters } } func addRoster(_ credentials: Credentials, contactJID: String, name: String?, groups: [String]) async throws { // check that roster exist in db as locally deleted and undelete it let deletedLocally = try await Roster.fetchDeletedLocally() if var roster = deletedLocally.first(where: { $0.contactBareJid == contactJID }) { try await roster.setLocallyDeleted(false) return } // add new roster guard let client = client(for: credentials) else { throw ClientStoreError.clientNotFound } try await client.addRoster(contactJID, name: name, groups: groups) } }