From 030d814ffeb6a9c41a48bf2489af936b823ca6c9 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sat, 4 Jul 2026 23:39:08 -0700 Subject: [PATCH] Network Type Transition Optimization Nucleic-Session: 40F8C53A-F001-423A-8607-EAE181DBE14B Co-authored-by: Nucleic --- .../NucleicRemote/Net/NWFrameChannel.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Net/NWFrameChannel.swift b/NucleicRemote/NucleicRemote/Net/NWFrameChannel.swift index ce62ed0..3130d05 100644 --- a/NucleicRemote/NucleicRemote/Net/NWFrameChannel.swift +++ b/NucleicRemote/NucleicRemote/Net/NWFrameChannel.swift @@ -28,7 +28,15 @@ final class NWFrameChannel: FrameChannel, @unchecked Sendable { } init(endpoint: NWEndpoint) { - let params = NWParameters.tcp + // Short TCP keepalive so a silently-dead LAN link (Wi-Fi dropped, Mac left the subnet) is + // detected in ~15s instead of the OS default of minutes — the belt to the path-monitor's + // braces for the case where a path still exists but the route to the Mac is gone. + let tcp = NWProtocolTCP.Options() + tcp.enableKeepalive = true + tcp.keepaliveIdle = 5 + tcp.keepaliveInterval = 5 + tcp.keepaliveCount = 2 + let params = NWParameters(tls: nil, tcp: tcp) params.includePeerToPeer = true connection = NWConnection(to: endpoint, using: params) peerDescription = "\(endpoint)" @@ -39,6 +47,13 @@ final class NWFrameChannel: FrameChannel, @unchecked Sendable { connection.stateUpdateHandler = { [weak self] state in switch state { case .ready: self?.markReady(); self?.onReady?() + case .waiting(let error): + // No usable path right now (Wi-Fi dropped, host unreachable). NWConnection would sit + // in `.waiting` indefinitely, holding the transport hostage — the core of the "still + // on LAN after leaving Wi-Fi" stall. Fail fast so the store falls straight through to + // tailnet/relay. (Healthy connections go setup→preparing→ready and never hit this; + // the connect-timeout guard covers a wrong-network socket that stays `.preparing`.) + self?.onFailed?("waiting: \(error)"); self?.continuation.finish() case .failed(let error): self?.onFailed?("\(error)"); self?.continuation.finish() case .cancelled: self?.continuation.finish() default: break