Merge nucleic/plucky-ancient-koala-2qbh into dev

This commit is contained in:
2026-07-14 19:54:13 -07:00
parent b2f1dc65e9
commit f291392900
2 changed files with 78 additions and 31 deletions
@@ -9,7 +9,7 @@ import UIKit
/// One phone→Mac connection and its projected state (mesh P3 multiplexer foundation).
///
/// This is the per-host connection engine extracted from `RemoteStore`: it owns the `SyncClient`,
/// the LAN→tailnet candidate chain, reconnect backoff, and the event stream for a single paired
/// the Relay-first candidate chain, reconnect backoff, and the event stream for a single paired
/// Mac, and it keeps that Mac's projection (connectivity, sessions, dashboard, capabilities, …).
/// `RemoteStore` will own one of these per paired host (`[HostID: HostConnection]`), aggregating
/// their state and routing intents to the right one; aggregate concerns (badge, Live Activity,
@@ -151,6 +151,10 @@ final class HostConnection {
/// In-flight LAN-reachability probe (the tailnet/relay → LAN return leg). Nil unless a probe is
/// running; at most one at a time so repeated path-change events don't stack.
private var lanUpgradeProbe: Task<Void, Never>?
/// A successful reachability probe causes one LAN-first authenticated reconnect. If that full
/// dial falls back to Relay, consume this flag there and proceed to STUN instead of probing the
/// same accepting-but-unusable endpoint forever.
private var skipNextLANOptimization = false
private var reconnectAttempts = 0
private var seenSeq: Set<UInt64> = []
@@ -176,8 +180,7 @@ final class HostConnection {
private enum TransportAttempt {
case lan(NWEndpoint)
case tailnet(host: String, port: UInt16)
/// Covalence Relay (mesh P2) — always the last candidate: works from anywhere,
/// but a direct path beats a brokered one when both exist.
/// Covalence Relay (mesh P2) — the immediate baseline whenever admission is available.
case relay(base: URL, membershipToken: String)
}
@@ -234,10 +237,12 @@ final class HostConnection {
_ = tryNextCandidate()
}
/// Reconnect to the pinned host using IK: LAN when reachable, else the pairing's tailnet hint.
/// `preferRelay` reorders the candidates to try the Nucleic Edge first (a backgrounded/locked
/// App Intent resolving an approval — see `buildCandidates`); the direct paths stay as fallbacks.
func reconnect(to host: PairedHost, preferRelay: Bool = false) {
/// Reconnect to the pinned host using IK. Relay admission always establishes the baseline first;
/// LAN and STUN optimization happen only after that session is usable. `preferLAN` is reserved
/// for a LAN endpoint just proven while Relay remained live.
func reconnect(
to host: PairedHost, preferRelay: Bool = false, preferLAN: Bool = false
) {
teardown()
pinnedHost = host
connectivity = reconnectAttempts == 0 ? .connecting : .reconnecting
@@ -248,7 +253,7 @@ final class HostConnection {
lanHost: host.lanHost, lanPort: host.lanPort,
tailnet: host.transportHint == .tailnet ? (host.tailnetHost, host.tailnetPort) : nil,
relay: (host.relayMembershipToken, host.relayURL),
preferRelay: preferRelay)
preferRelay: preferRelay, preferLAN: preferLAN)
guard !candidates.isEmpty else {
connectivity = .hostOffline
callbacks.didUpdate()
@@ -319,32 +324,33 @@ final class HostConnection {
fingerprint: String?, lanHost: String?, lanPort: UInt16?,
tailnet: (host: String?, port: UInt16?)?,
relay: (membershipToken: String?, url: String?)? = nil,
preferRelay: Bool = false
preferRelay: Bool = false, preferLAN: Bool = false
) -> [TransportAttempt] {
let relayCandidate: TransportAttempt? = {
guard let relay, let token = relay.membershipToken, !token.isEmpty else { return nil }
return .relay(base: RelayAPI.baseURL(relay.url), membershipToken: token)
}()
var candidates: [TransportAttempt] = []
// Relay-first: a backgrounded/locked App Intent (a Live Activity approve/deny) can't rely on
// the local network — iOS restricts LAN/Bonjour for a process launched into the background,
// so the LAN attempt just burns its connect timeout. Dial the Nucleic Edge first and keep the
// direct paths as fallbacks. Only when the account opted into cloud approvals (SyncedSettings).
if preferRelay, let relayCandidate { candidates.append(relayCandidate) }
var directCandidates: [TransportAttempt] = []
// Only offer LAN when the device actually has a LAN-capable path (Wi-Fi/wired). On cellular
// the pinned `lanHost:lanPort` is unreachable, so including it here would just burn the
// connect timeout before falling through — skip it and dial tailnet/relay immediately.
if pathMonitor.canUseLAN,
let endpoint = discovery.endpoint(forFingerprint: fingerprint, lanHost: lanHost, lanPort: lanPort) {
candidates.append(.lan(endpoint))
directCandidates.append(.lan(endpoint))
}
if let tailnet, let host = tailnet.host, let port = tailnet.port, TailnetSupport.isBuiltIn {
candidates.append(.tailnet(host: host, port: port))
directCandidates.append(.tailnet(host: host, port: port))
}
// Default order: relay is the last resort — a direct path beats a brokered one when both
// exist. `preferRelay` already placed it first, so don't add it twice.
if !preferRelay, let relayCandidate { candidates.append(relayCandidate) }
return candidates
// `preferRelay` is retained for existing App-Intent callers, but Relay is now first for all
// callers whenever it is configured. A proven LAN promotion gets one LAN-first attempt with
// Relay immediately behind it; all other direct candidates remain failure fallbacks.
_ = preferRelay
if preferLAN, let first = directCandidates.first,
case .lan = first, let relayCandidate {
return [first, relayCandidate] + Array(directCandidates.dropFirst())
}
if let relayCandidate { return [relayCandidate] + directCandidates }
return directCandidates
}
private func tryNextCandidate() -> Bool {
@@ -580,7 +586,7 @@ final class HostConnection {
// that predates the field omits it, and we keep whatever we already had.
if let settings = welcome.settings { callbacks.settingsChanged(settings) }
if let payload = pairingPayload, let hostKey = await client?.hostKey() {
callbacks.didPair(PairedHost(
let paired = PairedHost(
deviceID: IdentityStore.deviceID(), hostName: welcome.host.hostName,
hostStaticKey: hostKey, fingerprint: hostKey.fingerprintHex,
lanHost: payload.lanHost, lanPort: payload.lanPort,
@@ -588,10 +594,16 @@ final class HostConnection {
tailnetPort: payload.tailnetPort,
relayRoomID: payload.relayRoomID,
relayMembershipToken: payload.relayMembershipToken,
relayURL: payload.relayURL))
relayURL: payload.relayURL)
pinnedHost = paired
callbacks.didPair(paired)
}
callbacks.didUpdate()
await startDirectUpgradeIfPossible(welcome: welcome)
if activeTransport == .relay, let client {
optimizeRelayConnection(welcome: welcome, client: client)
} else {
skipNextLANOptimization = false
}
send(.listSessions)
send(.listDashboard)
// Mesh casting: subscribe with the merged ledger's cursors (every origin we hold) —
@@ -859,6 +871,41 @@ final class HostConnection {
}
}
/// Keep the authenticated Relay session available while checking the cheapest path. A proven
/// LAN endpoint gets one LAN-first reconnect; otherwise STUN begins. Failure at either stage
/// leaves or returns the connection on Relay.
private func optimizeRelayConnection(welcome: Welcome, client: SyncClient) {
guard self.client === client else { return }
if skipNextLANOptimization {
skipNextLANOptimization = false
Task { [weak self] in await self?.startDirectUpgradeIfPossible(welcome: welcome) }
return
}
guard let host = pinnedHost, pathMonitor.canUseLAN,
let endpoint = discovery.endpoint(
forFingerprint: host.fingerprint, lanHost: host.lanHost, lanPort: host.lanPort)
else {
Task { [weak self] in await self?.startDirectUpgradeIfPossible(welcome: welcome) }
return
}
lanUpgradeProbe?.cancel()
lanUpgradeProbe = Task { [weak self, weak client] in
let reachable = await Self.probeReachable(endpoint)
guard let self else { return }
self.lanUpgradeProbe = nil
guard !Task.isCancelled, let client, self.client === client,
self.connectivity.isLive, self.activeTransport == .relay
else { return }
if reachable, let host = self.pinnedHost {
self.skipNextLANOptimization = true
self.reconnectAttempts = 0
self.reconnect(to: host, preferLAN: true)
} else {
await self.startDirectUpgradeIfPossible(welcome: welcome)
}
}
}
/// The live data path migrated (direct upgrade / failback). Reflect it in the status chip
/// without disturbing the relay-backed session underneath.
private func directPathChanged(_ hint: SyncTransportHint) {
@@ -979,7 +1026,8 @@ final class HostConnection {
let host = self.pinnedHost
else { return }
self.reconnectAttempts = 0
self.reconnect(to: host) // rebuilds the candidate chain LAN-first
self.skipNextLANOptimization = true
self.reconnect(to: host, preferLAN: true)
}
}
@@ -957,8 +957,8 @@ final class RemoteStore: ObservableObject {
/// drop and is asking us to re-dial over a non-local transport before the connection silently
/// goes dark. Called from `PushAppDelegate` on a `content-available` background push, which may
/// relaunch the app with no scene, so this does the setup `onAppear` normally would. Re-running
/// `reconnect()` walks each host's LAN→tailnet→relay candidate loop — LAN just failed, so it
/// lands on tailnet or relay from wherever the phone now is. Holds a background-task assertion
/// `reconnect()` establishes Relay first when available, so it avoids retrying the LAN link that
/// just failed and becomes usable from wherever the phone now is. Holds a background-task assertion
/// until a host is live again or the window closes. Best-effort: iOS may deny background runtime
/// (budget, force-quit), in which case the reconnect simply happens on the next foreground.
func handleReconnectWake(completion: @escaping () -> Void) {
@@ -1643,13 +1643,12 @@ final class RemoteStore: ObservableObject {
}
/// Connect to every paired Mac at once (mesh P3 multiplexer): each `HostConnection` runs its own
/// IK reconnect (LAN→tailnet, with backoff), so all your Macs are live simultaneously and the
/// IK reconnect (Relay-first when configured, with backoff), so all your Macs are live simultaneously and the
/// switcher flips between them instantly. Idempotent — an already-live connection is left alone;
/// an offline one (re)dials. Connections for since-unpaired Macs are dropped. The launch +
/// "Reconnect" path.
/// `preferRelay` reorders each dialing host's candidates to try the Nucleic Edge first — set
/// only from a backgrounded/locked App Intent (a Live Activity approve/deny) where the local
/// network is unreliable. The normal launch/foreground path leaves it false (direct-first).
/// `preferRelay` remains as a source-compatible hint for App Intent callers; Relay-enabled
/// hosts now use that fast baseline for foreground and background reconnects alike.
func reconnect(preferRelay: Bool = false) {
let hosts = IdentityStore.pairedHosts()
guard !hosts.isEmpty else { connectivity = .unpaired; return }