Network Type Transition Optimization
Nucleic-Session: 40F8C53A-F001-423A-8607-EAE181DBE14B Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
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?() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user