mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +00:00
Treat desktop settings evaluation as part of daemon startup so restored app chrome cannot render between bootstrap phases.
248 lines
7.0 KiB
TypeScript
248 lines
7.0 KiB
TypeScript
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
|
import type {
|
|
DaemonStartCondition,
|
|
DaemonStartResult,
|
|
StartDaemonIfEnabledInput,
|
|
} from "@/runtime/daemon-start-service";
|
|
import type { Href } from "expo-router";
|
|
import {
|
|
buildHostRootRoute,
|
|
buildHostWorkspaceRoute,
|
|
buildOpenProjectRoute,
|
|
} from "@/utils/host-routes";
|
|
|
|
export interface HostRuntimeBootstrapStore {
|
|
boot: () => void;
|
|
}
|
|
|
|
export interface HostRuntimeBootstrapDaemonStartService {
|
|
startIfEnabled: (input: StartDaemonIfEnabledInput) => Promise<DaemonStartResult>;
|
|
}
|
|
|
|
export interface StartHostRuntimeBootstrapInput {
|
|
store: HostRuntimeBootstrapStore;
|
|
daemonStartService: HostRuntimeBootstrapDaemonStartService;
|
|
shouldStartDaemon: DaemonStartCondition;
|
|
}
|
|
|
|
export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput): void {
|
|
input.store.boot();
|
|
void input.daemonStartService.startIfEnabled({
|
|
shouldStart: input.shouldStartDaemon,
|
|
});
|
|
}
|
|
|
|
const WELCOME_ROUTE: Href = "/welcome";
|
|
|
|
export type StartupBlocker =
|
|
| { kind: "none" }
|
|
| { kind: "managed-daemon-starting" }
|
|
| { kind: "managed-daemon-error"; message: string };
|
|
|
|
export interface ResolveStartupBlockerInput {
|
|
isDesktopRuntime: boolean;
|
|
anyOnlineHostServerId: string | null;
|
|
daemonStartIsRunning: boolean;
|
|
daemonStartError: string | null;
|
|
}
|
|
|
|
export function resolveStartupBlocker(input: ResolveStartupBlockerInput): StartupBlocker {
|
|
if (!input.isDesktopRuntime) {
|
|
return { kind: "none" };
|
|
}
|
|
|
|
if (input.anyOnlineHostServerId) {
|
|
return { kind: "none" };
|
|
}
|
|
|
|
if (input.daemonStartError) {
|
|
return { kind: "managed-daemon-error", message: input.daemonStartError };
|
|
}
|
|
|
|
if (input.daemonStartIsRunning) {
|
|
return { kind: "managed-daemon-starting" };
|
|
}
|
|
|
|
return { kind: "none" };
|
|
}
|
|
|
|
export function resolveStartupNavigationReady(input: { startupBlocker: StartupBlocker }): boolean {
|
|
return input.startupBlocker.kind !== "managed-daemon-starting";
|
|
}
|
|
|
|
export function shouldRunStartupGiveUpTimer(input: {
|
|
startupBlocker: StartupBlocker;
|
|
anyOnlineHostServerId: string | null;
|
|
hasGivenUpWaitingForHost: boolean;
|
|
}): boolean {
|
|
if (input.anyOnlineHostServerId) {
|
|
return false;
|
|
}
|
|
if (input.hasGivenUpWaitingForHost) {
|
|
return false;
|
|
}
|
|
return input.startupBlocker.kind === "none";
|
|
}
|
|
|
|
export type StartupRegistryStatus = "loading" | "ready";
|
|
|
|
export interface IndexStartupRouteTarget {
|
|
kind: "index";
|
|
pathname: string;
|
|
}
|
|
|
|
export interface HostStartupRouteTarget {
|
|
kind: "host";
|
|
serverId: string | null;
|
|
}
|
|
|
|
export type StartupRouteTarget = IndexStartupRouteTarget | HostStartupRouteTarget;
|
|
|
|
interface ResolveStartupRouteBaseInput {
|
|
startupBlocker: StartupBlocker;
|
|
hostRegistryStatus: StartupRegistryStatus;
|
|
hosts: readonly { serverId: string }[];
|
|
}
|
|
|
|
export interface ResolveIndexStartupRouteInput extends ResolveStartupRouteBaseInput {
|
|
route: IndexStartupRouteTarget;
|
|
anyOnlineHostServerId: string | null;
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
isWorkspaceSelectionLoaded: boolean;
|
|
hasGivenUpWaitingForHost: boolean;
|
|
}
|
|
|
|
export interface ResolveHostStartupRouteInput extends ResolveStartupRouteBaseInput {
|
|
route: HostStartupRouteTarget;
|
|
}
|
|
|
|
export type ResolveStartupRouteInput = ResolveIndexStartupRouteInput | ResolveHostStartupRouteInput;
|
|
|
|
export type StartupRouteDecision =
|
|
| { kind: "render" }
|
|
| { kind: "splash" }
|
|
| { kind: "redirect"; href: Href };
|
|
|
|
export type WorkspaceSelectionStatus = "unknown" | "exists" | "missing";
|
|
|
|
function shouldRestoreWorkspaceSelection(input: {
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
}): input is {
|
|
workspaceSelection: ActiveWorkspaceSelection;
|
|
workspaceSelectionStatus: Exclude<WorkspaceSelectionStatus, "missing">;
|
|
} {
|
|
return input.workspaceSelection !== null && input.workspaceSelectionStatus !== "missing";
|
|
}
|
|
|
|
export function resolveWorkspaceSelectionStatus(input: {
|
|
hasHydratedWorkspaces: boolean;
|
|
workspaceExists: boolean;
|
|
}): WorkspaceSelectionStatus {
|
|
if (input.workspaceExists) {
|
|
return "exists";
|
|
}
|
|
return input.hasHydratedWorkspaces ? "missing" : "unknown";
|
|
}
|
|
|
|
export function resolveHostIndexRoute(input: {
|
|
serverId: string;
|
|
workspaceSelection: ActiveWorkspaceSelection | null;
|
|
workspaceSelectionStatus: WorkspaceSelectionStatus;
|
|
}): Href {
|
|
if (
|
|
input.workspaceSelection?.serverId === input.serverId &&
|
|
shouldRestoreWorkspaceSelection(input)
|
|
) {
|
|
return buildHostWorkspaceRoute(input.serverId, input.workspaceSelection.workspaceId);
|
|
}
|
|
return buildOpenProjectRoute();
|
|
}
|
|
|
|
function isIndexPathname(pathname: string) {
|
|
return pathname === "/" || pathname === "";
|
|
}
|
|
|
|
function hostExists(hosts: readonly { serverId: string }[], serverId: string | null): boolean {
|
|
if (!serverId) {
|
|
return false;
|
|
}
|
|
return hosts.some((host) => host.serverId === serverId);
|
|
}
|
|
|
|
function resolveReadyIndexStartupRoute(input: ResolveIndexStartupRouteInput): StartupRouteDecision {
|
|
if (!isIndexPathname(input.route.pathname)) {
|
|
return { kind: "render" };
|
|
}
|
|
|
|
if (!input.isWorkspaceSelectionLoaded) {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
if (
|
|
shouldRestoreWorkspaceSelection(input) &&
|
|
hostExists(input.hosts, input.workspaceSelection.serverId)
|
|
) {
|
|
// Native cold launch must enter the host boundary first. The host index
|
|
// owns workspace restore after its local dynamic params exist.
|
|
return {
|
|
kind: "redirect",
|
|
href: buildHostRootRoute(input.workspaceSelection.serverId),
|
|
};
|
|
}
|
|
|
|
if (input.anyOnlineHostServerId) {
|
|
return { kind: "redirect", href: buildHostRootRoute(input.anyOnlineHostServerId) };
|
|
}
|
|
|
|
const savedHostServerId = input.hosts[0]?.serverId ?? null;
|
|
if (savedHostServerId) {
|
|
return { kind: "redirect", href: buildHostRootRoute(savedHostServerId) };
|
|
}
|
|
|
|
if (input.hasGivenUpWaitingForHost) {
|
|
return { kind: "redirect", href: WELCOME_ROUTE };
|
|
}
|
|
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
function resolveReadyHostStartupRoute(input: ResolveHostStartupRouteInput): StartupRouteDecision {
|
|
if (hostExists(input.hosts, input.route.serverId)) {
|
|
return { kind: "render" };
|
|
}
|
|
|
|
const fallbackServerId = input.hosts[0]?.serverId ?? null;
|
|
if (fallbackServerId) {
|
|
return { kind: "redirect", href: buildOpenProjectRoute() };
|
|
}
|
|
|
|
return { kind: "redirect", href: WELCOME_ROUTE };
|
|
}
|
|
|
|
function isHostStartupRouteInput(
|
|
input: ResolveStartupRouteInput,
|
|
): input is ResolveHostStartupRouteInput {
|
|
return input.route.kind === "host";
|
|
}
|
|
|
|
export function resolveStartupRoute(input: ResolveStartupRouteInput): StartupRouteDecision {
|
|
if (isHostStartupRouteInput(input)) {
|
|
if (input.startupBlocker.kind !== "none" || input.hostRegistryStatus === "loading") {
|
|
return { kind: "render" };
|
|
}
|
|
return resolveReadyHostStartupRoute(input);
|
|
}
|
|
|
|
if (input.startupBlocker.kind !== "none") {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
if (input.hostRegistryStatus === "loading") {
|
|
return { kind: "splash" };
|
|
}
|
|
|
|
return resolveReadyIndexStartupRoute(input);
|
|
}
|