# 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 ``, `