Files
paseo/packages/app/src/runtime/daemon-start-service.ts
Mohamed Boudra afeb4d18f8 fix(app): keep reload splash continuous
Treat desktop settings evaluation as part of daemon startup so restored app chrome cannot render between bootstrap phases.
2026-07-21 13:44:23 +02:00

165 lines
5.0 KiB
TypeScript

import { startDesktopDaemon, type DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
import { connectionFromListen } from "@/types/host-connection";
import type { HostRuntimeStore } from "@/runtime/host-runtime";
export type DaemonStartResult = { ok: true } | { ok: false; error: string };
export type DaemonStartCondition = boolean | (() => boolean | Promise<boolean>);
export interface StartDaemonIfEnabledInput {
shouldStart: DaemonStartCondition;
}
type DaemonConnectionStore = Pick<HostRuntimeStore, "upsertConnectionFromListen">;
export interface DaemonStartServiceDeps {
store: DaemonConnectionStore;
startDesktopDaemon?: () => Promise<DesktopDaemonStatus>;
}
export async function upsertDesktopDaemonConnection(
store: DaemonConnectionStore,
daemon: DesktopDaemonStatus,
): Promise<DaemonStartResult> {
const listenAddress = daemon.listen?.trim() ?? "";
const serverId = daemon.serverId.trim();
if (!listenAddress) {
return { ok: false, error: "Desktop daemon did not return a listen address." };
}
if (!serverId) {
return { ok: false, error: "Desktop daemon did not return a server id." };
}
if (!connectionFromListen(listenAddress)) {
return {
ok: false,
error: `Desktop daemon returned an unsupported listen address: ${listenAddress}`,
};
}
await store.upsertConnectionFromListen({
listenAddress,
serverId,
hostname: daemon.hostname,
});
return { ok: true };
}
export class DaemonStartService {
private readonly store: DaemonConnectionStore;
private readonly invokeStartDesktopDaemon: () => Promise<DesktopDaemonStatus>;
private readonly listeners = new Set<() => void>();
private lastError: string | null = null;
private inFlightCount = 0;
constructor(deps: DaemonStartServiceDeps) {
this.store = deps.store;
this.invokeStartDesktopDaemon = deps.startDesktopDaemon ?? startDesktopDaemon;
}
async start(): Promise<DaemonStartResult> {
return this.startIfEnabled({ shouldStart: true });
}
async startIfEnabled(input: StartDaemonIfEnabledInput): Promise<DaemonStartResult> {
// Settings evaluation is part of startup. Publish the running state before
// its first await so restored app chrome cannot appear between these phases.
this.beginRequest();
try {
let shouldStart: boolean;
try {
shouldStart =
typeof input.shouldStart === "boolean" ? input.shouldStart : await input.shouldStart();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return this.fail(`Failed to evaluate desktop daemon settings: ${message}`);
}
if (!shouldStart) {
return { ok: true };
}
const daemon = await this.invokeStartDesktopDaemon();
const result = await upsertDesktopDaemonConnection(this.store, daemon);
return result.ok ? result : this.fail(result.error);
} catch (error) {
return this.fail(error instanceof Error ? error.message : String(error));
} finally {
this.endRequest();
}
}
getLastError(): string | null {
return this.lastError;
}
isRunning(): boolean {
return this.inFlightCount > 0;
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private fail(message: string): DaemonStartResult {
this.setLastError(message);
return { ok: false, error: message };
}
private setLastError(value: string | null): void {
if (this.lastError === value) {
return;
}
this.lastError = value;
this.notify();
}
private beginRequest(): void {
const becameRunning = this.inFlightCount === 0;
this.inFlightCount += 1;
const errorChanged = this.lastError !== null;
this.lastError = null;
if (becameRunning || errorChanged) {
this.notify();
}
}
private endRequest(): void {
this.inFlightCount = Math.max(0, this.inFlightCount - 1);
if (this.inFlightCount === 0) {
this.notify();
}
}
private notify(): void {
for (const listener of this.listeners) {
listener();
}
}
}
let singletonDaemonStartService: DaemonStartService | null = null;
const DAEMON_START_SERVICE_GLOBAL_KEY = "__paseoDaemonStartService";
type DaemonStartServiceGlobal = typeof globalThis & {
[DAEMON_START_SERVICE_GLOBAL_KEY]?: DaemonStartService;
};
export function getDaemonStartService(deps: DaemonStartServiceDeps): DaemonStartService {
if (singletonDaemonStartService) {
return singletonDaemonStartService;
}
const runtimeGlobal = globalThis as DaemonStartServiceGlobal;
if (runtimeGlobal[DAEMON_START_SERVICE_GLOBAL_KEY]) {
singletonDaemonStartService = runtimeGlobal[DAEMON_START_SERVICE_GLOBAL_KEY] ?? null;
if (singletonDaemonStartService) {
return singletonDaemonStartService;
}
}
singletonDaemonStartService = new DaemonStartService(deps);
runtimeGlobal[DAEMON_START_SERVICE_GLOBAL_KEY] = singletonDaemonStartService;
return singletonDaemonStartService;
}