fix(boot): ignore ResizeObserver loop notifications (#2405)

Fixes #2397

The startup guard in public/boot.js treated every window error with a message as fatal, including the benign "ResizeObserver loop completed with undelivered notifications" (and "loop limit exceeded") that a layout during startup can raise without an error object. These are now ignored; real errors, rejections, script load failures and the 30 s timeout still show the failure page.
This commit is contained in:
l0ng-ai
2026-11-10 03:06:51 +08:00
committed by GitHub
parent 22a56bfc36
commit 2d4e91546b
2 changed files with 65 additions and 0 deletions
+7
View File
@@ -90,6 +90,13 @@
if (target?.tagName === "SCRIPT" || (target?.tagName === "LINK" && target.rel === "modulepreload")) {
show(`Could not load startup script: ${target.src || target.href}`);
} else if (event.error || event.message) {
// ResizeObserver defers undelivered notifications to the next frame; startup can continue.
if (
!event.error &&
(event.message === "ResizeObserver loop completed with undelivered notifications." ||
event.message === "ResizeObserver loop limit exceeded")
)
return;
show(event.error || `${event.message}\n${event.filename || ""}:${event.lineno || 0}:${event.colno || 0}`);
}
};
+58
View File
@@ -24,6 +24,50 @@ it("surfaces module evaluation errors before React is available", () => {
expect(document.querySelector("textarea")?.value).toContain(navigator.userAgent);
});
const resizeObserverMessages = [
"ResizeObserver loop completed with undelivered notifications.",
"ResizeObserver loop limit exceeded",
];
it.each(resizeObserverMessages)("keeps waiting for boot-ready after %s", (message) => {
window.dispatchEvent(new ErrorEvent("error", { message, filename: "tauri://localhost" }));
expect(document.getElementById("splash")).not.toBeNull();
expect(document.getElementById("boot-error")).toBeNull();
expect(document.documentElement.dataset.boot).toBeUndefined();
window.dispatchEvent(new Event("openidea:boot-ready"));
vi.advanceTimersByTime(30_000);
expect(document.getElementById("boot-error")).toBeNull();
});
it.each(resizeObserverMessages)("still reports a real failure after %s", (message) => {
window.dispatchEvent(new ErrorEvent("error", { message }));
window.dispatchEvent(new ErrorEvent("error", { error: new Error("startup failed") }));
expect(document.getElementById("splash")).toBeNull();
expect(document.querySelector("textarea")?.value).toContain("startup failed");
expect(document.querySelector("textarea")?.value).not.toContain(message);
});
it.each(resizeObserverMessages)("still times out an unresolved startup after %s", (message) => {
vi.advanceTimersByTime(15_000);
window.dispatchEvent(new ErrorEvent("error", { message }));
vi.advanceTimersByTime(15_000);
expect(document.querySelector("textarea")?.value).toContain("Startup did not finish within 30 seconds.");
});
it.each(resizeObserverMessages)("keeps actual errors with the same message fatal: %s", (message) => {
window.dispatchEvent(new ErrorEvent("error", { message, error: new Error(message) }));
expect(document.getElementById("splash")).toBeNull();
expect(document.querySelector("textarea")?.value).toContain(message);
});
it.each(resizeObserverMessages)("keeps rejections with the same message fatal: %s", (message) => {
const event = new Event("unhandledrejection");
Object.defineProperty(event, "reason", { value: message });
window.dispatchEvent(event);
expect(document.getElementById("splash")).toBeNull();
expect(document.querySelector("textarea")?.value).toContain(message);
});
it("handles startup rejections and preserves the first failure", () => {
const event = new Event("unhandledrejection");
Object.defineProperty(event, "reason", { value: { message: "startup rejected" } });
@@ -46,6 +90,20 @@ it("handles script load failures but ignores image failures", () => {
expect(document.querySelector("textarea")?.value).toContain("/assets/entry.js");
});
it("handles modulepreload load failures", () => {
const link = document.createElement("link");
link.rel = "modulepreload";
link.href = "/assets/dependency.js";
document.head.append(link);
try {
link.dispatchEvent(new Event("error"));
expect(document.getElementById("splash")).toBeNull();
expect(document.querySelector("textarea")?.value).toContain("/assets/dependency.js");
} finally {
link.remove();
}
});
it("surfaces initial React render failures through the boot error event", () => {
window.dispatchEvent(new CustomEvent("openidea:boot-error", { detail: new Error("render failed") }));
expect(document.querySelector("textarea")?.value).toContain("render failed");