diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index 893e7d0..d72948e 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -483,6 +483,27 @@ final class HostConnection { Task { await client.send(msg) } } + /// The device's network path changed (Wi-Fi ⇄ cellular, joined/left a network). React now rather + /// than waiting for a zombie LAN socket to time out or the reconnect backoff to elapse — this is + /// what makes the transport switch feel immediate. Only reconnect-managed hosts (a pinned host) + /// re-plan here; a host mid-pairing runs its own one-shot candidate chain. + func networkPathChanged() { + guard let host = pinnedHost else { return } + // Using (or dialing) LAN but LAN is no longer reachable → the socket is dead in all but name. + // Drop it and re-plan now; `buildCandidates` skips LAN and dials tailnet/relay. + if activeTransport == .lan, !pathMonitor.canUseLAN { + reconnectAttempts = 0 + reconnect(to: host) + return + } + // Offline/reconnecting and the network just came back or changed → retry immediately instead + // of sitting out the remaining backoff. + if !connectivity.isLive, pathMonitor.isSatisfied { + reconnectAttempts = 0 + reconnect(to: host) + } + } + private func scheduleRetry(host: PairedHost?) { guard let host else { return } reconnectAttempts += 1