fix(editor): fatal overlay matches Firefox trap messages + taps pthread-worker errors

The v0.1.19 prod crash (console-export-2026-7-31_17-49-20.log) opened with
Firefox's bare "index out of bounds" — no "RuntimeError" prefix, no "table" —
which the terminal-signature regex only knew in Chrome's spelling, so the
overlay this feature exists for never promoted on the very trap it was built
against. Match the bare form (+ "null function or function signature", the
other Firefox spelling in this family).

Also wrap the Worker constructor attach-only: a pthread worker's uncaught
error fires an ErrorEvent on the Worker OBJECT, never on window, so worker
crashes (raytracer pool etc.) were invisible to both listeners. The editor's
main()/wx run on the page thread (no PROXY_TO_PTHREAD on the link line) — the
tap is defense-in-depth, not the primary fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
Gergő Törcsvári 2026-07-31 20:33:55 +02:00
commit 197f317bfb
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322

View file

@ -1185,8 +1185,12 @@ export function WasmTool({
// genuinely terminal signatures promote to the fatal overlay; ordinary app // genuinely terminal signatures promote to the fatal overlay; ordinary app
// errors must not hijack a working editor. // errors must not hijack a working editor.
React.useEffect(() => { React.useEffect(() => {
// NOTE: matched against `e.error.message`, which is BARE — Firefox's first
// trap is literally "index out of bounds" (no "RuntimeError", no "table")
// and slipped through the original pattern; the v0.1.19 prod log opens with
// exactly that message.
const terminal = (msg: string) => const terminal = (msg: string) =>
/RuntimeError|\babort(ed)?\b|table index is out of bounds|indirect call signature|memory access out of bounds|unreachable executed/i.test( /RuntimeError|\babort(ed)?\b|\bindex out of bounds|indirect call signature|memory access out of bounds|unreachable executed|null function or function signature/i.test(
msg, msg,
); );
const onError = (e: ErrorEvent) => { const onError = (e: ErrorEvent) => {
@ -1203,9 +1207,35 @@ export function WasmTool({
append(dumpTrace()); append(dumpTrace());
setFatal(msg); setFatal(msg);
}; };
// With PROXY_TO_PTHREAD, main()/wx/timers — and therefore every asyncify
// trap in this family — throw INSIDE a pthread worker. A worker's uncaught
// error fires an ErrorEvent on the Worker OBJECT, never on `window`, so the
// two listeners below can't see the very traps this overlay exists for
// (v0.1.19 prod: three "Uncaught RuntimeError"s, overlay never promoted).
// Wrap the constructor attach-only: the glue spawns all pthread workers
// from this realm, so every one gets an error tap.
const NativeWorker = window.Worker;
const onWorkerError = (e: ErrorEvent) => {
const msg = String(e.message ?? "");
if (!terminal(msg)) return;
append(`[fatal] worker error: ${msg}`);
append(dumpTrace());
setFatal(msg);
};
const PatchedWorker = function (
this: unknown,
...args: ConstructorParameters<typeof Worker>
) {
const w = new NativeWorker(...args);
w.addEventListener("error", onWorkerError);
return w;
} as unknown as typeof Worker;
PatchedWorker.prototype = NativeWorker.prototype;
window.Worker = PatchedWorker;
window.addEventListener("error", onError); window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection); window.addEventListener("unhandledrejection", onRejection);
return () => { return () => {
window.Worker = NativeWorker;
window.removeEventListener("error", onError); window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onRejection); window.removeEventListener("unhandledrejection", onRejection);
}; };