Skip to content

Rewrite to run in the browser (instead of locally) #42

Description

@jaysonvirissimo

Software Design Document: Practice Thy Algorithms (Browser Edition)


Table of Contents

  1. Introduction and Overview
  2. Goals and Non-Goals
  3. System Architecture
  4. Data Design
  5. Component Design
  6. Interface Design
  7. User Interface Design
  8. Assumptions and Dependencies
  9. Alternatives Considered
  10. Milestones and Rollout
  11. Future Work
  12. Glossary
  13. References

1. Introduction and Overview

1.1 Purpose

Practice Thy Algorithms is currently a multi-language (JavaScript, Python,
Ruby) algorithm-practice repository. Solving problems requires cloning the repo
and installing each language's toolchain locally (npm install, a Python
interpreter, bundle install).

This document describes a rewrite that lets users solve and verify the same
problems entirely in the browser — no clone, no local language installs, no
backend server. The application is a static site hosted on GitHub Pages,
modeled on the sibling project
build-your-own-alu.

1.2 Scope

Version 1.0 includes:

  • In-browser problem browsing and selection (the existing 22 problems).
  • In-browser code editor with syntax highlighting.
  • In-browser execution and test verification for JavaScript and Ruby.
  • Per-user progress/solution persistence (client-side).
  • Static deployment to GitHub Pages via GitHub Actions.

Stretch goal: Python support.

Out of scope (v1.0): see §2.2 Non-Goals.

1.3 Background and Context

The existing repository already provides the most important asset for this
rewrite: a centralized problem-definition system. Shared/problems.json is
the single source of truth for problem statements, function signatures (per
language), parameters, return types, and test cases — including comparison
metadata (exact, unordered_array, set_equality). Today, language-specific
generators in Shared/generators/ convert this JSON into Jest / unittest /
RSpec test files that run on the local CLI.

The browser rewrite reuses problems.json directly and replaces the "generate
test files and run a local test runner" workflow with "run user code and test
cases in a sandboxed in-browser language runtime."

1.4 Primary Features

  • Browse a catalog of algorithm problems with academic-style problem statements.
  • Choose a language (JS / Ruby; Python as stretch) per problem.
  • Write a solution in an editor pre-seeded with the correct function signature.
  • Run the bundled test cases and see pass/fail feedback instantly, client-side.
  • Resume previous work (solutions persist locally).

1.5 Target Audience

The primary user is the solo self-learner — an individual practicing
algorithms (e.g., for interview prep) on their own. This implies a
single-user, local-only model: no accounts, no authentication, no sharing,
and no social/leaderboard features. This reinforces the static, client-only
architecture (§3) — there is no per-user server state
to manage.


2. Goals and Non-Goals

2.1 Goals

  • G1. Zero-install: a learner can solve a problem with only a web browser.
  • G2. Reuse Shared/problems.json as the single source of truth — no
    divergence between problem definitions and what runs.
  • G3. Support JavaScript and Ruby execution and verification in v1.0.
  • G4. Deploy as a fully static site on GitHub Pages (no backend, no hosting
    cost, no server maintenance).
  • G5. Reuse the proven engineering patterns and deployment pipeline of the
    sibling build-your-own-alu project.

2.2 Non-Goals

  • Server-side execution, accounts/auth, sharing, or social/leaderboard features
    — everything stays client-side and single-user (see §1.5).
  • Arbitrary/freeform code execution beyond the defined problem set.
  • A mobile-first or native app experience (best-effort responsive is sufficient).
  • Maintaining the legacy local CLI workflow. The browser app deprecates and
    replaces the CLI test-runner workflow (npm test / rspec / unittest); the
    README and local setup are updated/archived at cutover (see
    §10).
  • Expanding or changing the problem set in v1.0. v1.0 is a strict port of the
    existing 22 problems — no additions, removals, reordering, or statement
    rewrites. Content curation is a post-v1.0 follow-up.

3. System Architecture

3.1 High-Level Architecture

A single-page, fully static web application. All computation — including running
user code and executing test cases — happens in the browser. The only "backend"
is GitHub Pages serving static assets.

