fix: defer terminal cleanup state write to stop cleanNode reentry crash

Terminal onCleanup ran persistTerminal synchronously during a dispose cascade, which flowed through props.onCleanup -> ops.update -> update() in context/terminal.tsx and fired setStore on the terminal store. That store write reentered the reactive graph mid cleanNode iteration; solid then nulled an ancestors owned while an outer cleanNode recursion was still iterating it, crashing with Cannot read properties of null reading 1 at node.owned[i]. Wrapping finalize in queueMicrotask pushes the store write past the current synchronous cleanup cascade so the teardown cannot race with cleanNodes owned walk.
This commit is contained in:
LukeParkerDev
2026-04-17 13:53:42 +10:00
parent 902ac2dad9
commit 8fd7bd19d6

View File

@@ -613,17 +613,30 @@ export const Terminal = (props: TerminalProps) => {
drop?.()
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
// Defer finalize (persistTerminal + local cleanup()) to a microtask so
// that its synchronous store write inside `persistTerminal` — which
// flows through `props.onCleanup` -> `ops.update` -> `update()` in
// `context/terminal.tsx` and calls `setStore("all", i, ...)` — does
// NOT run inside the outer solid cleanNode cascade. Running it
// synchronously mid-cascade races with solid's recursive owned
// iteration (readSignal on a stale memo re-enters updateComputation,
// which nulls an ancestor's owned while the outer loop is still
// iterating it) and crashes with "Cannot read properties of null
// (reading '1')" at node.owned[i] inside chunk-EZWYHVNM.js cleanNode.
// queueMicrotask runs after the current sync reactive flush, so the
// store write lands in a fresh tick.
const finalize = () => {
persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup })
cleanup()
}
const schedule = () => queueMicrotask(finalize)
if (!output) {
finalize()
schedule()
return
}
output.flush(finalize)
output.flush(schedule)
})
return (