Bug Description
In src/pages/CodingVerse.jsx (lines 130-205), the Pyodide Web Worker is managed through three module-level mutable variables:
let pythonWorker = null;
let pythonWorkerUrl = null;
let isPythonWorkerReady = false;
These live at ES module scope, outside the component. The component's cleanup effect calls terminatePythonWorker() on unmount, which terminates the worker and sets all three to null. But module-scope state is shared across all instances and persists across HMR cycles — it is not tied to the React component lifecycle in any way.
Two concrete problems this causes
-
Blob URL leak on rapid navigation: When the user navigates away (cleanup runs, pythonWorkerUrl is set to null and revoked) and quickly back before the cleanup finishes, getPythonWorker() creates a new Worker and immediately overwrites pythonWorkerUrl. The old blob URL from the previous mount is lost without being passed to URL.revokeObjectURL(). In Vite's HMR dev mode this happens on every hot reload — the module re-evaluates and resets the variables to null without terminating the running worker, leaving an orphaned 10MB+ WASM worker running in the background.
-
Parallel runPython calls clobber each other: runPython sets worker.onmessage directly on the shared worker instance. If the user clicks Run twice quickly before the first execution completes, the second assignment overwrites worker.onmessage, and the first call's promise never resolves — it silently hangs.
Proposed fix
Move worker state into useRef inside the component so the lifecycle is properly bounded:
const pythonWorkerRef = useRef(null);
const pythonWorkerUrlRef = useRef(null);
const getPythonWorker = useCallback(() => {
if (pythonWorkerRef.current) return pythonWorkerRef.current;
const blob = new Blob([PYTHON_WORKER_CODE], { type: "application/javascript" });
pythonWorkerUrlRef.current = URL.createObjectURL(blob);
pythonWorkerRef.current = new Worker(pythonWorkerUrlRef.current);
return pythonWorkerRef.current;
}, []);
const terminatePythonWorker = useCallback(() => {
pythonWorkerRef.current?.terminate();
pythonWorkerRef.current = null;
if (pythonWorkerUrlRef.current) {
URL.revokeObjectURL(pythonWorkerUrlRef.current);
pythonWorkerUrlRef.current = null;
}
}, []);
useEffect(() => () => terminatePythonWorker(), [terminatePythonWorker]);
For the parallel-call issue, track the current pending execution in a ref and cancel/reject it before starting a new run.
Bug Description
In
src/pages/CodingVerse.jsx(lines 130-205), the Pyodide Web Worker is managed through three module-level mutable variables:These live at ES module scope, outside the component. The component's cleanup effect calls
terminatePythonWorker()on unmount, which terminates the worker and sets all three to null. But module-scope state is shared across all instances and persists across HMR cycles — it is not tied to the React component lifecycle in any way.Two concrete problems this causes
Blob URL leak on rapid navigation: When the user navigates away (cleanup runs,
pythonWorkerUrlis set to null and revoked) and quickly back before the cleanup finishes,getPythonWorker()creates a newWorkerand immediately overwritespythonWorkerUrl. The old blob URL from the previous mount is lost without being passed toURL.revokeObjectURL(). In Vite's HMR dev mode this happens on every hot reload — the module re-evaluates and resets the variables to null without terminating the running worker, leaving an orphaned 10MB+ WASM worker running in the background.Parallel
runPythoncalls clobber each other:runPythonsetsworker.onmessagedirectly on the shared worker instance. If the user clicks Run twice quickly before the first execution completes, the second assignment overwritesworker.onmessage, and the first call's promise never resolves — it silently hangs.Proposed fix
Move worker state into
useRefinside the component so the lifecycle is properly bounded:For the parallel-call issue, track the current pending execution in a ref and cancel/reject it before starting a new run.