┌─────────────────────────────────────────────────────────────┐
│                          Browser                              │
│                                                               │
│  ┌─────────────┐   ┌──────────────┐   ┌───────────────────┐  │
│  │ Problem      │   │ Code Editor   │   │ Results / Test    │  │
│  │ Catalog UI   │   │ (CodeMirror 6)│   │ Feedback Panel    │  │
│  └──────┬───────┘   └──────┬────────┘   └─────────▲─────────┘  │
│         │                  │                      │            │
│         ▼                  ▼                      │            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │              App Core (state, routing)                │    │
│  │  - loads problems.json                                │    │
│  │  - localStorage persistence                           │    │
│  └───────────────────────┬──────────────────────────────┘    │
│                          │ dispatch run                       │
│                          ▼                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │           Execution Layer (Web Workers)               │    │
│  │  ┌────────────┐  ┌──────────────┐  ┌───────────────┐  │    │
│  │  │ JS Runner  │  │ Ruby Runner   │  │ Python Runner │ │    │
│  │  │ (native)   │  │ (ruby.wasm)   │  │ (Pyodide)*    │ │    │
│  │  └────────────┘  └──────────────┘  └───────────────┘  │    │
│  └──────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼  (static hosting only)
                       ┌──────────────┐
                       │ GitHub Pages │
                       └──────────────┘

* Python runner is a stretch goal.

3.2 Technology Stack

Concern Choice Notes
Build tool / dev server Vite As in build-your-own-alu.
UI framework React Component model and declarative state for the run lifecycle, results panel, and hint reveals.
Code editor CodeMirror 6 Framework-agnostic; wrapped in a React component. @codemirror/lang-javascript for JS, @codemirror/legacy-modes (Ruby) for Ruby; switched via a CodeMirror Compartment.
App language TypeScript Type safety for the LanguageRunner contracts and JS↔WASM marshalling.
Unit testing (of the app) Vitest As in build-your-own-alu.
E2E testing Playwright Minimal smoke suite (load app, run a JS solution → pass, run a Ruby solution → pass) to cover the WASM-in-Worker path unit tests can't.
JS user-code execution Native eval/Function in a Web Worker No runtime download.
Ruby user-code execution ruby.wasm (@ruby/wasm-wasi + @ruby/3.4-wasm-wasi, the ruby+stdlib.wasm build → CRuby 3.4) Lazy-loaded (~30 MB), self-hosted via Vite ?url; +stdlib needed for json/set/stringio.
Python user-code execution (stretch) Pyodide (CPython → WASM) Lazy-loaded.
Persistence localStorage
Hosting / CI GitHub Pages + GitHub Actions Static deploy.

3.3 Execution Model and Sandboxing

  • User code runs inside Web Workers to keep the UI responsive and to isolate
    long-running or infinite-loop solutions (the worker can be terminated on
    timeout).
  • Each language runner loads its runtime lazily — the multi-MB Ruby/Python WASM
    payloads are fetched only when that language is first selected.
  • A per-run timeout/watchdog terminates runaway user code.

Payload budget: lazy-load, no hard size cap. The initial page stays light —
the JavaScript path needs no runtime download and loads fast. The Ruby (and
later Python) WASM runtimes are fetched only when that language is first
selected, behind a loading indicator, accepting whatever size the upstream
runtime is. (As built, the Ruby ruby+stdlib.wasm binary is ~30 MB, self-hosted
as a hashed Vite asset emitted under the Pages base path — no CDN dependency.)
Optimizing WASM payload size is not a v1.0 concern; it can be revisited if
first-Ruby-run latency proves painful.

3.4 Deployment Architecture

  • A push to master triggers a GitHub Actions workflow: install dependencies →
    vite build → publish dist/ to GitHub Pages.
  • The base path is configured for project pages
    (jaysonvirissimo.github.io/practice-thy-algorithms/).
  • Fully static; no environment secrets or runtime config required.

4. Data Design

4.1 Source of Truth: problems.json

The existing Shared/problems.json schema is reused largely as-is. Per problem:

