Nucleic-Session: 40F8C53A-F001-423A-8607-EAE181DBE14B Co-authored-by: Nucleic <[email protected]>
52 lines
2.4 KiB
Swift
52 lines
2.4 KiB
Swift
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?() }
|
||
}
|
||
}
|