Merge nucleic/lucid-glass-bison-viwq into dev
This commit is contained in:
@@ -1097,10 +1097,18 @@ final class HostConnection {
|
||||
reconnect(to: host)
|
||||
return
|
||||
}
|
||||
// Live over a fallback transport (tailnet/relay) and a LAN path just reappeared (rejoined the
|
||||
// Mac's Wi-Fi) → the return leg of the switch above. The tunnel survives the interface change
|
||||
// on its own, so nothing else would ever re-plan back down to the direct path — probe LAN and
|
||||
// swap to it if the Mac actually answers here.
|
||||
// Live over a fallback transport (tailnet/relay) and the path just changed. On cellular this
|
||||
// is a tower roam that may have rebound our IP and silently killed the WebSocket — the tunnel
|
||||
// does NOT reliably survive an IP rebind, contrary to the interface-swap case. Don't trust the
|
||||
// stale `.connected`: fast-probe it and re-dial the instant it fails to answer (sub-second),
|
||||
// rather than waiting out the 25s relay ping / 55s keepalive / URLSession's own detection.
|
||||
// `revalidate` also runs `maybeUpgradeToLAN`, so the LAN return leg (rejoined the Mac's Wi-Fi)
|
||||
// is still covered here.
|
||||
if connectivity.isLive, activeTransport != .lan {
|
||||
revalidate()
|
||||
return
|
||||
}
|
||||
// Live over LAN (or a path change with nothing to re-probe) → just try the LAN return leg.
|
||||
maybeUpgradeToLAN()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@ final class NetworkPathMonitor: ObservableObject {
|
||||
private let queue = DispatchQueue(label: "nucleic.remote.path")
|
||||
private var started = false
|
||||
|
||||
/// A coarse fingerprint of the last path, so a *same-class* transition (a cellular tower roam
|
||||
/// that reprovisions the pdp interface, a Wi‑Fi interface swap) fires `onChange` too — not just
|
||||
/// the LAN/satisfied flips. Without this, a roam that keeps the path `satisfied` on `.cellular`
|
||||
/// leaves both booleans unchanged and the proactive re‑plan never fires, so a silently‑rebound
|
||||
/// socket is only caught by the slow reactive keepalive.
|
||||
private var lastSignature = ""
|
||||
|
||||
func start() {
|
||||
guard !started else { return }
|
||||
started = true
|
||||
@@ -43,9 +50,18 @@ final class NetworkPathMonitor: ObservableObject {
|
||||
let lan = path.status == .satisfied
|
||||
&& (path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet))
|
||||
let satisfied = path.status == .satisfied
|
||||
let changed = lan != canUseLAN || satisfied != isSatisfied
|
||||
// Also move on a same‑class transition (cellular tower roam, Wi‑Fi interface swap) so a live
|
||||
// relay/tailnet tunnel gets re‑validated instead of waiting out socket‑death detection. The
|
||||
// interface identity + expensiveness/constrained flags are what `NWPath` surfaces on a
|
||||
// handoff; the IP itself isn't exposed, so this is best‑effort — the follow‑on probe is the
|
||||
// gate, and it's cheap on a still‑healthy link.
|
||||
let signature = "\(path.status)|"
|
||||
+ path.availableInterfaces.map { "\($0.name)#\($0.index)" }.joined(separator: ",")
|
||||
+ "|\(path.isExpensive)|\(path.isConstrained)"
|
||||
let changed = lan != canUseLAN || satisfied != isSatisfied || signature != lastSignature
|
||||
canUseLAN = lan
|
||||
isSatisfied = satisfied
|
||||
lastSignature = signature
|
||||
if changed { onChange?() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user