Skip to content

[Bug]: Pyodide worker stored at module scope causes blob URL leaks and stale worker on CodingVerse remount #568

Description

@nyxsky404

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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

Labels

NSoC'26NSoC 2026backendBackend/Firebase related changesbugSomething isn't workingenhancementNew feature or requestfrontendFrontend related changes (HTML/CSS/JS/React)gssocGirlScript Summer of Codegssoc26GirlScript Summer of Code 2026needs-reviewIssue needs reviewnsocNSoC

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions