perf: fast relay connect on mobile and desktop + connect splash + edit-safe instances

Relay connect used to serialize a dead LAN probe (up to 8s per stale address
on mobile, 2-4s on desktop) in front of the relay attempt, then paid a second
WebSocket connect + E2EE handshake because the probe tunnel was thrown away.

- mobile probeConnectionCandidates: race the relay probe against the direct
  chain with a 1.5s direct headstart; a live LAN still wins, a dead one no
  longer delays startup
- relay probes adopt their tunnel as the runtime tunnel (adoptRelayTunnel)
  instead of dialing a fresh one — applies to auto-connect, pairing redeem,
  password login, and the desktop host switcher's relay fallback
- relay probe drops the /health round-trip: the E2EE handshake already proves
  the server identity, /auth/session alone proves liveness and auth
- desktop restoreDesktopRelayRuntime: same headstart race; a late direct
  success hot-switches back (stable runtimeKey); startup probe now passes
  expectedServerId so a re-leased LAN address never sees the token
- launch splash shows 'Connecting to device: <label>' with animated dots
  under the (still centered) logo, translated in all locales
- editing a saved instance no longer rebuilds it from the URL field alone:
  the id is passed through, relay/https candidates are preserved, and a
  token-key change migrates the Keychain token instead of orphaning it
This commit is contained in:
Bohdan Triapitsyn
2026-07-13 01:29:35 +03:00
parent 08e4ce5b99
commit 9b645f0e53
16 changed files with 372 additions and 104 deletions
@@ -32,6 +32,8 @@ import {
type HostProbeResult,
} from '@/lib/desktopHosts';
import { scheduleDesktopHostCandidateRefresh } from '@/lib/desktopRelayRestore';
import { adoptRelayTunnel } from '@/lib/relay/runtime-tunnel';
import { createRelayTunnelClient } from '@/lib/relay/tunnel-client';
import { getRuntimeApiBaseUrl, getRuntimeKey, subscribeRuntimeEndpointChanged, switchRuntimeEndpoint } from '@/lib/runtime-switch';
import {
desktopSshConnect,
@@ -513,7 +515,13 @@ export function DesktopHostSwitcherDialog({
// Relay legs ride the E2EE tunnel activated in-renderer via
// switchRuntimeEndpoint({ relay }); the runtime fetch/socket layers route
// through the tunnel from the singleton registry.
const activateRelay = (relay: NonNullable<DesktopHost['relay']>) => {
const activateRelay = (relay: NonNullable<DesktopHost['relay']>, liveTunnel?: ReturnType<typeof createRelayTunnelClient>) => {
// Adopt the probe's live tunnel (when it kept one) BEFORE the switch: the
// activate call inside switchRuntimeEndpoint sees an equal descriptor and
// reuses it — no second WebSocket connect + E2EE handshake.
if (liveTunnel) {
adoptRelayTunnel({ relayUrl: relay.relayUrl, serverId: relay.serverId, hostEncPubJwk: relay.hostEncPubJwk }, liveTunnel);
}
switchRuntimeEndpoint({
apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
clientToken: host.clientToken || null,
@@ -562,11 +570,14 @@ export function DesktopHostSwitcherDialog({
finalStatus = { status: probe.status, latencyMs: probe.latencyMs };
if (!isBlockedHostStatus(probe.status)) transport = 'direct';
}
let relayProbeTunnel: ReturnType<typeof createRelayTunnelClient> | undefined;
if (!transport && host.relay) {
const probe = await probeRelayDesktopHost(host.relay).catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
const probe = await probeRelayDesktopHost(host.relay, { keepTunnel: true })
.catch((): HostProbeResult => ({ status: 'unreachable', latencyMs: 0 }));
if (probe.status === 'ok') {
finalStatus = { status: probe.status, latencyMs: probe.latencyMs, via: 'relay' };
transport = 'relay';
relayProbeTunnel = 'tunnel' in probe ? probe.tunnel : undefined;
}
}
setStatusById((prev) => ({ ...prev, [host.id]: finalStatus }));
@@ -577,7 +588,7 @@ export function DesktopHostSwitcherDialog({
return;
}
if (transport === 'relay' && host.relay) {
activateRelay(host.relay);
activateRelay(host.relay, relayProbeTunnel);
} else {
switchRuntimeEndpoint({ apiBaseUrl: apiOrigin, clientToken: clientToken || null, requestHeaders: host.requestHeaders || null, runtimeKey: runtimeKeyForHost(host) });
}