{
  "two_sum": {
    "title": "Two Sum",
    "description": "Implement a function that locates a pair of elements ...",
    "complexity": "O(n)",
    "parameters": [ { "name": "...", "type": "...", "description": "..." } ],
    "returnType":         { "javascript": "...", "ruby": "...", "python": "..." },
    "functionSignatures": { "javascript": "function twoSum(nums, target)",
                            "ruby": "def two_sum(nums, target)",
                            "python": "def two_sum(nums, target)" },
    "hints": [ "Approach hint 1 (language-agnostic) ...", "Approach hint 2 ..." ],
    "testCases": [
      { "input": { "nums": [2,7,11,15], "target": 9 },
        "expected": [0,1],
        "description": "...",
        "comparison": { "mode": "exact|unordered_array|set_equality",
                        "type": "deep_equality" } }
    ]
  }
}

hints is a new optional, top-level (language-agnostic) field added for the
browser app; see §5.6.

4.2 How the Browser Consumes Problem Data

  • problems.json is imported/bundled at build time (or fetched at runtime as a
    static asset) and rendered into the catalog and editor.
  • Function signatures seed the editor's starting code per language.
  • Test cases drive the in-browser test harness (see §5.3).
  • Comparison metadata (exact, unordered_array, set_equality) is honored by
    the in-runtime comparison logic instead of by generated test files.

4.3 User State and Persistence

State is local-only, with no portability. All state lives in localStorage;
there is no export/import, no URL sharing, and no cross-device sync (sync would
require a backend, and sharing is out of scope for the solo-learner audience —
see §1.5). Data is tied to a single browser profile.

Persisted state:

  • Latest solution code per (problem, language).
  • Pass/fail / completion status per problem.
  • Hint-reveal progress per problem (how many hints have been shown).
  • UI preferences (theme; Vim-keybindings on/off toggle — see §7.3).

4.4 Known Data-Layer Gotchas (from current repo)

  • Function-name inconsistencies: JSON signatures sometimes disagree with
    what tests expect (documented in CLAUDE.md). The browser harness must invoke
    the function name actually defined by functionSignatures, consistently across
    languages.
  • Ordering-sensitive results: handled by the comparison.mode metadata.

5. Component Design

5.1 Problem Catalog

  • Input: problems.json.
  • Output: a browsable/filterable list; selecting a problem loads it into the
    workspace.
  • Notes: shows per-language availability (all 22 problems exist in
    JS/Ruby/Python today).

5.2 Editor Workspace

  • Input: selected problem and language; seeded with the function signature.
  • Output: the user's source code; emits "run" events.
  • Built on: CodeMirror 6 (wrapped as a React component) with the appropriate
    language mode.
  • Features: optional Vim keybindings (persisted toggle) and a "reset to
    signature" action; no autocomplete or syntax-guide panel in v1.0 (see
    §7.3).

5.3 Test Harness

The core new component. Strategy: assert in runtime. Assertions run natively
inside the target language's VM, which then serializes a structured result back
to the host. This compares results using each language's own equality/type
semantics — critical for the linked-list problems (Reverse Linked List, Remove
Nth From End, Detect Cycle) where marshalling to JS would be lossy — and lets the
existing per-language comparison matchers (exact, unordered_array,
set_equality) port over largely intact. (Note: the problem set contains no tree
problems, so ListNode is the only non-primitive marshalled type in v1.0.)

