import Foundation import Network import OSLog /// One framed message connection (either side). Callbacks arrive on the main actor. final class Link: @unchecked Sendable { let connection: NWConnection private let queue: DispatchQueue private var framer = MessageFramer() // queue only private let log = Logger(subsystem: "studio.zielke.musubi", category: "remote") var onMessage: (@MainActor (RemoteMessage) -> Void)? var onState: (@MainActor (NWConnection.State) -> Void)? init(connection: NWConnection, queue: DispatchQueue) { self.connection = connection self.queue = queue } func start() { connection.stateUpdateHandler = { [weak self] state in guard let self else { return } let handler = onState Task { @MainActor in handler?(state) } } connection.start(queue: queue) receive() } func cancel() { connection.cancel() } func send(_ message: RemoteMessage) { do { let data = try MessageFramer.encode(message) connection.send(content: data, completion: .contentProcessed { [log] error in if let error { log.error("Send failed: \(error.localizedDescription, privacy: .public)") } }) } catch { log.error("Encode failed: \(error.localizedDescription, privacy: .public)") } } private func receive() { connection.receive(minimumIncompleteLength: 1, maximumLength: 65_536) { [weak self] data, _, isComplete, error in guard let self else { return } if let data, !data.isEmpty { do { let messages = try framer.append(data) let handler = onMessage if !messages.isEmpty { Task { @MainActor in messages.forEach { handler?($0) } } } } catch { log.error("Framing error, closing: \(String(describing: error), privacy: .public)") connection.cancel() return } } if isComplete || error != nil { connection.cancel() return } receive() } } } extension NWParameters { /// TCP bound to one interface, with Bonjour peer-to-peer off (technical network only). static func musubi(interface: NWInterface?) -> NWParameters { let parameters = NWParameters.tcp parameters.includePeerToPeer = false if let interface { parameters.requiredInterface = interface } if let tcp = parameters.defaultProtocolStack.transportProtocol as? NWProtocolTCP.Options { tcp.enableKeepalive = true tcp.keepaliveIdle = 2 tcp.keepaliveInterval = 1 tcp.keepaliveCount = 3 tcp.noDelay = true } return parameters } }