From 810d791bf945e2301362ba5d4d447fc80e95775e Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 12:45:16 -0700 Subject: [PATCH] Network Reconnection Delay Nucleic-Session: 4BE3E40C-E939-444C-8AED-93C0AFE13C07 Co-authored-by: Nucleic --- .../NucleicRemote/Models/HostConnection.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index a0a929e..c58860d 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -689,6 +689,33 @@ final class HostConnection { } } + /// Foreground liveness check — the fast-reconnect path. iOS suspends the app when it's + /// backgrounded, which silently kills the socket; but no `.closed`/`.failed` reaches us while + /// we're frozen, so this connection can wake still marked `.connected`. Trusting that stale + /// state means nothing reconnects until the 55s keepalive deadline (or ~15s TCP keepalive) + /// finally notices — the several-second (or worse) stall on return. Instead: if we're already + /// not live, re-dial now with no backoff; if we *look* live, actively ping and re-dial the + /// instant the link fails to answer within a tight deadline. A pinned host only (a host + /// mid-pairing runs its own one-shot chain and has no `pinnedHost`). + func revalidate() { + guard let host = pinnedHost else { return } + guard connectivity.isLive, let client else { + reconnectAttempts = 0 + reconnect(to: host) + return + } + revalidateTask?.cancel() + revalidateTask = Task { [weak self] in + let alive = await client.probeAlive(within: .milliseconds(800)) + guard !Task.isCancelled, let self, self.client === client, + self.connectivity.isLive else { return } + if !alive { + self.reconnectAttempts = 0 + self.reconnect(to: host) + } + } + } + private func scheduleRetry(host: PairedHost?) { guard let host else { return } reconnectAttempts += 1