Files
rust_browser/_bmad-output/implementation-artifacts/3-10-web-api-exposure.md
Zachary D. Rowitsch fb64ca1d34
All checks were successful
ci / fast (linux) (push) Successful in 7m9s
Create story files for Epic 3 stories 3.5-3.10
Create comprehensive implementation-ready story files for the remaining
Epic 3 (JavaScript Engine Maturity) stories and update sprint status
from backlog to ready-for-dev:

- 3.5: Built-in Completeness (Array/String/Object)
- 3.6: Built-in Completeness (Date/RegExp/Map/Set)
- 3.7: WeakRef, FinalizationRegistry & Strict Mode Edge Cases
- 3.8: DOM Bindings via web_api
- 3.9: Event Dispatch Completeness
- 3.10: Web API Exposure (fetch, Math, setInterval, rAF)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:06:03 -04:00

23 KiB

Story 3.10: Web API Exposure

Status: ready-for-dev

Story

As a web developer using JavaScript, I want fetch, console, and scheduling APIs to work correctly, So that network requests, debugging, and async task scheduling function properly.

Acceptance Criteria

  1. fetch() API: fetch(url) returns a Promise that resolves to a Response object. response.status, .statusText, .ok, .headers reflect HTTP response. response.text() returns Promise resolving to body string. response.json() returns Promise resolving to parsed JSON. response.url reflects final URL. Uses NetworkStack::load_sync() internally (synchronous load, Promise wraps result). Per Fetch Standard §5.

  2. setInterval / clearInterval: setInterval(callback, delay) returns numeric ID and calls callback repeatedly every delay ms. clearInterval(id) stops the repeating calls. Per HTML §8.6.

  3. requestAnimationFrame: requestAnimationFrame(callback) registers callback to run before next paint. Callback receives a DOMHighResTimeStamp argument (milliseconds since page load). Callbacks execute in registration order. cancelAnimationFrame(id) cancels a registered callback. Per HTML §8.10.

  4. Console completeness: console.dir(obj) outputs object with enumerable properties. console.table(data) outputs tabular data (simplified: format as key-value pairs). console.assert(condition, ...args) logs only if condition is falsy. console.count(label) / console.countReset(label) track call counts. console.time(label) / console.timeEnd(label) measure elapsed time. Per Console Standard.

  5. Global utility functions: parseInt(string, radix), parseFloat(string), isNaN(value), isFinite(value), Number.isNaN(value), Number.isFinite(value), Number.isInteger(value), Number.parseInt(string, radix), Number.parseFloat(string). Per ECMAScript §19.2.

  6. Math object: Math.PI, Math.E, Math.abs, Math.ceil, Math.floor, Math.round, Math.trunc, Math.min, Math.max, Math.pow, Math.sqrt, Math.random, Math.log, Math.log2, Math.log10, Math.sin, Math.cos, Math.tan, Math.sign, Math.fround, Math.clz32. Per ECMAScript §21.3.

  7. URI encoding: encodeURI(string), decodeURI(string), encodeURIComponent(string), decodeURIComponent(string). Per ECMAScript §19.2.6.

  8. Promise.all / Promise.race / Promise.allSettled / Promise.any: Complete the Promise static combinator methods. Per ECMAScript §27.2.4.

  9. Integration tests verify each API, and just ci passes.

What NOT to Implement

  • No fetch() with Request objects -- only string URL argument. No custom headers, methods, or body. POST/PUT deferred.
  • No Response.arrayBuffer()/Response.blob()/Response.formData() -- only .text() and .json() body methods.
  • No Headers object -- response.headers returns a simplified object with .get(name) method only.
  • No streaming fetch -- Response.body (ReadableStream) out of scope.
  • No AbortController/AbortSignal -- fetch cancellation deferred.
  • No window.location/window.navigator/window.history -- browser shell APIs deferred to Epic 5.
  • No window.getComputedStyle() -- requires layout data access from JS, complex threading. Deferred.
  • No localStorage/sessionStorage -- deferred to Epic 6.
  • No URL constructor -- URL parsing from JS deferred.
  • No TextEncoder/TextDecoder -- encoding APIs deferred.
  • No atob()/btoa() -- Base64 encoding deferred.
  • No crypto.getRandomValues() -- Web Crypto deferred.
  • No performance.now() -- Performance API deferred.

Files to Modify

