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>
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
-
fetch() API:
fetch(url)returns a Promise that resolves to a Response object.response.status,.statusText,.ok,.headersreflect HTTP response.response.text()returns Promise resolving to body string.response.json()returns Promise resolving to parsed JSON.response.urlreflects final URL. UsesNetworkStack::load_sync()internally (synchronous load, Promise wraps result). Per Fetch Standard §5. -
setInterval / clearInterval:
setInterval(callback, delay)returns numeric ID and callscallbackrepeatedly everydelayms.clearInterval(id)stops the repeating calls. Per HTML §8.6. -
requestAnimationFrame:
requestAnimationFrame(callback)registerscallbackto run before next paint. Callback receives aDOMHighResTimeStampargument (milliseconds since page load). Callbacks execute in registration order.cancelAnimationFrame(id)cancels a registered callback. Per HTML §8.10. -
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. -
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. -
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. -
URI encoding:
encodeURI(string),decodeURI(string),encodeURIComponent(string),decodeURIComponent(string). Per ECMAScript §19.2.6. -
Promise.all / Promise.race / Promise.allSettled / Promise.any: Complete the Promise static combinator methods. Per ECMAScript §27.2.4.
-
Integration tests verify each API, and
just cipasses.
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
Headersobject --response.headersreturns 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
URLconstructor -- 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)-- registersMathas 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 methodsmin(...values),max(...values)-- handle 0 args (Infinity/-Infinity), NaN propagationpow(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()-- usestd::collections::hash_map::RandomStateor simple PRNG (norandcrate dependency)
- 1.3 Wire
Mathintosetup_builtins()inmod.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())
- Each method is
- 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 argsMath.round(-0.5)edge case
- 1.1 Create
-
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/0Xprefix 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 NaNNumber.isFinite(value)-- strict: no type coercionNumber.isInteger(value)-- check if value is integerNumber.parseInt(string, radix)-- same as global parseIntNumber.parseFloat(string)-- same as global parseFloatNumber.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 alphanumericdecodeURI(string)-- decode %XX sequences (preserve reserved chars)encodeURIComponent(string)-- encode all except-_.!~*'()decodeURIComponent(string)-- decode all %XX sequences- Use
percent-encodingcrate (already a dependency) or manual UTF-8 encoding
- 2.7 Wire all global functions into
setup_builtins()inmod.rs - 2.8 Add unit tests in
crates/js_vm/src/interpreter/tests/global_tests.rs
- 2.1 Create
-
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 FetchResponsestruct: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)
- Response storage:
- 3.2 Implement
fetch(url)incall_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
FetchResponsefrom 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
- 3.1 Create
-
Task 4: setInterval and requestAnimationFrame (AC: #2, #3)
- 4.1 Add interval support to
scheduling.rs:IntervalTaskstruct: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) -> u32returns IDcancel_interval(id)removes from queue
- 4.2 Implement
setInterval/clearIntervalincall_global_function():setInterval(callback, delay)→ register repeating timer, return IDclearInterval(id)→ cancel timer- IDs should be in same namespace as setTimeout (unified timer ID space)
- 4.3 Add requestAnimationFrame support:
RafCallbackstruct:id: u32,callback: JsValueraf_queue: Vec<RafCallback>in WebApiFacaderequestAnimationFrame(callback)→ push to raf_queue, return IDcancelAnimationFrame(id)→ remove from queueflush_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, callflush_raf_callbacks()before paint - Pass
performance.now()equivalent timestamp (ms since page load)
- In
- 4.5 Add tests:
setIntervalfires multiple timesclearIntervalstops firing- Timer IDs don't collide between setTimeout and setInterval
requestAnimationFramecallback receives timestampcancelAnimationFrameprevents callback
- 4.1 Add interval support to
-
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
- If condition is falsy: output
- 5.4 Add
console.count(label)/console.countReset(label):- Track counter per label string in interpreter state
count()outputs"label: N"and incrementscountReset()resets counter to 0- Default label:
"default"
- 5.5 Add
console.time(label)/console.timeEnd(label):time(): record start timestamp (usestd::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
- 5.1 Add
-
Task 6: Promise combinators (AC: #8)
- 6.1 Implement
Promise.all(iterable)inpromise.rsor 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
AggregateErrorif ALL reject
- 6.5 Wire Promise static methods into
construct()orcall_method()dispatch - 6.6 Add tests for each combinator with mixed resolve/reject scenarios
- 6.1 Implement
-
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_httpin 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_vmcargo test -p web_apicargo test -p rust_browser --test js_testscargo test -p rust_browser --test js_schedulingcargo test -p rust_browser --test js_dom_testscargo test -p rust_browser --test js_eventscargo test -p rust_browser --test js_asynccargo 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
- 7.1 Add integration tests in appropriate test files:
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 nowfetch()) - 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) -- needsnetcrate 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
unsafecode needed - No new external dependencies (percent-encoding already available)
References
- Fetch Standard §5 -- Fetch API -- fetch() function
- Fetch Standard §6.4 -- Response -- Response interface
- HTML §8.6 -- Timers -- setTimeout, setInterval
- HTML §8.10 -- Animation Frames -- requestAnimationFrame
- Console Standard -- Console API
- ECMAScript §21.3 -- Math Object -- Math methods and constants
- ECMAScript §19.2 -- Function Properties of Global Object -- parseInt, parseFloat, isNaN, isFinite, URI encoding
- ECMAScript §27.2.4 -- Promise Static Methods -- Promise.all, race, allSettled, any
- [Source: crates/js_vm/src/interpreter/mod.rs:559-583] -- Existing console implementation
- [Source: crates/web_api/src/scheduling.rs] -- TaskQueue, timer infrastructure
- [Source: crates/web_api/src/dom_host/host_environment.rs:735-756] -- setTimeout/clearTimeout
- [Source: crates/web_api/src/promise.rs] -- Promise implementation
- [Source: _bmad-output/planning-artifacts/epics.md#Story 3.10] -- Story requirements
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}