From 2d4e91546b153048d356bfb2f968e2512a0b1490 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 10 Nov 2026 03:06:51 +0800 Subject: [PATCH] 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. --- public/boot.js | 7 +++++ src/platform/boot.test.ts | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/public/boot.js b/public/boot.js index 81e9f6506..d378132c5 100644 --- a/public/boot.js +++ b/public/boot.js @@ -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}`); } }; diff --git a/src/platform/boot.test.ts b/src/platform/boot.test.ts index df9bbdb38..c4c7f1bf9 100644 --- a/src/platform/boot.test.ts +++ b/src/platform/boot.test.ts @@ -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");