Zum Inhalt

apple/PhysarumApp/Sources/Remote/RemoteLink.swift

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()
        }
    }
}