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();