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
+63 -19
View File
@@ -6,6 +6,11 @@ import { getRuntimeKey, switchRuntimeEndpoint } from '@/lib/runtime-switch';
// Let the post-switch bootstrap traffic settle before the background refresh.
const CANDIDATE_REFRESH_DELAY_MS = 5_000;
// How long the stored direct address keeps startup to itself before the relay
// takes over. A live LAN probe answers well inside this window; a dead one no
// longer stalls startup for the probe's full timeout.
const DIRECT_PROBE_HEADSTART_MS = 1_500;
let candidateRefreshInFlight = false;
/**
@@ -120,28 +125,67 @@ export const restoreDesktopRelayRuntime = async (targetHostId?: string): Promise
const runtimeKey = `host:${host.id}`;
if (getRuntimeKey() === runtimeKey) return;
const switchToDirect = (url: string) => {
switchRuntimeEndpoint({
apiBaseUrl: url,
clientToken: host.clientToken || null,
requestHeaders: host.requestHeaders || null,
runtimeKey,
});
};
const switchToRelay = () => {
switchRuntimeEndpoint({
apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
clientToken: host.clientToken || null,
runtimeKey,
relay: host.relay ?? undefined,
});
// On the relay because the stored direct address did not answer (yet) —
// ask the server for its current LAN address in the background and
// hot-switch back to direct if it simply moved (DHCP re-lease).
scheduleDesktopHostCandidateRefresh(host.id);
};
const directUrl = host.apiUrl ? normalizeHostUrl(getDesktopHostApiUrl(host)) : null;
if (directUrl) {
const probe = await desktopHostProbe(directUrl, { clientToken: host.clientToken || null, requestHeaders: host.requestHeaders || null })
.catch(() => ({ status: 'unreachable' as const, latencyMs: 0 }));
if (probe.status !== 'unreachable' && probe.status !== 'wrong-service' && probe.status !== 'incompatible') {
switchRuntimeEndpoint({
apiBaseUrl: directUrl,
clientToken: host.clientToken || null,
requestHeaders: host.requestHeaders || null,
runtimeKey,
});
if (!directUrl) {
switchToRelay();
return;
}
// Race the direct probe against a short headstart instead of serializing the
// full probe timeout in front of the relay fallback: a live LAN answers well
// inside the window (direct keeps priority); a dead one no longer delays
// startup — the relay takes over and a late direct success hot-switches back
// (stable runtimeKey → transport-only swap, same as the candidate refresh).
const probeOk = (probe: { status: string }) =>
probe.status !== 'unreachable' && probe.status !== 'wrong-service' && probe.status !== 'incompatible';
const probePromise = desktopHostProbe(directUrl, {
clientToken: host.clientToken || null,
requestHeaders: host.requestHeaders || null,
// Identity gate: a re-leased LAN address may now belong to a different
// machine; the probe must not send the token on a serverId mismatch.
expectedServerId: host.relay.serverId,
}).catch(() => ({ status: 'unreachable' as const, latencyMs: 0 }));
const winner = await Promise.race([
probePromise,
new Promise<null>((resolve) => setTimeout(() => resolve(null), DIRECT_PROBE_HEADSTART_MS)),
]);
if (winner) {
if (probeOk(winner)) {
switchToDirect(directUrl);
return;
}
switchToRelay();
return;
}
switchRuntimeEndpoint({
apiBaseUrl: typeof window !== 'undefined' ? window.location.origin : '',
clientToken: host.clientToken || null,
runtimeKey,
relay: host.relay,
// Headstart expired: connect via relay now; adopt the direct transport if the
// still-running probe succeeds a moment later.
switchToRelay();
void probePromise.then((probe) => {
if (!probeOk(probe)) return;
if (getRuntimeKey() !== runtimeKey) return; // user switched away meanwhile
switchToDirect(directUrl);
});
// Landed on the relay because the stored direct address failed — ask the
// server for its current LAN address in the background and hot-switch back
// to direct if it simply moved (DHCP re-lease).
scheduleDesktopHostCandidateRefresh(host.id);
};