snikket-ios/XMPPSwift/Modules/SocketModule.swift
2023-09-24 02:45:40 +04:00

157 lines
4.7 KiB
Swift

import Foundation
import Combine
import Network
enum SocketStatus: Codable {
case disconnected
case connected
}
enum SocketError: Error, Codable {
case connectionInitError
}
enum SocketAction: ModuleAction {
var moduleName: ModuleName {
"Socket"
}
case tryConnect(host: String, port: Int)
case connectionUpdated(_ status: SocketStatus)
case connectionError(_ err: SocketError)
}
struct SocketState: ModuleState {
var moduleName: ModuleName { "Socket" }
var socketStatus: SocketStatus = .disconnected
}
class SocketModule: Module, ModuleContinuousActing {
var moduleName: ModuleName { "Socket" }
var asyncDispatch: ((any ModuleAction) -> ())?
private let socketQueue = DispatchQueue.init(label: "socket.queue.\(UUID().uuidString)")
private var connection: NWConnection?
func getInitState() -> OptionalState {
SocketState.init()
}
func reduce(state: inout any ModuleState, action: any ModuleAction) {
guard let action = action as? SocketAction, var newState = state as? SocketState else {
return
}
switch action {
case .connectionUpdated(let status):
if newState.socketStatus != status {
newState.socketStatus = status
}
default:
break
}
state = newState
}
func middleware(state: GlobalState, action: any ModuleAction) -> AnyPublisher<any ModuleAction, Never> {
switch action {
case let act as SRVResolverAction:
switch act {
case .recordsFound:
if let founded = (state["Resolver"] as? SRVResolverState)?.foundedRecords, let record = founded.first {
return Just(SocketAction.tryConnect(host: record.target, port: record.port))
.eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
default:
return Empty().eraseToAnyPublisher()
}
case let act as SocketAction:
switch act {
case .tryConnect(let host, let port):
do {
try startConnection(host: host, port: port)
return Empty().eraseToAnyPublisher()
} catch {
return Just(SocketAction.connectionError(.connectionInitError))
.eraseToAnyPublisher()
}
default:
return Empty().eraseToAnyPublisher()
}
default:
return Empty().eraseToAnyPublisher()
}
}
}
private extension SocketModule {
private func startConnection(host: String, port: Int) throws {
// options and params
let tcpOptions = NWProtocolTCP.Options()
tcpOptions.noDelay = true
tcpOptions.connectionTimeout = 5
tcpOptions.enableFastOpen = true
tcpOptions.disableAckStretching = true
let params = NWParameters(tls: nil, tcp: tcpOptions)
params.serviceClass = .responsiveData
// connection
connection = NWConnection(host: .name(host, nil), port: .init(integerLiteral: UInt16(port)), using: params)
guard let connection = connection else {
throw SocketError.connectionInitError
}
// subscribe to state
connection.stateUpdateHandler = { [weak self] state in
switch state {
case .cancelled, .setup:
self?.asyncDispatch?(SocketAction.connectionUpdated(.disconnected))
case .preparing:
self?.asyncDispatch?(SocketAction.connectionUpdated(.disconnected))
case .failed(let error):
self?.asyncDispatch?(SocketAction.connectionUpdated(.disconnected))
case .waiting(let error):
self?.asyncDispatch?(SocketAction.connectionUpdated(.disconnected))
case .ready:
self?.asyncDispatch?(SocketAction.connectionUpdated(.connected))
self?.read()
default:
break
}
}
//
connection.start(queue: socketQueue)
}
func write(data: Data) {
connection?.send(content: data, completion: .contentProcessed({ err in
if let err = err {
print(err)
}
}))
}
private func read() {
connection?.receive(minimumIncompleteLength: 1, maximumLength: 4096 * 2, completion: { [weak self] data, _, complete, error in
if let data = data, complete == true {
print(String(bytes: data, encoding: .utf8) ?? "???")
self?.read()
} else if let err = error {
print(err)
}
})
}
}