import Foundation import Network /// Watches the device's network path (Wi‑Fi ⇄ cellular ⇄ offline) so the app can switch transports /// the instant the link changes, instead of waiting for a dead LAN socket to time out. This is the /// missing proactive signal behind the "iOS sticks on LAN after leaving Wi‑Fi" stall: the phone's /// pinned `lanHost:lanPort` is only reachable over Wi‑Fi/wired, so on cellular the LAN candidate is a /// guaranteed stall. `HostConnection` skips LAN when `canUseLAN` is false and re‑plans on `onChange`. @MainActor final class NetworkPathMonitor: ObservableObject { /// True when a LAN‑capable path (Wi‑Fi or wired Ethernet) is available — the only paths on which /// the pinned `lanHost:lanPort` could resolve. Cellular‑only ⇒ false, so the LAN candidate is /// dropped and we dial tailnet/relay straight away. Defaults optimistic so a not‑yet‑started /// monitor never spuriously suppresses LAN. @Published private(set) var canUseLAN = true /// True when any network path is available at all (used to decide whether an immediate reconnect /// is even worth attempting). @Published private(set) var isSatisfied = true /// Fired on the main actor whenever a *relevant* aspect of the path changed — LAN availability or /// overall reachability. The cue for connections to re‑plan their transport immediately rather /// than waiting out a reconnect backoff. var onChange: (() -> Void)? private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "nucleic.remote.path") private var started = false func start() { guard !started else { return } started = true monitor.pathUpdateHandler = { [weak self] path in Task { @MainActor in self?.apply(path) } } monitor.start(queue: queue) } /// Re‑read the current path synchronously — used on foreground, where an update that landed while /// the app was suspended may not have been redelivered yet. func refresh() { apply(monitor.currentPath) } private func apply(_ path: NWPath) { let lan = path.status == .satisfied && (path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet)) let satisfied = path.status == .satisfied let changed = lan != canUseLAN || satisfied != isSatisfied canUseLAN = lan isSatisfied = satisfied if changed { onChange?() } } }