From 8fd7bd19d68d22d9d8b954c28737c561dd2daa85 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:53:42 +1000 Subject: [PATCH] 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. --- packages/app/src/components/terminal.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index bf87e67c2a..edbbd752c9 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -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 (