mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +00:00
Surface desktop daemon gate failures instead of silent no-op
The async settings gate added in f0d96f8e called
shouldStartDaemon() inside Promise.resolve(...).then(...) with no
catch. If loadDesktopSettings() rejects (corrupted JSON, FS error),
the daemon never starts, no error renders on the splash, and the
retry button repeats the same silent path.
Add a recordError method to DaemonStartService and an onGateError
callback to startDaemonIfGateAllows / startHostRuntimeBootstrap.
_layout.tsx wires it through so a gate rejection populates
lastError, the splash shows the failure, and retry has signal.
This commit is contained in:
@@ -329,6 +329,7 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
||||
store,
|
||||
daemonStartService,
|
||||
shouldStartDaemon: shouldStartBuiltInDaemon,
|
||||
onGateError: (message) => daemonStartService.recordError(message),
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -355,9 +356,11 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
|
||||
}, [anyOnlineHostServerId, daemonStartError, daemonStartIsRunning, hasGivenUpWaitingForHost]);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() });
|
||||
startDaemonIfGateAllows({
|
||||
daemonStartService: getDaemonStartService({ store: getHostRuntimeStore() }),
|
||||
daemonStartService,
|
||||
shouldStartDaemon: shouldStartBuiltInDaemon,
|
||||
onGateError: (message) => daemonStartService.recordError(message),
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -70,6 +70,27 @@ describe("startHostRuntimeBootstrap", () => {
|
||||
expect(daemonStartService.start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces gate rejection to onGateError without starting the daemon", async () => {
|
||||
const store = createFakeStore();
|
||||
const daemonStartService = createFakeDaemonStartService();
|
||||
const onGateError = vi.fn();
|
||||
|
||||
startHostRuntimeBootstrap({
|
||||
store,
|
||||
daemonStartService,
|
||||
shouldStartDaemon: async () => {
|
||||
throw new Error("settings file unreadable");
|
||||
},
|
||||
onGateError,
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(onGateError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(daemonStartService.start).not.toHaveBeenCalled();
|
||||
expect(onGateError).toHaveBeenCalledWith(expect.stringContaining("settings file unreadable"));
|
||||
});
|
||||
|
||||
it("does not await the daemon-start promise", () => {
|
||||
const store = createFakeStore();
|
||||
let resolveStart: ((value: { ok: true }) => void) | undefined;
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface StartHostRuntimeBootstrapInput {
|
||||
store: HostRuntimeBootstrapStore;
|
||||
daemonStartService: HostRuntimeBootstrapDaemonStartService;
|
||||
shouldStartDaemon: HostRuntimeBootstrapStartGate;
|
||||
onGateError?: (message: string) => void;
|
||||
}
|
||||
|
||||
export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput): void {
|
||||
@@ -24,26 +25,35 @@ export function startHostRuntimeBootstrap(input: StartHostRuntimeBootstrapInput)
|
||||
startDaemonIfGateAllows({
|
||||
daemonStartService: input.daemonStartService,
|
||||
shouldStartDaemon: input.shouldStartDaemon,
|
||||
onGateError: input.onGateError,
|
||||
});
|
||||
}
|
||||
|
||||
export function startDaemonIfGateAllows(input: {
|
||||
daemonStartService: HostRuntimeBootstrapDaemonStartService;
|
||||
shouldStartDaemon: HostRuntimeBootstrapStartGate;
|
||||
onGateError?: (message: string) => void;
|
||||
}): void {
|
||||
if (typeof input.shouldStartDaemon === "boolean") {
|
||||
if (input.shouldStartDaemon) {
|
||||
const gate = input.shouldStartDaemon;
|
||||
if (typeof gate === "boolean") {
|
||||
if (gate) {
|
||||
void input.daemonStartService.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void Promise.resolve(input.shouldStartDaemon()).then((shouldStartDaemon) => {
|
||||
if (shouldStartDaemon) {
|
||||
void input.daemonStartService.start();
|
||||
}
|
||||
return;
|
||||
});
|
||||
void Promise.resolve()
|
||||
.then(() => gate())
|
||||
.then((shouldStartDaemon) => {
|
||||
if (shouldStartDaemon) {
|
||||
void input.daemonStartService.start();
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
input.onGateError?.(`Failed to evaluate desktop daemon settings: ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
export const WELCOME_ROUTE: Href = "/welcome";
|
||||
|
||||
@@ -188,6 +188,21 @@ describe("DaemonStartService", () => {
|
||||
expect(service.getLastError()).toBeNull();
|
||||
});
|
||||
|
||||
it("recordError surfaces an external error and notifies subscribers", () => {
|
||||
const fake = createFakeStore();
|
||||
const service = new DaemonStartService({
|
||||
store: fake.store,
|
||||
startDesktopDaemon: async () => makeStatus(),
|
||||
});
|
||||
const notifications = vi.fn();
|
||||
service.subscribe(notifications);
|
||||
|
||||
service.recordError("settings file unreadable");
|
||||
|
||||
expect(service.getLastError()).toBe("settings file unreadable");
|
||||
expect(notifications).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("stops notifying after a subscriber unsubscribes", async () => {
|
||||
const fake = createFakeStore();
|
||||
let notifications = 0;
|
||||
|
||||
@@ -53,6 +53,10 @@ export class DaemonStartService {
|
||||
return this.lastError;
|
||||
}
|
||||
|
||||
recordError(message: string): void {
|
||||
this.setLastError(message);
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.inFlightCount > 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user