File Change
crates/web_api/src/dom_host/host_environment.rs Add fetch() to call_global_function(). Add setInterval/clearInterval/requestAnimationFrame/cancelAnimationFrame.
crates/web_api/src/dom_host/fetch_bridge.rs New file -- fetch() implementation: create Response HostObject, wire to NetworkStack::load_sync(), return Promise.
crates/web_api/src/scheduling.rs Add interval task support (repeating timers). Add animation frame callback queue.
crates/web_api/src/lib.rs Add response storage for fetch. Add rAF callback list. Add advance_animation_frame() method.
crates/js_vm/src/interpreter/mod.rs Add Math object to setup_builtins(). Add parseInt/parseFloat/isNaN/isFinite as global functions. Add encodeURI/decodeURI/encodeURIComponent/decodeURIComponent.
crates/js_vm/src/interpreter/math_builtins.rs New file -- Math object with all static methods and constants.
crates/js_vm/src/interpreter/global_builtins.rs New file -- parseInt, parseFloat, isNaN, isFinite, URI encoding/decoding functions.
crates/js_vm/src/interpreter/builtins.rs Add console.dir, console.table, console.assert, console.count/countReset, console.time/timeEnd to console dispatch.
crates/web_api/src/promise.rs Add Promise.all(), Promise.race(), Promise.allSettled(), Promise.any() static methods.
crates/js_vm/src/interpreter/primitive_builtins.rs Add Number.isNaN, Number.isFinite, Number.isInteger, Number.parseInt, Number.parseFloat to Number constructor.
crates/js_vm/src/interpreter/tests/math_tests.rs New file -- Math object unit tests.
crates/js_vm/src/interpreter/tests/global_tests.rs New file -- parseInt/parseFloat/isNaN/isFinite/URI encoding tests.
tests/js_scheduling.rs Add setInterval, requestAnimationFrame tests.
tests/js_tests.rs Add fetch, Promise combinator tests.
docs/JavaScript_Implementation_Checklist.md Check off Math, parseInt/parseFloat, URI encoding, fetch, setInterval, rAF items.
docs/old/js_feature_matrix.md Update Web API coverage.

