import Foundation // MARK: Events enum Event { case startClientLogin(jid: JID, credsId: UUID) case resolveDomain case domainResolved([SRVRecord]) case domainResolvingError(SRVResolverError) case tryConnect case socketConnected(SocketType) case socketDisconnected case socketError(Error) case socketReceived(Data) case allRecordsUnreachable case startStream case streamStarted(args: [String: String]) case streamEnded case parserError(Error) case xmlInbound(XMLElement) case xmlOutbound(XMLElement) case startTls case startTlsDone case startTlsFailed(Error) case gotAuthError(AuthorizationError) case startAuth(XMLElement) case challengeAuth(XMLElement) case authDone(sasl: SaslType, args: [String: String]) case stanzaInbound(Stanza) case stanzaOutbound(Stanza) case bindStream case bindStreamDone(String) case bindStreamError case streamReady } // MARK: State struct ClientState: Codable & Equatable { var jid: JID var credentialsId: UUID var userAgent: UserAgent var sessionState: SessionState var srvRecords: [SRVRecord] var srvRecordIndex: Int var socketType: SocketType var isSocketSecured: Bool var streamId: String // for allow self-signed or expired certificates // not secure, but sometimes needed var allowInsecure: Bool var allowPlainAuth: Bool var authorizationStep: AuthorizationStep var isStreamBound: Bool static var initial: ClientState { // swiftlint:disable:next force_try let initJid = try! JID("need@initiali.ze") return .init( jid: initJid, credentialsId: UUID(), userAgent: .init(uuid: "", software: "", device: ""), sessionState: .waitingSRVRecords, srvRecords: [], srvRecordIndex: -1, socketType: .startTls, isSocketSecured: false, streamId: "", allowInsecure: false, allowPlainAuth: false, authorizationStep: .notAuthorized, isStreamBound: false ) } } final class XMPPClient { private var state = ClientState.initial private let logger = ClientLogger() private let storage: XMPPClientStorageProtocol private lazy var modules: [any XmppModule] = [ SRVResolverModule(), ConnectionModule(self.fire), ParserModule(self.fire), SessionModule(), AuthorizationModule(self.storage), StanzaModule(self.storage), DiscoveryModule() ] init(storage: any XMPPClientStorageProtocol, userAgent: UserAgent) { self.storage = storage state.userAgent = userAgent } func tryLogin(jid: JID, credentialsId: UUID) { logger.update(jid.description) Task { await fire(.startClientLogin(jid: jid, credsId: credentialsId)) } } } // MARK: Private part private extension XMPPClient { private func fire(_ event: Event) async { // log logger.logEvent(event) // apply reducing let newState = modules.reduce(state) { result, next in next.reduce(oldState: result, with: event) } logger.logState(state, newState) state = newState // apply side effects await withTaskGroup(of: Event?.self) { [state] group in for mod in modules { group.addTask { await mod.process(state: state, with: event) } } for await case let nextEvent? in group { await fire(nextEvent) } } } }