import Foundation
import Network
/// Wraps a single `NWConnection`: frames `RemoteMessage`s out (`RemoteFraming.encode`), reassembles
/// them in (`RemoteFraming.Decoder`), and delivers each decoded message + connection-state change on
/// the **main queue** (the model is main-only). The decoder runs on the connection's private serial
/// queue, so there is no concurrent access to it. A corrupt/oversized frame tears the connection down
/// (it can't be resynced); malformed/zero-length frames are skipped by the decoder.
final class RemoteLink {
private let connection: NWConnection
private let queue = DispatchQueue(label: "de.jkaindl.physarum.remotelink")
private var decoder = RemoteFraming.Decoder()
/// Called on the main queue for each decoded inbound message.
var onMessage: ((RemoteMessage) -> Void)?
/// Called on the main queue when the underlying connection changes state.
var onStateChange: ((NWConnection.State) -> Void)?
init(connection: NWConnection) {
self.connection = connection
}
func start() {
connection.stateUpdateHandler = { [weak self] state in
DispatchQueue.main.async { self?.onStateChange?(state) }
}
connection.start(queue: queue)
receiveLoop()
}
func send(_ message: RemoteMessage) {
guard let data = try? RemoteFraming.encode(message) else { return }
connection.send(content: data, completion: .contentProcessed { [weak self] error in
// A send failure (e.g. Wi-Fi dropped mid-send) means the peer never got it — tear the
// connection down so the state machine reports a clean disconnect instead of a phantom
// "connected" with a lost message.
if error != nil { self?.connection.cancel() }
})
}
func cancel() {
connection.cancel()
}
private func receiveLoop() {
connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] data, _, isComplete, error in
guard let self else { return }
if let data, !data.isEmpty {
do {
let messages = try self.decoder.consume(data)
if !messages.isEmpty {
DispatchQueue.main.async { [weak self] in messages.forEach { self?.onMessage?($0) } }
}
} catch {
self.connection.cancel() // corrupt/oversized frame → tear down (the peer reconnects)
return
}
}
if isComplete || error != nil {
self.connection.cancel()
return
}
self.receiveLoop()
}
}
}