From b0b041f26888a74a0217aa008bc5c95a90d4e2a6 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 4 Jul 2026 23:39:36 -0700 Subject: [PATCH] Network Type Transition Optimization Nucleic-Session: 40F8C53A-F001-423A-8607-EAE181DBE14B Co-authored-by: Nucleic --- .../NucleicRemote/Models/HostConnection.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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