Files
nucleic-remote-ios/NucleicRemote/NucleicRemote/Net/NetworkPathMonitor.swift
T
abkslmandNucleic 63a1c9dd9e Network Type Transition Optimization
Nucleic-Session: 40F8C53A-F001-423A-8607-EAE181DBE14B
Co-authored-by: Nucleic <[email protected]>
2026-07-04 23:38:55 -07:00

52 lines
2.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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?() }
}
}