# 09 - DOM window lifetime hypothesis > Goal: explain the current best theory for the DOM-port regression in > `08-dom-port-regression.md`: where the code goes wrong, why the DOM port > exposes it, why the Asyncify arbiter is probably not the fix, and what the > first repair experiment should be. ## Status Hypothesis, not yet proven by a green run. The evidence from 08 rules out the old primary suspects: lost `Asyncify.currData`, a wedged fiber trampoline, a nulled wasm table entry, and an `ASYNCIFY_REMOVE` instrumentation gap. The remaining failure shape is a wx window lifetime bug exposed by the DOM-backed control layer. ## Short version The crash is most likely: ```text DOM/event/coroutine work destroys or half-destroys a wxWindow -> the window remains reachable from a parent GetChildren() list -> the next ProcessEvents() idle phase calls SendIdleEvents() -> SendIdleEvents() recurses into that stale child pointer -> virtual call on dead/invalid wxWindow -> wasm call_indirect index 0 -> RuntimeError: indirect call to null ``` The modal/nested event-loop pump is a trigger because it drives `ProcessEvents()` while a modal sleep or tool coroutine is parked. It is not the only trigger: eeschema also crashes on the normal requestAnimationFrame main-loop tick. The DOM port likely changed the timing because wx controls now have real DOM nodes and browser event listeners. Destroying/removing a DOM node can cause synchronous browser focus/change/input side effects and direct `wx_dom_event` entries into C++. The old canvas/universal path did not have per-control DOM nodes doing this during wx object destruction. ## What the crash means The symbolized eeschema fault is inside: ```text wxWindowBase::SendIdleEvents(wxIdleEvent&) called by wxAppBase::ProcessIdle() called by ProcessEvents() called by the rAF main-loop tick ``` `SendIdleEvents()` does these indirect calls per window: ```text this->OnInternalIdle() this->HandleWindowEvent(event) child->SendIdleEvents(event) ``` In wasm, "indirect call to null" with an intact function table means the call target index is 0. In this code path that is not a JS table corruption story; it means one of the C++ objects being used for a virtual call is bad. The current temp diagnostic in `wxwidgets/src/common/wincmn.cpp` checks exactly that: before recursing into each child during the idle walk, it validates the child pointer and vtable pointer. If it logs a bad child, we have the parent window, class, and name that held the stale pointer. ## Why this is not primarily Asyncify The old async bugs from docs 01-07 were real, but their signatures were different: - wrong sleep buffer or `currData` loss -> bad `doRewind()` buffer, often `index out of bounds`; - trampoline guard stuck -> later fiber swaps never resume; - pump rejection -> modal/nested loop silently remains parked. The 08 traces show: - unwind/rewind bookkeeping alternates cleanly; - stack pointers match the fiber saved-SP slot; - `exportCallStack` is rebuilt correctly by rewind replay; - `Asyncify.state` is Normal at the idle crash; - the wasm function table does not change and no non-null slot flips to null; - gerbview can throw synchronously inside `ProcessEvents()` with no intervening sleep/fiber swap during that `ccall`. An Asyncify arbiter would make sleep/fiber buffer ownership cleaner, and it is still the right blueprint if we see wakeups during active transitions. But it does not stop wx from walking a dangling window pointer during idle. ## Why main can work with the same pump Root `main` points wxwidgets at the async-hardening commit that already has the modal/nested pump fixes: ```text startModal() -> setTimeout pump -> await ccall("ProcessEvents", ..., { async: true }) wxWasmRunNestedLoop() -> same pump shape for nested/quasi-modal loops ``` The DOM-port branch did not introduce those pumps. The major new ingredient is the DOM-backed controls layer: - `build/wasm/wx-dom.js` creates real ``, `