Tasks / Subtasks

  • Task 1: Math object (AC: #6)

    • 1.1 Create crates/js_vm/src/interpreter/math_builtins.rs:
      • setup_math_builtins(env: &mut Environment) -- registers Math as a plain object (NOT a constructor)
      • Constants: Math.PI, Math.E, Math.LN2, Math.LN10, Math.LOG2E, Math.LOG10E, Math.SQRT2, Math.SQRT1_2
      • All constants use Rust std::f64::consts
    • 1.2 Implement Math static methods:
      • abs(x), ceil(x), floor(x), round(x), trunc(x) -- use Rust f64 methods
      • min(...values), max(...values) -- handle 0 args (Infinity/-Infinity), NaN propagation
      • pow(base, exp), sqrt(x), cbrt(x), hypot(...values)
      • log(x) (natural), log2(x), log10(x), exp(x), expm1(x), log1p(x)
      • sin(x), cos(x), tan(x), asin(x), acos(x), atan(x), atan2(y, x)
      • sinh(x), cosh(x), tanh(x), asinh(x), acosh(x), atanh(x)
      • sign(x), fround(x), clz32(x), imul(a, b)
      • random() -- use std::collections::hash_map::RandomState or simple PRNG (no rand crate dependency)
    • 1.3 Wire Math into setup_builtins() in mod.rs
    • 1.4 Implement as NativeFunction values on the Math object:
      • Each method is JsValue::NativeFunction { name, func } set on Math object
      • Math is a global object, not a constructor (no new Math())
    • 1.5 Add unit tests in crates/js_vm/src/interpreter/tests/math_tests.rs:
      • Constants have correct values
      • Each method with basic inputs and edge cases (NaN, Infinity, -0)
      • Math.random() returns [0, 1)
      • Math.min()/Math.max() with no args
      • Math.round(-0.5) edge case
  • Task 2: Global utility functions (AC: #5, #7)

    • 2.1 Create crates/js_vm/src/interpreter/global_builtins.rs:
      • setup_global_builtins(env: &mut Environment)
    • 2.2 Implement parseInt(string, radix) (ECMAScript §19.2.5):
      • Strip leading whitespace
      • Handle 0x/0X prefix for hex (radix=16)
      • Parse digits for given radix (2-36), default radix=10
      • Return NaN if no valid digits
      • Handle leading +/- sign
    • 2.3 Implement parseFloat(string) (ECMAScript §19.2.4):
      • Strip leading whitespace
      • Parse decimal number (including scientific notation)
      • Return NaN if no valid number
    • 2.4 Implement isNaN(value) / isFinite(value) (ECMAScript §19.2.2/§19.2.3):
      • isNaN: convert to number first, then check NaN (loose)
      • isFinite: convert to number first, then check finite (loose)
    • 2.5 Add Number static methods to primitive_builtins.rs:
      • Number.isNaN(value) -- strict: no type coercion, true only for actual NaN
      • Number.isFinite(value) -- strict: no type coercion
      • Number.isInteger(value) -- check if value is integer
      • Number.parseInt(string, radix) -- same as global parseInt
      • Number.parseFloat(string) -- same as global parseFloat
      • Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.MAX_VALUE, Number.MIN_VALUE, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NaN
    • 2.6 Implement URI encoding/decoding functions:
      • encodeURI(string) -- encode URI, preserve ;,/?:@&=+$-_.!~*'()# and alphanumeric
      • decodeURI(string) -- decode %XX sequences (preserve reserved chars)
      • encodeURIComponent(string) -- encode all except -_.!~*'()
      • decodeURIComponent(string) -- decode all %XX sequences
      • Use percent-encoding crate (already a dependency) or manual UTF-8 encoding
    • 2.7 Wire all global functions into setup_builtins() in mod.rs
    • 2.8 Add unit tests in crates/js_vm/src/interpreter/tests/global_tests.rs
  • Task 3: fetch() API (AC: #1)

    • 3.1 Create crates/web_api/src/dom_host/fetch_bridge.rs:
      • Response storage: HashMap<u64, FetchResponse> in WebApiFacade
      • FetchResponse struct: status: u16, status_text: String, url: String, headers: HashMap<String, String>, body: String, ok: bool
      • ID range: FETCH_RESPONSE_ID_BASE (new range in ID space)
    • 3.2 Implement fetch(url) in call_global_function():
      • Accept URL string argument
      • Use NetworkStack::load_sync() via DomHost's network reference (wired in Story 3.4)
      • If network unavailable → reject Promise with TypeError
      • Create FetchResponse from HTTP response (status, headers, body)
      • Create Promise, resolve with Response HostObject
      • Return the Promise
      • Note: load_sync() is blocking. fetch() wraps it in a Promise for API compatibility, but resolution is synchronous (same pattern as dynamic import).
    • 3.3 Implement Response HostObject properties via get_property():
      • "status" → Number (HTTP status code)
      • "statusText" → String (HTTP status text)
      • "ok" → Boolean (status 200-299)
      • "url" → String (final URL)
      • "headers" → Headers HostObject (simplified)
    • 3.4 Implement Response methods via call_method():
      • .text() → Promise resolving to body string
      • .json() → Promise resolving to JSON.parse(body) result
      • Promises resolve synchronously (body already available)
    • 3.5 Implement simplified Headers HostObject:
      • .get(name) → return header value string or null (case-insensitive lookup)
      • .has(name) → boolean
    • 3.6 Handle fetch errors:
      • Network error → reject Promise with TypeError("Failed to fetch")
      • Invalid URL → reject Promise with TypeError
      • HTTP errors (4xx, 5xx) → resolve normally (Response.ok = false), NOT reject
    • 3.7 Add integration tests:
      • fetch("http://...") returns Response with status, text(), json()
      • Network error rejects Promise
      • 404 response resolves with response.ok === false
      • response.headers.get("content-type") works
  • Task 4: setInterval and requestAnimationFrame (AC: #2, #3)

    • 4.1 Add interval support to scheduling.rs:
      • IntervalTask struct: id: u32, callback: JsValue, interval_ms: u64, next_fire_at: u64
      • When interval fires: re-enqueue with next_fire_at += interval_ms
      • add_interval(callback, interval_ms) -> u32 returns ID
      • cancel_interval(id) removes from queue
    • 4.2 Implement setInterval/clearInterval in call_global_function():
      • setInterval(callback, delay) → register repeating timer, return ID
      • clearInterval(id) → cancel timer
      • IDs should be in same namespace as setTimeout (unified timer ID space)
    • 4.3 Add requestAnimationFrame support:
      • RafCallback struct: id: u32, callback: JsValue
      • raf_queue: Vec<RafCallback> in WebApiFacade
      • requestAnimationFrame(callback) → push to raf_queue, return ID
      • cancelAnimationFrame(id) → remove from queue
      • flush_raf_callbacks(timestamp_ms: f64) → invoke all registered callbacks with timestamp, clear queue
    • 4.4 Wire rAF into render loop:
      • In crates/app_browser/src/event_handler.rs, call flush_raf_callbacks() before paint
      • Pass performance.now() equivalent timestamp (ms since page load)
    • 4.5 Add tests:
      • setInterval fires multiple times
      • clearInterval stops firing
      • Timer IDs don't collide between setTimeout and setInterval
      • requestAnimationFrame callback receives timestamp
      • cancelAnimationFrame prevents callback
  • Task 5: Console enhancements (AC: #4)

    • 5.1 Add console.dir(obj) to console dispatch:
      • Output object with all enumerable properties, one per line
      • Format: Object { key: value, key2: value2 } (simplified)
    • 5.2 Add console.table(data):
      • If array of objects: output as key-value table (simplified text format)
      • If object: output key-value pairs
      • Simplified: format as structured text, not actual ASCII table
    • 5.3 Add console.assert(condition, ...args):
      • If condition is falsy: output "Assertion failed: " + args joined
      • If condition is truthy: no output
    • 5.4 Add console.count(label) / console.countReset(label):
      • Track counter per label string in interpreter state
      • count() outputs "label: N" and increments
      • countReset() resets counter to 0
      • Default label: "default"
    • 5.5 Add console.time(label) / console.timeEnd(label):
      • time(): record start timestamp (use std::time::Instant::now())
      • timeEnd(): output "label: Nms" elapsed time
      • Store timers in interpreter state
      • Default label: "default"
    • 5.6 Add tests for each new console method
  • Task 6: Promise combinators (AC: #8)

    • 6.1 Implement Promise.all(iterable) in promise.rs or host environment:
      • Accept array of Promises (or values)
      • Return Promise that resolves when ALL resolve (with array of results)
      • Reject immediately if ANY rejects (with first rejection reason)
      • Handle non-Promise values (wrap in Promise.resolve())
    • 6.2 Implement Promise.race(iterable):
      • Return Promise that settles as soon as first Promise settles (resolve or reject)
    • 6.3 Implement Promise.allSettled(iterable):
      • Return Promise resolving when ALL settle
      • Result array: [{status: "fulfilled", value}, {status: "rejected", reason}]
    • 6.4 Implement Promise.any(iterable):
      • Resolve with first fulfillment
      • Reject with AggregateError if ALL reject
    • 6.5 Wire Promise static methods into construct() or call_method() dispatch
    • 6.6 Add tests for each combinator with mixed resolve/reject scenarios
  • Task 7: Testing and validation (AC: #9)

    • 7.1 Add integration tests in appropriate test files:
      • Math: basic operations, constants, edge cases
      • parseInt/parseFloat/isNaN/isFinite with various inputs
      • URI encoding/decoding roundtrip
      • fetch with real HTTP server (use tiny_http in test, pattern from existing tests)
      • setInterval fires multiple times (test with virtual clock)
      • requestAnimationFrame receives timestamp
      • Promise.all/race/allSettled/any with multiple Promises
    • 7.2 Run all existing test suites:
      • cargo test -p js_vm
      • cargo test -p web_api
      • cargo test -p rust_browser --test js_tests
      • cargo test -p rust_browser --test js_scheduling
      • cargo test -p rust_browser --test js_dom_tests
      • cargo test -p rust_browser --test js_events
      • cargo test -p rust_browser --test js_async
      • cargo test -p rust_browser --test js_modules
    • 7.3 Update docs/JavaScript_Implementation_Checklist.md
    • 7.4 Update docs/old/js_feature_matrix.md
    • 7.5 Run just ci -- full validation pass

Dev Notes

Key Architecture Decisions

fetch() is synchronous under the hood. NetworkStack::load_sync() blocks. fetch() wraps this in a Promise for API compatibility, but the Promise resolves synchronously (same pattern as import() dynamic import from Story 3.4). This is acceptable for the single-threaded model.

Math object is a plain global, not a constructor. Math is set up like JSON -- a regular object with static methods. new Math() should throw TypeError (or just fail silently). No prototype chain needed.

parseInt is a global AND on Number. parseInt/parseFloat are both global functions and Number.parseInt/Number.parseFloat. Implement once, reference twice.

Math.random() without rand crate. Use a simple xorshift64 PRNG seeded from std::time::SystemTime::now(). Store PRNG state in interpreter. No need for cryptographic quality.

setInterval uses same timer ID space as setTimeout. Unified next_timer_id: u32 counter. clearTimeout and clearInterval are interchangeable per spec (clearing either type with either function works).

requestAnimationFrame integrates with render loop. In event_handler.rs, after processing events and before painting, call flush_raf_callbacks(). Pass elapsed time since page load as the timestamp argument.

Implementation Patterns

Console sentinel functions (in bytecode_exec.rs):

"__console_log__" => { /* format args, write to output */ }
"__console_warn__" => { /* same with "WARN: " prefix */ }
// Add: "__console_dir__", "__console_table__", "__console_assert__", etc.

Global function setup (in mod.rs:setup_builtins()):

env.define_global("parseInt", JsValue::NativeFunction {
    name: "parseInt".into(),
    func: |args| { /* ... */ }
});

Math setup (in math_builtins.rs):

let math = JsObject::new();
math.set("PI", JsValue::Number(std::f64::consts::PI));
math.set("abs", JsValue::NativeFunction { name: "abs".into(), func: |args| {
    let x = args.first().map_or(f64::NAN, |v| v.to_number());
    Ok(JsValue::Number(x.abs()))
}});
env.define_global("Math", JsValue::Object(math));

Critical Implementation Details

parseInt radix handling: Default radix is 10 (NOT 8 for "010"). With 0x prefix and no explicit radix, use 16. With explicit radix, ignore prefix. Radix outside 2-36 returns NaN. Radix 0 treated as 10.

URI encoding uses UTF-8. Each non-ASCII character is encoded as multiple %XX sequences. Use Rust's string UTF-8 encoding. The percent-encoding crate (already a workspace dependency) provides utf8_percent_encode().

Promise.all short-circuits on first rejection. Don't wait for all Promises to settle. As soon as one rejects, reject the aggregate. However, remaining Promises still run (no cancellation).

fetch() needs network access. DomHost already has network: Option<&NetworkStack> from Story 3.4. Reuse this. If network is None (no network stack available), reject with TypeError.

console.time uses Instant, not SystemTime. std::time::Instant is monotonic and suitable for elapsed time measurement. Store HashMap<String, Instant> in interpreter state for console timers.

Dependencies

Story 3.4 (ES Modules): NetworkStack is already wired into DomHost via with_network(). fetch() reuses this.

No dependency on Stories 3.5-3.9. This story is largely independent -- Math, parseInt, fetch, setInterval are self-contained.

Previous Story Patterns

From Story 3.4:

  • Promise-wrapping synchronous operations (used for import() and now fetch())
  • NetworkStack access via DomHost.network field

From Story 3.6:

  • New builtin setup pattern: create module, wire into setup_builtins()

Risk Assessment

LOW: Math object. Pure functions wrapping Rust f64 methods. Straightforward.

LOW: parseInt/parseFloat. Well-defined spec, mainly string parsing.

MEDIUM: fetch() API. Requires Promise creation, Response HostObject storage, and network integration. Multiple ID spaces and HostObject types to manage.

MEDIUM: setInterval. Repeating timers need re-enqueue logic in the scheduling system. Must handle cases where callback takes longer than interval.

MEDIUM: Promise combinators. Promise.all/race need to track multiple Promise states. Integration with existing PromiseRegistry may be complex.

LOW: Console enhancements. Simple extensions to existing sentinel function pattern.

Phased Implementation Strategy

Phase A -- Math + Global Utilities (Tasks 1-2): Pure JS engine additions. No web_api changes. Highest value for Test262 compliance.

Phase B -- fetch() (Task 3): New HostObject type. Network integration.

Phase C -- setInterval + rAF (Task 4): Scheduling extensions.

Phase D -- Console + Promise Combinators (Tasks 5-6): Enhancements to existing systems.

Phase E -- Testing + Validation (Task 7): After all APIs implemented.

Project Structure Notes

  • Math/parseInt/parseFloat/URI in crates/js_vm/src/interpreter/ (Layer 1) -- pure JS engine
  • fetch bridge in crates/web_api/src/dom_host/ (Layer 1) -- needs net crate access
  • setInterval/rAF in crates/web_api/src/scheduling.rs (Layer 1)
  • rAF integration in crates/app_browser/src/event_handler.rs (Layer 3)
  • No unsafe code needed
  • No new external dependencies (percent-encoding already available)

References

Dev Agent Record

Agent Model Used

{{agent_model_name_version}}

Debug Log References

Completion Notes List

File List