As implemented (M1–M2): the two runtimes realize this differently.

  • JavaScript — the harness is real worker code, not a generated string: a
    pure, synchronous runHarness(userCode, problem) evaluates the user function
    via new Function(...) and asserts using shared TypeScript comparators. Because
    it is worker-agnostic, the exact same function is unit-tested on the main thread
    under Vitest (workers don't run under jsdom).
  • Ruby — a static prelude.rb (ListNode, array↔list, comparators) is eval'd
    once at VM boot, and a driver.rb (__ptap_run) is eval'd per run. The
    metadata (function name, arg names, test cases) is passed in as base64-encoded
    JSON embedded in an eval string and decoded + JSON.parsed in-VM
    — the
    ruby.wasm JS bridge cannot pass a raw JS string as a Ruby argument, and base64
    is injection-safe. (Python/Pyodide would mirror the Ruby shape.)

For a (problem, language, userCode) tuple:

  1. Compose a runnable program in the target language: user code plus a generated
    harness. The harness receives the problem's test cases as a JSON string
    and parses it inside the VM (JSON.parse in Ruby / json.loads in
    Python) — inputs travel as data, never as code-generated literals, avoiding
    quoting/injection issues and using one mechanism across all languages.
  2. For each test case the harness builds the input, calls the problem's
    function, and asserts the return against expected using comparison.mode.
  3. Execute it in the language's runner (worker).
  4. The harness serializes per-case outcomes (pass/fail, actual, expected, error)
    plus timing into a JSON string and returns it as the eval's return value
    across the runtime's JS bridge
    (ruby.wasm RbValue / Pyodide runPython
    return) — not via stdout. This keeps the result protocol cleanly separated
    from the user's own stdout.
  5. The host deserializes that JSON into the TestRunResult contract
    (§6.1) for rendering.

User stdout is captured separately from the result protocol and surfaced in
the results panel as debug output (see §5.5),
so learners can puts/print while debugging without corrupting the harness
result.

Comparison-mode parity. The three comparison modes (exact,
unordered_array, set_equality) are implemented natively in each language. To
keep the implementations from diverging, a single shared fixture
comparison-vectors.json holds { mode, a, b, equal } vectors that every
language's comparator must satisfy; it is exercised in CI so a divergence fails
the build. As built, the JS comparators run directly under Vitest, and the Ruby
comparators are exercised by booting ruby.wasm (Node, via the pure-JS
browser_wasi_shim) inside a Vitest test and running every vector through the
real in-VM __ptap_compare.

5.4 Language Runners

  • JS Runner: native execution in a Worker; cheapest path, no download. Spawns
    a fresh worker per run so an infinite loop can be hard-terminated (3 s
    timeout); spawning is cheap.
  • Ruby Runner: ruby.wasm VM in a Worker; lazy-loaded WASM. Marshals inputs
    (JSON) into Ruby values and results back out. Because VM boot is multi-MB and
    takes seconds, it uses a persistent worker loaded once via init() and
    reused across runs (10 s execution timeout); on timeout the worker is terminated
    and the VM transparently re-booted on the next run.
  • Python Runner (stretch): Pyodide VM; same lazy-load, persistent-worker, and
    marshalling pattern as Ruby.

5.5 Results and Feedback Panel

  • Renders per-test pass/fail, diffs of expected vs. actual, runtime errors, and
    overall completion state; persists status to localStorage.
  • Also displays the user's captured stdout (debug puts/print output),
    kept separate from the harness result protocol (see §5.3).

5.6 Hints

v1.0 includes progressive hints (not full guided walkthroughs). Hint content
is authored in problems.json as a hints field per problem, keeping the JSON
the single source of truth.

Hints are language-agnostic: a single set per problem, shared across
JavaScript, Ruby, and Python. They describe the approach/algorithm (e.g., "use
a hash map to remember complements") rather than language-specific syntax, so
the same hint sequence applies regardless of the selected language. hints is a
top-level field on the problem, not keyed by language (unlike functionSignatures
and returnType).

  • Input: the selected problem's hints array (independent of language).
  • Output: progressively revealable hints (one at a time) in the problem pane.
  • Persistence: the number of hints revealed is tracked in localStorage
    (see §4.3).

The field is optional and the UI degrades gracefully when it is absent, so
authoring hints for all 22 problems can trail the engine work.


6. Interface Design

6.1 Internal Interfaces

A common LanguageRunner contract keeps the app core language-agnostic:

interface LanguageRunner {
  init(): Promise<void>            // load runtime (lazy)
  run(userCode: string,
      problem: Problem): Promise<TestRunResult>   // execute + verify
  dispose(): void                  // tear down worker / free memory
}

TestRunResult = {
  protocolVersion: number, // harness-protocol version (currently 1)
  passed: boolean,
  cases: Array<{ description, passed, expected, actual, error? }>,
  durationMs: number,
  runtimeError?: string,
  stdout?: string          // user debug output, captured separately
}

The in-VM harness asserts natively, then emits this exact JSON, identical across
JS/Ruby/Python:

{
  "protocolVersion": 1,
  "passed": false,
  "durationMs": 12.3,
  "runtimeError": null,
  "stdout": "",
  "cases": [
    { "description": "...", "passed": true,
      "expected": <json>, "actual": <json>, "error": null }
  ]
}
  • expected/actual are display-only canonical serializations — the
    pass/fail verdict is decided in-VM (so pointer structures like
    ListNode/TreeNode are compared with native semantics), and these fields
    exist only so the results panel can render a diff. Pointer structures
    serialize to their array representation for display.
  • error is per-case (assertion detail or an exception raised while running
    that case); runtimeError is reserved for load/parse/compile failures that
    prevent any case from running.
  • protocolVersion lets the three emitters be version-guarded against drift.

6.2 External Interfaces

  • None at runtime beyond fetching static assets (app bundle and WASM modules)
    from GitHub Pages. No external APIs, no auth.

6.3 Data Contracts

  • The problems.json schema (see §4.1) is
    the contract between content and app. Schema changes must remain
    backward-compatible with the browser app.

7. User Interface Design

7.1 Layout

  • Problem list / navigation (sidebar or top).
  • Problem statement pane (title, description, complexity target, examples).
  • Code editor pane (with language selector).
  • Run button plus results/feedback pane.

The layout is desktop-first: optimized for desktop, not broken on tablet,
with no dedicated mobile layout. A code editor plus results panel needs
horizontal space, and the solo-learner audience is desktop-centric.

7.2 Key Flows

  1. Land → browse problems → pick one.
  2. Pick language → editor seeds the signature.
  3. Write solution → Run → see per-case feedback.
  4. Pass → problem marked complete (persisted); move on.

7.3 Visual and UX Details

Visual direction: a fresh, themed design — not bound to alu's aesthetics.
The UI adopts a medieval illuminated-manuscript / "holy book" aesthetic to
match the archaic project name Practice Thy Algorithms. Motifs: parchment/vellum
textures, blackletter display typography (paired with a legible body/monospace
face for code), illuminated drop-caps, marginalia-style annotations, and
ornamental borders.

Typography and palette ("Scriptorium"). All faces are OFL-licensed and
Google-Fonts hosted (GitHub-Pages friendly):

Role Face
Display (headings only) UnifrakturMaguntia (blackletter)
Body EB Garamond (serif)
Code / editor JetBrains Mono
Palette role Hex
Parchment (background) #f4ecd8
Ink (text) #2b2117
Gold (illumination accent) #b8860b
Rubric (emphasis / errors) #8b2e2e

UnifrakturMaguntia is ornate, so it is restricted to short display headings;
body copy uses EB Garamond for readability.

The code editor and results panel must stay highly legible — theming must not
compromise readability of code or pass/fail feedback. The CodeMirror editor
theme should harmonize with the manuscript palette while keeping strong syntax
contrast.

Editor features:

  • Optional Vim keybindings — a toggle (CodeMirror extension); preference
    persisted in localStorage (see §4.3).
  • Reset to signature — a button that restores the editor to the starting
    function signature for the current (problem, language), discarding the
    in-progress attempt.
  • No autocomplete in v1.0.
  • No syntax-guide panel in v1.0.

8. Assumptions and Dependencies

8.1 Assumptions

  • Users run a modern, WASM-capable, evergreen browser.
  • The problem set and its test cases are correct and authoritative as defined in
    problems.json today.
  • Static client-side execution is acceptable — i.e., users could inspect or
    bypass the test cases (acceptable for a practice tool).

8.2 Dependencies

  • ruby.wasm (@ruby/wasm-wasi + @ruby/3.4-wasm-wasi, ruby+stdlib.wasm) —
    CRuby 3.4 compiled to WASM. Known limits: WASI Preview 1 (no sockets/networking,
    no threads) — fine for self-contained algorithm problems. Booted via the
    browser entry (pure-JS browser_wasi_shim); no COOP/COEP isolation needed,
    so it runs on plain GitHub Pages.
  • Pyodide (stretch) — CPython compiled to WASM.
  • Vite, React, CodeMirror 6, Vitest, Playwright.
  • GitHub Pages + GitHub Actions for hosting and CI.

8.3 Constraints

  • No backend and no persistent server-side state.
  • WASM payload sizes affect first-load UX for Ruby/Python (mitigated by lazy
    loading).

9. Alternatives Considered

Decision Options Chosen Rationale
Ruby in browser ruby.wasm (CRuby→WASM) vs. Opal (Ruby→JS transpile) vs. server-side ruby.wasm Runs real CRuby semantics client-side; matches the try.ruby-lang.org direction; no server. Opal diverges from real Ruby; a server breaks the static-hosting goal.
Test harness location Assert inside the WASM runtime vs. bridge values to JS and compare in JS Assert in runtime Native equality/type semantics and correct handling of ListNode/TreeNode returns; reuses existing per-language matchers. Trade-off: comparison logic maintained in three languages.
Reuse generators Keep generating CLI test files vs. run test cases live in the browser Run live in browser Avoids a build step; problems.json stays the single source of truth.
App language JavaScript vs. TypeScript TypeScript Type safety for runner contracts and JS↔WASM marshalling.
UI framework React vs. Preact / Svelte / Solid vs. near-vanilla React Familiar component model and declarative state for moderate interactivity.
Problem set (v1.0) Strict port vs. revisit content Strict port The existing 22 problems' passing tests are the regression baseline for trusting the in-browser runner; content curation deferred.
Python engine Pyodide vs. skip for v1.0 Defer (stretch) Keeps v1.0 scope tight.

10. Milestones and Rollout

  • M0 — Scaffold ✅ (done): Vite + React + TypeScript app and GitHub Pages CI
    deploying a minimal themed page (proves the deploy pipeline).
  • M1 — JS MVP ✅ (done): catalog, editor, JS runner, and in-runtime test
    harness across all 22 problems.
  • M2 — Ruby ✅ (done): ruby.wasm runner integrated and verified across all 22
    problems, with comparator parity asserted in CI.
  • M3 — Polish (next): persistence (localStorage of code/language/results),
    feedback UX, and hints (seed hint sequences for 2–3 problems to exercise the UI;
    the rest trail into v1.x). Vim-toggle persistence also lands here.
  • M4 — Stretch: Python via Pyodide (same persistent-worker + in-VM harness
    pattern as Ruby).
  • M5 — Cutover: retire/archive the legacy CLI workflow and rewrite the README
    around the browser app.

The M0→M5 ordering is firm; no calendar target dates are committed (solo,
spare-time project). Milestones advance in order as time allows. Current status:
M0–M2 complete and deployed; M3 is next.


11. Future Work

Items intentionally deferred beyond v1.0:

  • Hint authoring for the full set of 22 problems (content task; the engine
    degrades gracefully without it — see §5.6).
  • WASM payload optimization if first-Ruby-run latency proves painful
    (out of scope for v1.0 — see §3.3).
  • Python support via Pyodide (M4 stretch).
  • Content curation of the problem set (deferred post-v1.0 — see
    §2.2).

12. Glossary

  • ALU — Arithmetic Logic Unit; subject of the sibling reference project.
  • CRuby (MRI) — the reference C implementation of Ruby.
  • CodeMirror 6 — extensible in-browser code editor.
  • GitHub Pages — static site hosting served from a GitHub repo.
  • Pyodide — CPython compiled to WebAssembly for in-browser Python.
  • ruby.wasm — WebAssembly ports of CRuby; runs Ruby in the browser.
  • SPA — Single-Page Application.
  • WASI — WebAssembly System Interface (Preview 1 in current ruby.wasm).
  • WASM (WebAssembly) — portable binary instruction format runnable in browsers.
  • Web Worker — background browser thread for isolating/parallelizing work.

13. References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions