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 94d98967a..cd137bc88 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 1653a5f89..05ac0ad19 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 373352236..9f75aefcd 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 342a9a8e8..e221c40f9 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 9a6d1600c..41eda724b 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();