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 (