Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions apps/desktop/src/backendStartupReadiness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from "vitest";

import { BackendReadinessAbortedError } from "./backendReadiness.ts";
import { waitForBackendStartupReady } from "./backendStartupReadiness.ts";

describe("waitForBackendStartupReady", () => {
it("falls back to the HTTP probe when no listening signal exists", async () => {
const waitForHttpReady = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
const cancelHttpWait = vi.fn();

await expect(
waitForBackendStartupReady({
waitForHttpReady,
cancelHttpWait,
}),
).resolves.toBe("http");

expect(waitForHttpReady).toHaveBeenCalledTimes(1);
expect(cancelHttpWait).not.toHaveBeenCalled();
});

it("uses the listening signal and cancels the HTTP probe", async () => {
let rejectHttpWait: ((error: unknown) => void) | null = null;
const waitForHttpReady = vi.fn(
() =>
new Promise<void>((_resolve, reject) => {
rejectHttpWait = reject;
}),
);
const cancelHttpWait = vi.fn(() => {
rejectHttpWait?.(new BackendReadinessAbortedError());
});

await expect(
waitForBackendStartupReady({
listeningPromise: Promise.resolve(),
waitForHttpReady,
cancelHttpWait,
}),
).resolves.toBe("listening");

expect(waitForHttpReady).toHaveBeenCalledTimes(1);
expect(cancelHttpWait).toHaveBeenCalledTimes(1);
});

it("rejects when the listening signal fails before HTTP readiness", async () => {
const error = new Error("backend exited");
const waitForHttpReady = vi.fn(() => new Promise<void>(() => {}));

await expect(
waitForBackendStartupReady({
listeningPromise: Promise.reject(error),
waitForHttpReady,
cancelHttpWait: vi.fn(),
}),
).rejects.toBe(error);
});
});
56 changes: 56 additions & 0 deletions apps/desktop/src/backendStartupReadiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { isBackendReadinessAborted } from "./backendReadiness.ts";

export interface WaitForBackendStartupReadyOptions {
readonly listeningPromise?: Promise<void> | null;
readonly waitForHttpReady: () => Promise<void>;
readonly cancelHttpWait: () => void;
}

export async function waitForBackendStartupReady(
options: WaitForBackendStartupReadyOptions,
): Promise<"listening" | "http"> {
const httpReadyPromise = options.waitForHttpReady();
const listeningPromise = options.listeningPromise;

if (!listeningPromise) {
await httpReadyPromise;
return "http";
}

return await new Promise<"listening" | "http">((resolve, reject) => {
let settled = false;

const settleResolve = (source: "listening" | "http") => {
if (settled) {
return;
}
settled = true;
if (source === "listening") {
options.cancelHttpWait();
}
resolve(source);
};

const settleReject = (error: unknown) => {
if (settled) {
return;
}
settled = true;
reject(error);
};

listeningPromise.then(
() => settleResolve("listening"),
(error) => settleReject(error),
);
httpReadyPromise.then(
() => settleResolve("http"),
(error) => {
if (settled && isBackendReadinessAborted(error)) {
return;
}
settleReject(error);
},
);
});
}
59 changes: 11 additions & 48 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness.
import { showDesktopConfirmDialog } from "./confirmDialog.ts";
import { resolveDesktopServerExposure } from "./serverExposure.ts";
import { syncShellEnvironment } from "./syncShellEnvironment.ts";
import { waitForBackendStartupReady } from "./backendStartupReadiness.ts";
import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState.ts";
import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels.ts";
import { ServerListeningDetector } from "./serverListeningDetector.ts";
Expand Down Expand Up @@ -459,51 +460,13 @@ function cancelBackendReadinessWait(): void {
}

async function waitForBackendWindowReady(baseUrl: string): Promise<"listening" | "http"> {
const httpReadyPromise = waitForBackendHttpReady(baseUrl, {
timeoutMs: 60_000,
});
const listeningPromise = backendListeningDetector?.promise;

if (!listeningPromise) {
await httpReadyPromise;
return "http";
}

return await new Promise<"listening" | "http">((resolve, reject) => {
let settled = false;

const settleResolve = (source: "listening" | "http") => {
if (settled) {
return;
}
settled = true;
if (source === "listening") {
cancelBackendReadinessWait();
}
resolve(source);
};

const settleReject = (error: unknown) => {
if (settled) {
return;
}
settled = true;
reject(error);
};

listeningPromise.then(
() => settleResolve("listening"),
(error) => settleReject(error),
);
httpReadyPromise.then(
() => settleResolve("http"),
(error) => {
if (settled && isBackendReadinessAborted(error)) {
return;
}
settleReject(error);
},
);
return await waitForBackendStartupReady({
listeningPromise: backendListeningDetector?.promise ?? null,
waitForHttpReady: () =>
waitForBackendHttpReady(baseUrl, {
timeoutMs: 60_000,
}),
cancelHttpWait: cancelBackendReadinessWait,
});
}

Expand Down Expand Up @@ -2119,9 +2082,9 @@ async function bootstrap(): Promise<void> {
if (isDevelopment) {
mainWindow = createWindow();
writeDesktopLogHeader("bootstrap main window created");
void waitForBackendHttpReady(backendHttpUrl)
.then(() => {
writeDesktopLogHeader("bootstrap backend ready");
void waitForBackendWindowReady(backendHttpUrl)
.then((source) => {
writeDesktopLogHeader(`bootstrap backend ready source=${source}`);
})
.catch((error) => {
if (isBackendReadinessAborted(error)) {
Expand Down
Loading