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.
This commit is contained in:
l0ng-ai
2026-11-11 15:34:30 +08:00
parent 71072dbe4b
commit cb3c0bafd7
2 changed files with 44 additions and 4 deletions
+23 -4
View File
@@ -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);
+21
View File
@@ -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" }));