From 1ec448d2b99335872645be1719fc7e3e2b6c8a79 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 26 Apr 2026 08:15:06 +0700 Subject: [PATCH] fix: prevent bootstrap phase regression when an existing host comes online first When a saved host reconnects before bootstrapDesktop finishes, the phase was regressed from "online" back to "connecting". Track the race settlement and skip the setPhase("connecting") call if anyOnline already won. Also extract getEarliestOnlineHostServerId into HostRuntimeStore and use it to initialize the bootstrap phase, avoiding a flash of the starting screen. --- packages/app/src/app/_layout.tsx | 4 +- .../src/app/host-runtime-bootstrap.test.ts | 58 +++++++++++++++++++ .../app/src/app/host-runtime-bootstrap.ts | 4 +- packages/app/src/app/index.tsx | 25 ++------ packages/app/src/runtime/host-runtime.ts | 26 +++++---- 5 files changed, 84 insertions(+), 33 deletions(-) diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 94d98967a8..cd137bc88b 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -267,7 +267,9 @@ function HostSessionManager() { } function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) { - const [phase, setPhase] = useState("starting-daemon"); + const [phase, setPhase] = useState(() => + getHostRuntimeStore().getEarliestOnlineHostServerId() !== null ? "online" : "starting-daemon", + ); const [error, setError] = useState(null); const [retryToken, setRetryToken] = useState(0); const retry = useCallback(() => { diff --git a/packages/app/src/app/host-runtime-bootstrap.test.ts b/packages/app/src/app/host-runtime-bootstrap.test.ts index 1653a5f897..05ac0ad197 100644 --- a/packages/app/src/app/host-runtime-bootstrap.test.ts +++ b/packages/app/src/app/host-runtime-bootstrap.test.ts @@ -42,4 +42,62 @@ describe("initializeHostRuntime", () => { expect(setPhase).toHaveBeenLastCalledWith("online"); expect(setError).toHaveBeenLastCalledWith(null); }); + + it("does not regress the phase to connecting when anyOnline wins the race before bootstrapDesktop returns", async () => { + let resolveBootstrapDesktop!: () => void; + const bootstrapDesktopGate = new Promise((resolve) => { + resolveBootstrapDesktop = resolve; + }); + + let resolveAddConnection!: () => void; + const addConnectionCompleted = new Promise((resolve) => { + resolveAddConnection = resolve; + }); + + const setPhase = vi.fn(); + const setError = vi.fn(); + + const initPromise = initializeHostRuntime({ + shouldManageDesktop: true, + loadSettings: async () => ({ + theme: "auto", + sendBehavior: "interrupt", + manageBuiltInDaemon: true, + releaseChannel: "stable", + }), + store: { + loadFromStorage: async () => {}, + bootstrap: async () => {}, + bootstrapDesktop: async () => { + await bootstrapDesktopGate; + return { + ok: true as const, + listenAddress: "127.0.0.1:6767", + serverId: "srv_test", + hostname: "test", + }; + }, + addConnectionFromListenAndWaitForOnline: async () => { + resolveAddConnection(); + }, + waitForAnyConnectionOnline: () => ({ + promise: Promise.resolve(), + cancel: vi.fn(), + }), + }, + setPhase, + setError, + isCancelled: () => false, + }); + + await initPromise; + expect(setPhase).toHaveBeenLastCalledWith("online"); + + resolveBootstrapDesktop(); + await addConnectionCompleted; + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(setPhase).not.toHaveBeenCalledWith("connecting"); + expect(setPhase).toHaveBeenLastCalledWith("online"); + }); }); diff --git a/packages/app/src/app/host-runtime-bootstrap.ts b/packages/app/src/app/host-runtime-bootstrap.ts index 3733522367..9f75aefcdd 100644 --- a/packages/app/src/app/host-runtime-bootstrap.ts +++ b/packages/app/src/app/host-runtime-bootstrap.ts @@ -46,6 +46,7 @@ export async function initializeHostRuntime(args: { setPhase("starting-daemon"); setError(null); + let raceSettled = false; const anyOnline = store.waitForAnyConnectionOnline(); const bootstrapPromise = (async (): Promise => { @@ -54,7 +55,7 @@ export async function initializeHostRuntime(args: { if (!bootstrapResult.ok) { return { type: "error", error: bootstrapResult.error }; } - if (!isCancelled()) { + if (!isCancelled() && !raceSettled) { setPhase("connecting"); } await store.addConnectionFromListenAndWaitForOnline({ @@ -76,6 +77,7 @@ export async function initializeHostRuntime(args: { })); const result = await Promise.race([onlineFromAny, bootstrapPromise]); + raceSettled = true; anyOnline.cancel(); if (!isCancelled()) { diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index 342a9a8e80..e221c40f97 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -2,33 +2,17 @@ import { useEffect, useSyncExternalStore } from "react"; import { usePathname, useRouter } from "expo-router"; import { StartupSplashScreen } from "@/screens/startup-splash-screen"; import { useHostRuntimeBootstrapState, useStoreReady } from "@/app/_layout"; -import { getHostRuntimeStore, isHostRuntimeConnected, useHosts } from "@/runtime/host-runtime"; +import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { buildHostRootRoute } from "@/utils/host-routes"; import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; const WELCOME_ROUTE = "/welcome"; -function useAnyOnlineHostServerId(serverIds: string[]): string | null { +function useEarliestOnlineHostServerId(): string | null { const runtime = getHostRuntimeStore(); - return useSyncExternalStore( (onStoreChange) => runtime.subscribeAll(onStoreChange), - () => { - let firstOnlineServerId: string | null = null; - let firstOnlineAt: string | null = null; - for (const serverId of serverIds) { - const snapshot = runtime.getSnapshot(serverId); - const lastOnlineAt = snapshot?.lastOnlineAt ?? null; - if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) { - continue; - } - if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) { - firstOnlineAt = lastOnlineAt; - firstOnlineServerId = serverId; - } - } - return firstOnlineServerId; - }, + () => runtime.getEarliestOnlineHostServerId(), () => null, ); } @@ -40,8 +24,7 @@ export default function Index() { const pathname = usePathname(); const bootstrapState = useHostRuntimeBootstrapState(); const storeReady = useStoreReady(); - const hosts = useHosts(); - const anyOnlineServerId = useAnyOnlineHostServerId(hosts.map((host) => host.serverId)); + const anyOnlineServerId = useEarliestOnlineHostServerId(); useEffect(() => { if (!storeReady) { diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 9a6d1600c2..41eda724bd 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -1739,25 +1739,31 @@ export class HostRuntimeStore { }; } + getEarliestOnlineHostServerId(): string | null { + let earliestServerId: string | null = null; + let earliestOnlineAt: string | null = null; + for (const host of this.hosts) { + const snapshot = this.getSnapshot(host.serverId); + if (!isHostRuntimeConnected(snapshot) || !snapshot?.lastOnlineAt) continue; + if (!earliestOnlineAt || snapshot.lastOnlineAt < earliestOnlineAt) { + earliestOnlineAt = snapshot.lastOnlineAt; + earliestServerId = host.serverId; + } + } + return earliestServerId; + } + waitForAnyConnectionOnline(): { promise: Promise; cancel: () => void } { let unsubscribe: (() => void) | null = null; - const isAnyOnline = (): boolean => { - for (const host of this.hosts) { - const snapshot = this.getSnapshot(host.serverId); - if (snapshot?.connectionStatus === "online") return true; - } - return false; - }; - const promise = new Promise((resolve) => { - if (isAnyOnline()) { + if (this.getEarliestOnlineHostServerId() !== null) { resolve(); return; } unsubscribe = this.subscribeAll(() => { - if (isAnyOnline()) { + if (this.getEarliestOnlineHostServerId() !== null) { unsubscribe?.(); unsubscribe = null; resolve();