Merge nucleic/lucid-glass-bison-viwq into dev
This commit is contained in:
@@ -1097,10 +1097,18 @@ final class HostConnection {
|
|||||||
reconnect(to: host)
|
reconnect(to: host)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Live over a fallback transport (tailnet/relay) and a LAN path just reappeared (rejoined the
|
// Live over a fallback transport (tailnet/relay) and the path just changed. On cellular this
|
||||||
// Mac's Wi-Fi) → the return leg of the switch above. The tunnel survives the interface change
|
// is a tower roam that may have rebound our IP and silently killed the WebSocket — the tunnel
|
||||||
// on its own, so nothing else would ever re-plan back down to the direct path — probe LAN and
|
// does NOT reliably survive an IP rebind, contrary to the interface-swap case. Don't trust the
|
||||||
// swap to it if the Mac actually answers here.
|
// 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()
|
maybeUpgradeToLAN()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ final class NetworkPathMonitor: ObservableObject {
|
|||||||
private let queue = DispatchQueue(label: "nucleic.remote.path")
|
private let queue = DispatchQueue(label: "nucleic.remote.path")
|
||||||
private var started = false
|
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() {
|
func start() {
|
||||||
guard !started else { return }
|
guard !started else { return }
|
||||||
started = true
|
started = true
|
||||||
@@ -43,9 +50,18 @@ final class NetworkPathMonitor: ObservableObject {
|
|||||||
let lan = path.status == .satisfied
|
let lan = path.status == .satisfied
|
||||||
&& (path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet))
|
&& (path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet))
|
||||||
let satisfied = path.status == .satisfied
|
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
|
canUseLAN = lan
|
||||||
isSatisfied = satisfied
|
isSatisfied = satisfied
|
||||||
|
lastSignature = signature
|
||||||
if changed { onChange?() }
|
if changed { onChange?() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user