From cb3c0bafd7469288b2f009c0bf30222bd020ceca Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:34:30 +0800 Subject: [PATCH] fix(boot): count the startup timeout only while the window shows A window behind others or on another Space gets no animation frames, and the splash waits for two of them before startup finishes. The 30-second timeout kept running meanwhile and reported a healthy but slow start, such as a cold dev build, as "OpenIDEA could not start". It now pauses while the document is hidden and resumes with the time left. --- public/boot.js | 27 +++++++++++++++++++++++---- src/platform/boot.test.ts | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/public/boot.js b/public/boot.js index 240015eb6..fee10a7b3 100644 --- a/public/boot.js +++ b/public/boot.js @@ -142,11 +142,30 @@ window.removeEventListener("unhandledrejection", onRejection); window.removeEventListener("openidea:boot-error", onBootError); window.removeEventListener("openidea:boot-ready", onReady); + document.removeEventListener("visibilitychange", onVisibility); }; - const timer = window.setTimeout( - () => show(rejection === undefined ? text.timeout : `${text.timeout}\n\nUnhandled rejection: ${rejection}`), - 30_000, - ); + // The timeout counts only while the window shows: a hidden or covered window gets no + // animation frames, and the splash waits for those to finish startup. + let remaining = 30_000; + let armedAt = 0; + let timer; + const arm = () => { + if (failed || ready || timer !== undefined || document.hidden) return; + armedAt = Date.now(); + timer = window.setTimeout( + () => show(rejection === undefined ? text.timeout : `${text.timeout}\n\nUnhandled rejection: ${rejection}`), + remaining, + ); + }; + const onVisibility = () => { + if (!document.hidden) return arm(); + if (timer === undefined) return; + window.clearTimeout(timer); + timer = undefined; + remaining -= Date.now() - armedAt; + }; + arm(); + document.addEventListener("visibilitychange", onVisibility); window.addEventListener("error", onError, true); window.addEventListener("unhandledrejection", onRejection); window.addEventListener("openidea:boot-error", onBootError); diff --git a/src/platform/boot.test.ts b/src/platform/boot.test.ts index 9b3a02d90..e87d620f6 100644 --- a/src/platform/boot.test.ts +++ b/src/platform/boot.test.ts @@ -95,6 +95,27 @@ it("reports the first rejection when startup then times out", () => { expect(details).not.toContain("later rejection"); }); +it("counts the startup timeout only while the window shows", () => { + const hidden = (value: boolean) => { + Object.defineProperty(document, "hidden", { configurable: true, value }); + document.dispatchEvent(new Event("visibilitychange")); + }; + try { + vi.advanceTimersByTime(20_000); + // Behind another window: no frames, so the splash can't finish; that isn't a failure. + hidden(true); + vi.advanceTimersByTime(60_000); + expect(document.getElementById("boot-error")).toBeNull(); + hidden(false); + vi.advanceTimersByTime(9_000); + expect(document.getElementById("boot-error")).toBeNull(); + vi.advanceTimersByTime(1_000); + expect(document.querySelector("textarea")?.value).toContain("Startup did not finish within 30 seconds."); + } finally { + Object.defineProperty(document, "hidden", { configurable: true, value: false }); + } +}); + it("preserves the first failure", () => { window.dispatchEvent(new ErrorEvent("error", { error: new Error("startup failed") })); window.dispatchEvent(new ErrorEvent("error", { message: "later failure" }));