Next.js 16 · React 19 · TypeScript
App Router server components for marketing, guide, and learn pages; the visualizer workspace is a client-side React app. Type-checked end to end.
Technical documentation
DSA Lab's core bet is that you should never write visualization code. Paste an algorithm, add JSON input, and the tracer infers every state change — arrays, strings, maps, matrices, graphs, linked lists, trees, and recursion frames — with no custom annotations. This page documents how that works: the trace engine, the sandbox, and the canvas that turns steps into pictures.
01 · Overview
Everything that matters happens in the browser. Instrumentation, execution, step capture, and rendering are all client-side; the server only handles sign-in, saved collections, and feedback. There is no backend that runs your code — the sandbox is a Web Worker, not a server.
The pipeline has three stages. First, acornparses the user's code and the instrumenter rewrites it so that every state change calls a capture hook. Second, the rewritten code runs inside a sandboxed Web Worker that executes it and emits one self-contained snapshot per meaningful change. Third, the canvas is a pure function of those snapshots: it renders whatever structures a step carries and highlights what changed since the previous step.
02 · Tech stack
Each dependency earns its place — most exist to make the trace pipeline fast, safe, or self-contained.
App Router server components for marketing, guide, and learn pages; the visualizer workspace is a client-side React app. Type-checked end to end.
Tiny, fast ECMAScript parser. The tracer rewrites user code by parsing it into an AST and splicing instrumentation calls into the source — no regex brace-counting.
User code never runs on the main thread. Instrumented source is executed inside a Blob-constructed Worker with networking disabled — a lightweight browser sandbox.
Real CPython compiled to WebAssembly. Python tracing runs natively in the browser via sys.settrace — no AI translation, no server round-trip.
The code editor with line numbers, syntax highlighting, and a decoration that highlights the active trace line as you step.
FLIP-animates array bars when values swap positions, so a bubble-sort exchange visibly slides two bars into each other's places.
Server-side PKCE OAuth against Google, minting a stateless HS256 session JWT (httpOnly cookie). No third-party auth provider.
Plain Postgres for users, saved algorithms, and feedback, plus a storage bucket for feedback screenshots. Supabase Auth is not used.
AI provider: translates Java, C++, and TypeScript the annotation stripper can't handle into instrumented JS with //# line mapping, via meta/llama-3.1-8b-instruct on the NVIDIA NIM API (NVIDIA_API_KEY from build.nvidia.com). JS/TS/Python never touch it. Cached client-side (LRU).
Deployment with Analytics and Speed Insights wired into the root layout.
03 · Architecture
The full trace pipeline, from pasted code to rendered state.
Instrumentation, worker execution, step normalization, rendering, playback, and code editing. No app secrets ever reach the client.
Google OAuth dance, session cookie minting, saved algorithms, feedback (with image upload), and the optional Gemini translation route.
04 · The trace engine
The tracer lives in src/lib/trace/. It has four responsibilities: rewrite code, run it safely, normalize what came back, and bridge pasted LeetCode solutions.
The instrumenter parses user code with acorn (locations enabled) and walks the AST with a custom scope-aware walker. Instead of evaluating code and guessing at state, it rewrites the source so every interesting moment explicitly reports itself. Statements get a capture call appended; branch conditions are wrapped so their truthiness becomes a step; functions get enter/return hooks; and double-indexed matrix accesses become instrumented calls so the exact cell is traced.
// before — user's code
function binarySearch(arr, target) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
// after — what the tracer actually runs (conceptual)
__autoCapture(1, {}, "Start");
function binarySearch(arr, target) {
__autoEnter(1, "binarySearch", { arr, target });
let left = 0, right = arr.length - 1;
while (__autoBranch(4, "for left <= right", (left <= right), { left, right, arr })) {
const mid = Math.floor((left + right) / 2);
if (__autoBranch(6, "if arr[mid] === target", (arr[mid] === target), { left, right, mid, arr }))
return __autoReturn(6, { left, right, mid, arr }, "return mid", mid);
if (__autoBranch(7, "if arr[mid] < target", (arr[mid] < target), { left, right, mid, arr }))
left = mid + 1;
else right = mid - 1;
__autoCapture(9, { left, right, mid, arr }, "left = mid + 1");
}
return __autoReturn(10, { left, right }, "return -1", -1);
}Six hooks cover every kind of step the UI understands:
__autoCapture(line, values, label) emit a step snapshot after a statement __autoBranch(line, label, value, values) record a condition's truthiness, count comparisons __autoEnter(line, name, values) push a recursive call-stack frame __autoReturn(line, values, label, value) record the return value, pop the frame __autoAccess(name, obj, r, c, src, kind) capture an exact matrix cell read / write __autoRecord(name, obj, r, c, src) side-effect-free cell record inside conditions
The walker also handles the language's sharp edges, all of which would corrupt a naive rewrite:
if (c) stmt; else gets wrapped in a block so the inserted capture doesn't detach the else.const x = cond ? a : b evaluates while x is still uninitialized, so names declared by the enclosing declarator are excluded from its capture.var names declared inside a loop body are collected so loop-condition branches can read them on later iterations.//# line: Ndirectives (emitted by AI translation) remap steps back to the user's original source.The rewritten code never touches the main thread. A Blob-constructed Worker (built from a template string via URL.createObjectURL) receives the instrumented source and the parsed input JSON, executes it with an async function wrapper, and posts the step list back. The worker is deliberately boring — it has no UI, no DOM, no network.
[cycle].Raw worker steps are untrusted data. normalizeSteps whitelists every field a step may carry, coerces types (numbers stay numbers, garbage drops out), and then derives a human-readable phase note for matrix steps — BFS vs DFS context, cell changes, traversal movement, and the exact instrumented access. The canvas only ever sees normalized steps.
{
"title": "Branch",
"note": "if arr[mid] < target → false",
"line": 7,
"array": [1, 3, 5, 7, 9],
"active": [3], // every in-bounds integer pointer lands here
"range": [0, 4], // left … right search band
"pointers": { "left": 0, "right": 4, "mid": 2 },
"ops": { "comparisons": 3, "swaps": 0, "writes": 0 }
}Students paste solutions in platform format — class Solution { twoSum(nums, target) } or a bare function — which never references the app's input JSON. The harness detects the entry point, maps the input JSON onto its parameters (by name, else positionally), converts LeetCode-style arrays into real ListNode / TreeNode chains, invokes the entry, and returns the result. The harness is appended afterinstrumentation, so helper functions build lists and trees without polluting the trace. On the way out, ListNode / TreeNode / GraphNode results are converted back to LeetCode's array encoding so the output strip shows [1, 2, 3, 4], not nested JSON.
05 · Rendering
The canvas is a pure function of (current step, previous step). Render what the step carries, diff against the last one, animate the difference.
06 · Languages
Three languages run today. Each uses the cheapest path that produces a trustworthy trace.
Native. Parsed and instrumented locally with acorn; runs in the worker. No network, no AI, no translation — this is the reference path.
Type annotations are stripped locally (LeetCode-style signatures, interfaces, casts) so it stays on the fast path. Exotic type syntax falls back to Gemini translation, and emitted //# line: directives keep line mapping honest.
Real CPython via Pyodide (WASM) with a sys.settrace harness — the same technique Python Tutor uses. Raw events are converted to VisualSteps with true line numbers; zero Gemini calls.
07 · Accounts
Sign-in is first-party and minimal: Google OAuth straight to an httpOnly cookie, with Supabase used as plain Postgres.
users table, and mints the session.SESSION_SECRET via jose) in an httpOnly, SameSite=Lax, Secure-in-prod cookie named dsa_session, valid 30 days. Nothing in the browser can read it.users.is_admin flag, re-read from the database on every admin request. The cookie is never trusted for privileges.08 · Trust
The product runs arbitrary user code, so the threat model is taken seriously — and mostly solved by never letting that code near anything.
User code runs only inside a dedicated Web Worker with network APIs and importScripts disabled. A timeout and step cap bound runaway loops.
Steps are normalized through a strict whitelist — arbitrary code cannot inject fields into the UI. All DB access is server-side with the service-role key.
NEXT_PUBLIC_, never committed. .env.local is gitignored.09 · Quality
The tracer is the whole product, so its behavior is pinned down by golden trace tests.
runTraceInNode, which polyfills selfso the Blob worker code runs unchanged in a test process. Tests assert algorithm invariants: bubble sort's final array equals the sorted input, binary search lands on the target index, DFS visits every reachable node, recursion frames grow and shrink correctly.npm test runs the whole suite; it doubles as the regression contract for any tracer refactor.10 · How it was developed
A short timeline of how the tracer evolved from a regex hack to the current pipeline.
A JS-only prototype with a brace-counting regex instrumenter embedded inside the visualizer component. Traces were fragile — ternaries, multi-line statements, and branch decisions were silently missed.
The tracer was extracted into a dedicated module and rewritten with acorn. A custom walker with lexical scoping replaced the regex, and golden trace tests became the behavioral contract for every change.
Double-indexed accesses (grid[r][c]) are rewritten so the exact cell, access source, and read/write kind are captured — enabling active-cell highlighting and plain-language phase narration.
The workspace became a CodeMirror-based, resizable IDE with a first-run tour, zoomable panels, and student learning tools.
Pyodide + sys.settrace landed: real CPython in the browser traces Python with true line numbers and zero Gemini calls. Python is the one non-JS language with a genuinely native path.
Linked lists and binary trees get stable node ids with active-node highlighting; a LeetCode/GFG entry harness converts pasted solutions into runnable traces and back into LeetCode-encoded answers.
Call/Return steps feed an interactive call tree, and cumulative comparisons / swaps / writes counters turn traces into complexity lessons.
The Top Interview 150 bank, topic-grouped learning paths with checkpoint quizzes, verified expected answers with pass/fail verdicts, and a side-by-side compare mode.
Supabase Auth was removed entirely: server-side PKCE OAuth + an httpOnly session cookie, with the users table migrated and hardened with RLS.
Shareable run links and a chrome-free embed mode — the distribution loop.
11 · Run & contribute
The visualizer runs without any keys. Only sign-in, saving, and feedback need the optional env vars.
npm install # install dependencies npm run dev # development server → http://localhost:3000 npm run build # production build npm run test # vitest unit tests (incl. golden trace tests) npm run lint # eslint
.env.example to .env.local for sign-in; see SETUP.md for the full environment, Google OAuth, Supabase migration, and Vercel guide.