we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement Fetch API backed by net crate

Add fetch() global function that returns a Promise<Response>, backed by
the existing HTTP/1.1 client and TLS implementation in the net crate.

- fetch(url, options?) spawns a background thread for non-blocking I/O
- Response object: status, statusText, ok, url, headers, text(), json()
- Headers object: get(), has(), set(), delete() with case-insensitive matching
- Event loop integration: pending fetches polled alongside timers
- GC roots for pending fetch promises prevent premature collection
- Promise rejects on network errors (DNS failure, connection refused, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+766 -6
+2
Cargo.lock
··· 56 56 "we-css", 57 57 "we-dom", 58 58 "we-html", 59 + "we-net", 59 60 "we-style", 61 + "we-url", 60 62 ] 61 63 62 64 [[package]]
+2
crates/js/Cargo.toml
··· 12 12 we-css = { path = "../css" } 13 13 we-html = { path = "../html" } 14 14 we-style = { path = "../style" } 15 + we-net = { path = "../net" } 16 + we-url = { path = "../url" }
+14
crates/js/src/builtins.rs
··· 5026 5026 add_reaction(gc, promise, on_fulfilled, on_rejected) 5027 5027 } 5028 5028 5029 + /// Initialize Promise.prototype in a standalone GC (for unit tests that 5030 + /// create promise objects without a full VM). 5031 + pub fn init_promise_proto_for_test(gc: &mut Gc<HeapObject>) { 5032 + let proto_data = ObjectData::new(); 5033 + let promise_proto = gc.alloc(HeapObject::Object(proto_data)); 5034 + init_promise_prototype(gc, promise_proto); 5035 + PROMISE_PROTO.with(|cell| cell.set(Some(promise_proto))); 5036 + } 5037 + 5029 5038 fn init_promise_builtins(vm: &mut Vm) { 5030 5039 // Create Promise.prototype (inherits from Object.prototype). 5031 5040 let mut proto_data = ObjectData::new(); ··· 6147 6156 } 6148 6157 Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))) 6149 6158 } 6159 + } 6160 + 6161 + /// Public wrapper for `json_parse` used by the fetch module. 6162 + pub fn json_parse_pub(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 6163 + json_parse(args, ctx) 6150 6164 } 6151 6165 6152 6166 fn json_parse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
+714
crates/js/src/fetch.rs
··· 1 + //! Fetch API: `fetch()`, Response, Headers. 2 + //! 3 + //! Network I/O runs on background threads; the event loop polls for completed 4 + //! requests and resolves/rejects the returned promises. 5 + 6 + use std::cell::RefCell; 7 + use std::sync::{Arc, Mutex}; 8 + 9 + use crate::builtins::{ 10 + create_promise_object_pub, make_native, reject_promise_internal, resolve_promise_internal, 11 + set_builtin_prop, 12 + }; 13 + use crate::gc::{Gc, GcRef}; 14 + use crate::vm::*; 15 + 16 + // ── Pending fetch state ───────────────────────────────────────── 17 + 18 + /// Result of a completed HTTP fetch. 19 + pub struct FetchResult { 20 + pub status: u16, 21 + pub status_text: String, 22 + pub headers: Vec<(String, String)>, 23 + pub body: Vec<u8>, 24 + pub url: String, 25 + } 26 + 27 + /// A fetch that is in-flight or just completed. 28 + struct PendingFetch { 29 + /// The promise returned to JS. 30 + promise: GcRef, 31 + /// Shared slot: the background thread writes the result here. 32 + result: Arc<Mutex<Option<Result<FetchResult, String>>>>, 33 + } 34 + 35 + thread_local! { 36 + static FETCH_STATE: RefCell<Vec<PendingFetch>> = const { RefCell::new(Vec::new()) }; 37 + } 38 + 39 + /// Reset fetch state (useful for tests). 40 + pub fn reset_fetch_state() { 41 + FETCH_STATE.with(|s| s.borrow_mut().clear()); 42 + } 43 + 44 + /// Returns true if there are any in-flight fetches. 45 + pub fn has_pending_fetches() -> bool { 46 + FETCH_STATE.with(|s| !s.borrow().is_empty()) 47 + } 48 + 49 + /// Collect GC roots for pending fetch promises. 50 + pub fn fetch_gc_roots() -> Vec<GcRef> { 51 + FETCH_STATE.with(|s| s.borrow().iter().map(|f| f.promise).collect()) 52 + } 53 + 54 + /// A completed fetch ready to be resolved. 55 + pub struct CompletedFetch { 56 + pub promise: GcRef, 57 + pub result: Result<FetchResult, String>, 58 + } 59 + 60 + /// Take all completed fetches (leaving in-flight ones). 61 + pub fn take_completed_fetches() -> Vec<CompletedFetch> { 62 + FETCH_STATE.with(|s| { 63 + let mut state = s.borrow_mut(); 64 + let mut completed = Vec::new(); 65 + let mut still_pending = Vec::new(); 66 + 67 + for pending in state.drain(..) { 68 + let done = { 69 + let lock = pending.result.lock().unwrap(); 70 + lock.is_some() 71 + }; 72 + if done { 73 + let result = pending.result.lock().unwrap().take().unwrap(); 74 + completed.push(CompletedFetch { 75 + promise: pending.promise, 76 + result, 77 + }); 78 + } else { 79 + still_pending.push(pending); 80 + } 81 + } 82 + 83 + *state = still_pending; 84 + completed 85 + }) 86 + } 87 + 88 + // ── fetch() native function ───────────────────────────────────── 89 + 90 + /// `fetch(url, options?)` — initiate an HTTP request, return Promise<Response>. 91 + pub fn fetch_native(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 92 + // Parse URL argument. 93 + let url_str = match args.first() { 94 + Some(v) => v.to_js_string(ctx.gc), 95 + None => { 96 + return Err(RuntimeError::type_error( 97 + "fetch requires at least one argument", 98 + )) 99 + } 100 + }; 101 + 102 + // Parse options (method, headers, body). 103 + let mut method = "GET".to_string(); 104 + let mut req_headers: Vec<(String, String)> = Vec::new(); 105 + let mut body: Option<Vec<u8>> = None; 106 + 107 + if let Some(Value::Object(opts_ref)) = args.get(1) { 108 + if let Some(HeapObject::Object(data)) = ctx.gc.get(*opts_ref) { 109 + // method 110 + if let Some(prop) = data.properties.get("method") { 111 + if let Value::String(m) = &prop.value { 112 + method = m.to_uppercase(); 113 + } 114 + } 115 + // body 116 + if let Some(prop) = data.properties.get("body") { 117 + if let Value::String(b) = &prop.value { 118 + body = Some(b.as_bytes().to_vec()); 119 + } 120 + } 121 + // headers — read from nested object 122 + if let Some(prop) = data.properties.get("headers") { 123 + if let Some(hdrs_ref) = prop.value.gc_ref() { 124 + if let Some(HeapObject::Object(hdr_data)) = ctx.gc.get(hdrs_ref) { 125 + for (key, hdr_prop) in &hdr_data.properties { 126 + if hdr_prop.enumerable { 127 + if let Value::String(v) = &hdr_prop.value { 128 + req_headers.push((key.clone(), v.clone())); 129 + } 130 + } 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 + // Create a pending promise. 139 + let promise = create_promise_object_pub(ctx.gc); 140 + 141 + // Shared slot for the result. 142 + let result_slot: Arc<Mutex<Option<Result<FetchResult, String>>>> = Arc::new(Mutex::new(None)); 143 + let slot_clone = result_slot.clone(); 144 + 145 + // Register the pending fetch. 146 + FETCH_STATE.with(|s| { 147 + s.borrow_mut().push(PendingFetch { 148 + promise, 149 + result: result_slot, 150 + }); 151 + }); 152 + 153 + // Spawn a background thread for the network I/O. 154 + std::thread::spawn(move || { 155 + let result = do_fetch(&url_str, &method, &req_headers, body.as_deref()); 156 + let mut lock = slot_clone.lock().unwrap(); 157 + *lock = Some(result); 158 + }); 159 + 160 + Ok(Value::Object(promise)) 161 + } 162 + 163 + /// Perform the actual HTTP fetch (runs on a background thread). 164 + fn do_fetch( 165 + url_str: &str, 166 + method: &str, 167 + headers: &[(String, String)], 168 + body: Option<&[u8]>, 169 + ) -> Result<FetchResult, String> { 170 + let url = we_url::Url::parse(url_str).map_err(|e| format!("Invalid URL: {e}"))?; 171 + 172 + let mut req_headers = we_net::http::Headers::new(); 173 + for (key, val) in headers { 174 + req_headers.add(key, val); 175 + } 176 + 177 + let http_method = match method { 178 + "GET" => we_net::http::Method::Get, 179 + "POST" => we_net::http::Method::Post, 180 + "PUT" => we_net::http::Method::Put, 181 + "DELETE" => we_net::http::Method::Delete, 182 + "HEAD" => we_net::http::Method::Head, 183 + "OPTIONS" => we_net::http::Method::Options, 184 + "PATCH" => we_net::http::Method::Patch, 185 + other => return Err(format!("Unsupported HTTP method: {other}")), 186 + }; 187 + 188 + let mut client = we_net::client::HttpClient::new(); 189 + let response = client 190 + .request(http_method, &url, &req_headers, body) 191 + .map_err(|e| format!("Network error: {e}"))?; 192 + 193 + let resp_headers: Vec<(String, String)> = response 194 + .headers 195 + .iter() 196 + .map(|(k, v)| (k.to_string(), v.to_string())) 197 + .collect(); 198 + 199 + Ok(FetchResult { 200 + status: response.status_code, 201 + status_text: response.reason, 202 + headers: resp_headers, 203 + body: response.body, 204 + url: url.serialize(), 205 + }) 206 + } 207 + 208 + // ── Response object creation ──────────────────────────────────── 209 + 210 + /// Create a JS Response object from a completed fetch result. 211 + pub fn create_response_object(gc: &mut Gc<HeapObject>, result: &FetchResult) -> GcRef { 212 + let mut data = ObjectData::new(); 213 + 214 + data.properties.insert( 215 + "status".to_string(), 216 + Property::data(Value::Number(result.status as f64)), 217 + ); 218 + data.properties.insert( 219 + "statusText".to_string(), 220 + Property::data(Value::String(result.status_text.clone())), 221 + ); 222 + data.properties.insert( 223 + "ok".to_string(), 224 + Property::data(Value::Boolean((200..300).contains(&result.status))), 225 + ); 226 + data.properties.insert( 227 + "url".to_string(), 228 + Property::data(Value::String(result.url.clone())), 229 + ); 230 + 231 + // Store body as hidden property for text()/json(). 232 + let body_str = String::from_utf8_lossy(&result.body).to_string(); 233 + data.properties.insert( 234 + "__body__".to_string(), 235 + Property::builtin(Value::String(body_str)), 236 + ); 237 + 238 + let resp_ref = gc.alloc(HeapObject::Object(data)); 239 + 240 + // Create Headers object. 241 + let headers_ref = create_headers_object(gc, &result.headers); 242 + set_builtin_prop(gc, resp_ref, "headers", Value::Object(headers_ref)); 243 + 244 + // Register methods. 245 + let text_fn = make_native(gc, "text", response_text); 246 + set_builtin_prop(gc, resp_ref, "text", Value::Function(text_fn)); 247 + 248 + let json_fn = make_native(gc, "json", response_json); 249 + set_builtin_prop(gc, resp_ref, "json", Value::Function(json_fn)); 250 + 251 + resp_ref 252 + } 253 + 254 + /// `response.text()` — returns Promise<String> with the response body. 255 + fn response_text(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 256 + let body_str = get_hidden_body(ctx.gc, &ctx.this); 257 + let promise = create_promise_object_pub(ctx.gc); 258 + resolve_promise_internal(ctx.gc, promise, Value::String(body_str)); 259 + Ok(Value::Object(promise)) 260 + } 261 + 262 + /// `response.json()` — returns Promise<Object> with parsed JSON body. 263 + fn response_json(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 264 + let body_str = get_hidden_body(ctx.gc, &ctx.this); 265 + let promise = create_promise_object_pub(ctx.gc); 266 + let json_args = [Value::String(body_str)]; 267 + match crate::builtins::json_parse_pub(&json_args, ctx) { 268 + Ok(parsed) => resolve_promise_internal(ctx.gc, promise, parsed), 269 + Err(e) => { 270 + let err_val = Value::String(e.to_string()); 271 + reject_promise_internal(ctx.gc, promise, err_val); 272 + } 273 + } 274 + Ok(Value::Object(promise)) 275 + } 276 + 277 + /// Read the hidden `__body__` string from a Response object. 278 + fn get_hidden_body(gc: &Gc<HeapObject>, this: &Value) -> String { 279 + if let Some(obj_ref) = this.gc_ref() { 280 + if let Some(HeapObject::Object(data)) = gc.get(obj_ref) { 281 + if let Some(prop) = data.properties.get("__body__") { 282 + if let Value::String(s) = &prop.value { 283 + return s.clone(); 284 + } 285 + } 286 + } 287 + } 288 + String::new() 289 + } 290 + 291 + // ── Headers object ────────────────────────────────────────────── 292 + 293 + /// Create a JS Headers object from response headers. 294 + /// 295 + /// Headers are stored as hidden indexed properties for case-insensitive 296 + /// lookup. Duplicate header names are combined with ", " per HTTP spec. 297 + fn create_headers_object(gc: &mut Gc<HeapObject>, headers: &[(String, String)]) -> GcRef { 298 + // Combine headers with same name (case-insensitive). 299 + let mut combined: Vec<(String, String)> = Vec::new(); 300 + for (name, value) in headers { 301 + let lower = name.to_lowercase(); 302 + if let Some(existing) = combined.iter_mut().find(|(n, _)| *n == lower) { 303 + existing.1 = format!("{}, {value}", existing.1); 304 + } else { 305 + combined.push((lower, value.clone())); 306 + } 307 + } 308 + 309 + let mut data = ObjectData::new(); 310 + data.properties.insert( 311 + "__hdr_count__".to_string(), 312 + Property::builtin(Value::Number(combined.len() as f64)), 313 + ); 314 + for (i, (name, value)) in combined.iter().enumerate() { 315 + data.properties.insert( 316 + format!("__hdr_{i}_n__"), 317 + Property::builtin(Value::String(name.clone())), 318 + ); 319 + data.properties.insert( 320 + format!("__hdr_{i}_v__"), 321 + Property::builtin(Value::String(value.clone())), 322 + ); 323 + } 324 + 325 + let obj_ref = gc.alloc(HeapObject::Object(data)); 326 + 327 + let get_fn = make_native(gc, "get", headers_get); 328 + set_builtin_prop(gc, obj_ref, "get", Value::Function(get_fn)); 329 + 330 + let has_fn = make_native(gc, "has", headers_has); 331 + set_builtin_prop(gc, obj_ref, "has", Value::Function(has_fn)); 332 + 333 + let set_fn = make_native(gc, "set", headers_set); 334 + set_builtin_prop(gc, obj_ref, "set", Value::Function(set_fn)); 335 + 336 + let delete_fn = make_native(gc, "delete", headers_delete); 337 + set_builtin_prop(gc, obj_ref, "delete", Value::Function(delete_fn)); 338 + 339 + obj_ref 340 + } 341 + 342 + /// Read header entries from a Headers object: returns (count, Vec<(name, value)>). 343 + fn read_header_entries(gc: &Gc<HeapObject>, obj_ref: GcRef) -> Vec<(String, String)> { 344 + let mut entries = Vec::new(); 345 + if let Some(HeapObject::Object(data)) = gc.get(obj_ref) { 346 + let count = data 347 + .properties 348 + .get("__hdr_count__") 349 + .and_then(|p| { 350 + if let Value::Number(n) = &p.value { 351 + Some(*n as usize) 352 + } else { 353 + None 354 + } 355 + }) 356 + .unwrap_or(0); 357 + 358 + for i in 0..count { 359 + let name = data 360 + .properties 361 + .get(&format!("__hdr_{i}_n__")) 362 + .and_then(|p| { 363 + if let Value::String(s) = &p.value { 364 + Some(s.clone()) 365 + } else { 366 + None 367 + } 368 + }) 369 + .unwrap_or_default(); 370 + let value = data 371 + .properties 372 + .get(&format!("__hdr_{i}_v__")) 373 + .and_then(|p| { 374 + if let Value::String(s) = &p.value { 375 + Some(s.clone()) 376 + } else { 377 + None 378 + } 379 + }) 380 + .unwrap_or_default(); 381 + entries.push((name, value)); 382 + } 383 + } 384 + entries 385 + } 386 + 387 + /// `headers.get(name)` — case-insensitive lookup, returns string or null. 388 + fn headers_get(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 389 + let name = match args.first() { 390 + Some(Value::String(s)) => s.to_lowercase(), 391 + Some(v) => v.to_js_string(ctx.gc).to_lowercase(), 392 + None => return Ok(Value::Null), 393 + }; 394 + let obj_ref = match ctx.this.gc_ref() { 395 + Some(r) => r, 396 + None => return Ok(Value::Null), 397 + }; 398 + let entries = read_header_entries(ctx.gc, obj_ref); 399 + for (n, v) in entries { 400 + if n == name { 401 + return Ok(Value::String(v)); 402 + } 403 + } 404 + Ok(Value::Null) 405 + } 406 + 407 + /// `headers.has(name)` — case-insensitive check. 408 + fn headers_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 409 + let name = match args.first() { 410 + Some(Value::String(s)) => s.to_lowercase(), 411 + Some(v) => v.to_js_string(ctx.gc).to_lowercase(), 412 + None => return Ok(Value::Boolean(false)), 413 + }; 414 + let obj_ref = match ctx.this.gc_ref() { 415 + Some(r) => r, 416 + None => return Ok(Value::Boolean(false)), 417 + }; 418 + let entries = read_header_entries(ctx.gc, obj_ref); 419 + Ok(Value::Boolean(entries.iter().any(|(n, _)| n == &name))) 420 + } 421 + 422 + /// `headers.set(name, value)` — set or replace a header. 423 + fn headers_set(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 424 + let name = match args.first() { 425 + Some(Value::String(s)) => s.to_lowercase(), 426 + Some(v) => v.to_js_string(ctx.gc).to_lowercase(), 427 + None => return Ok(Value::Undefined), 428 + }; 429 + let value = match args.get(1) { 430 + Some(Value::String(s)) => s.clone(), 431 + Some(v) => v.to_js_string(ctx.gc), 432 + None => String::new(), 433 + }; 434 + let obj_ref = match ctx.this.gc_ref() { 435 + Some(r) => r, 436 + None => return Ok(Value::Undefined), 437 + }; 438 + let mut entries = read_header_entries(ctx.gc, obj_ref); 439 + let mut found = false; 440 + for entry in &mut entries { 441 + if entry.0 == name { 442 + entry.1 = value.clone(); 443 + found = true; 444 + break; 445 + } 446 + } 447 + if !found { 448 + entries.push((name, value)); 449 + } 450 + write_header_entries(ctx.gc, obj_ref, &entries); 451 + Ok(Value::Undefined) 452 + } 453 + 454 + /// `headers.delete(name)` — remove a header. 455 + fn headers_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 456 + let name = match args.first() { 457 + Some(Value::String(s)) => s.to_lowercase(), 458 + Some(v) => v.to_js_string(ctx.gc).to_lowercase(), 459 + None => return Ok(Value::Undefined), 460 + }; 461 + let obj_ref = match ctx.this.gc_ref() { 462 + Some(r) => r, 463 + None => return Ok(Value::Undefined), 464 + }; 465 + let mut entries = read_header_entries(ctx.gc, obj_ref); 466 + entries.retain(|(n, _)| n != &name); 467 + write_header_entries(ctx.gc, obj_ref, &entries); 468 + Ok(Value::Undefined) 469 + } 470 + 471 + /// Rewrite header entries into a Headers object. 472 + fn write_header_entries(gc: &mut Gc<HeapObject>, obj_ref: GcRef, entries: &[(String, String)]) { 473 + if let Some(HeapObject::Object(data)) = gc.get_mut(obj_ref) { 474 + // Remove old header entries. 475 + data.properties.retain(|k, _| !k.starts_with("__hdr_")); 476 + 477 + // Write new entries. 478 + data.properties.insert( 479 + "__hdr_count__".to_string(), 480 + Property::builtin(Value::Number(entries.len() as f64)), 481 + ); 482 + for (i, (name, value)) in entries.iter().enumerate() { 483 + data.properties.insert( 484 + format!("__hdr_{i}_n__"), 485 + Property::builtin(Value::String(name.clone())), 486 + ); 487 + data.properties.insert( 488 + format!("__hdr_{i}_v__"), 489 + Property::builtin(Value::String(value.clone())), 490 + ); 491 + } 492 + } 493 + } 494 + 495 + // ── Registration ──────────────────────────────────────────────── 496 + 497 + /// Register the `fetch` global function in the VM. 498 + pub fn init_fetch_api(vm: &mut Vm) { 499 + let fetch_fn = make_native(&mut vm.gc, "fetch", fetch_native); 500 + vm.set_global("fetch", Value::Function(fetch_fn)); 501 + } 502 + 503 + // ── Tests ─────────────────────────────────────────────────────── 504 + 505 + #[cfg(test)] 506 + mod tests { 507 + use super::*; 508 + use crate::builtins; 509 + use crate::compiler; 510 + use crate::parser::Parser; 511 + use std::cell::RefCell; 512 + use std::rc::Rc; 513 + 514 + struct CapturedConsole { 515 + log_messages: RefCell<Vec<String>>, 516 + } 517 + 518 + impl CapturedConsole { 519 + fn new() -> Self { 520 + Self { 521 + log_messages: RefCell::new(Vec::new()), 522 + } 523 + } 524 + } 525 + 526 + impl ConsoleOutput for CapturedConsole { 527 + fn log(&self, message: &str) { 528 + self.log_messages.borrow_mut().push(message.to_string()); 529 + } 530 + fn error(&self, _message: &str) {} 531 + fn warn(&self, _message: &str) {} 532 + } 533 + 534 + struct RcConsole(Rc<CapturedConsole>); 535 + 536 + impl ConsoleOutput for RcConsole { 537 + fn log(&self, message: &str) { 538 + self.0.log(message); 539 + } 540 + fn error(&self, message: &str) { 541 + self.0.error(message); 542 + } 543 + fn warn(&self, message: &str) { 544 + self.0.warn(message); 545 + } 546 + } 547 + 548 + #[test] 549 + fn test_fetch_returns_promise() { 550 + reset_fetch_state(); 551 + crate::timers::reset_timers(); 552 + 553 + let source = r#"typeof fetch"#; 554 + let program = Parser::parse(source).expect("parse failed"); 555 + let func = compiler::compile(&program).expect("compile failed"); 556 + let mut vm = Vm::new(); 557 + init_fetch_api(&mut vm); 558 + let result = vm.execute(&func).expect("execute failed"); 559 + assert_eq!(result.to_js_string(&vm.gc), "function"); 560 + } 561 + 562 + #[test] 563 + fn test_fetch_returns_promise_object() { 564 + reset_fetch_state(); 565 + crate::timers::reset_timers(); 566 + 567 + // fetch() should return something (a promise) even if the URL is bogus. 568 + // We can't easily test network fetches in unit tests, so we test the 569 + // synchronous parts: argument parsing, promise creation. 570 + let source = r#" 571 + var p = fetch("http://0.0.0.0:1/nonexistent"); 572 + typeof p 573 + "#; 574 + let program = Parser::parse(source).expect("parse failed"); 575 + let func = compiler::compile(&program).expect("compile failed"); 576 + let mut vm = Vm::new(); 577 + init_fetch_api(&mut vm); 578 + let result = vm.execute(&func).expect("execute failed"); 579 + assert_eq!(result.to_js_string(&vm.gc), "object"); 580 + } 581 + 582 + #[test] 583 + fn test_fetch_no_args_error() { 584 + reset_fetch_state(); 585 + crate::timers::reset_timers(); 586 + 587 + let source = r#" 588 + try { 589 + fetch(); 590 + "no error"; 591 + } catch(e) { 592 + "caught: " + e.message; 593 + } 594 + "#; 595 + let program = Parser::parse(source).expect("parse failed"); 596 + let func = compiler::compile(&program).expect("compile failed"); 597 + let mut vm = Vm::new(); 598 + init_fetch_api(&mut vm); 599 + let result = vm.execute(&func).expect("execute failed"); 600 + let s = result.to_js_string(&vm.gc); 601 + assert!( 602 + s.contains("fetch requires"), 603 + "expected error about missing args, got: {s}" 604 + ); 605 + } 606 + 607 + /// Execute JS with fetch API, pump event loop, return console logs. 608 + fn eval_with_fetch(source: &str, max_iterations: usize) -> Vec<String> { 609 + reset_fetch_state(); 610 + crate::timers::reset_timers(); 611 + let console = Rc::new(CapturedConsole::new()); 612 + let program = Parser::parse(source).expect("parse failed"); 613 + let func = compiler::compile(&program).expect("compile failed"); 614 + let mut vm = Vm::new(); 615 + init_fetch_api(&mut vm); 616 + vm.set_console_output(Box::new(RcConsole(console.clone()))); 617 + vm.execute(&func).expect("execute failed"); 618 + vm.run_event_loop(max_iterations) 619 + .expect("event loop failed"); 620 + let result = console.log_messages.borrow().clone(); 621 + result 622 + } 623 + 624 + #[test] 625 + fn test_fetch_rejects_on_network_error() { 626 + // Connect to a port that almost certainly won't be listening. 627 + let logs = eval_with_fetch( 628 + r#" 629 + fetch("http://127.0.0.1:1/fail").then( 630 + function(resp) { console.log("resolved: " + resp.status); }, 631 + function(err) { console.log("rejected"); } 632 + ); 633 + "#, 634 + 200, 635 + ); 636 + assert_eq!(logs, vec!["rejected"]); 637 + } 638 + 639 + #[test] 640 + fn test_response_object_properties() { 641 + // Test that create_response_object creates the right structure. 642 + let mut gc = crate::gc::Gc::new(); 643 + builtins::init_promise_proto_for_test(&mut gc); 644 + 645 + let result = FetchResult { 646 + status: 200, 647 + status_text: "OK".to_string(), 648 + headers: vec![ 649 + ("Content-Type".to_string(), "text/plain".to_string()), 650 + ("X-Custom".to_string(), "hello".to_string()), 651 + ], 652 + body: b"hello world".to_vec(), 653 + url: "http://example.com/".to_string(), 654 + }; 655 + 656 + let resp_ref = create_response_object(&mut gc, &result); 657 + let resp_obj = gc.get(resp_ref).unwrap(); 658 + if let HeapObject::Object(data) = resp_obj { 659 + // Check status 660 + assert!(matches!( 661 + data.properties.get("status").map(|p| &p.value), 662 + Some(Value::Number(n)) if *n == 200.0 663 + )); 664 + // Check ok 665 + assert!(matches!( 666 + data.properties.get("ok").map(|p| &p.value), 667 + Some(Value::Boolean(true)) 668 + )); 669 + // Check statusText 670 + assert!(matches!( 671 + data.properties.get("statusText").map(|p| &p.value), 672 + Some(Value::String(s)) if s == "OK" 673 + )); 674 + // Check url 675 + assert!(matches!( 676 + data.properties.get("url").map(|p| &p.value), 677 + Some(Value::String(s)) if s == "http://example.com/" 678 + )); 679 + } else { 680 + panic!("expected Object"); 681 + } 682 + } 683 + 684 + #[test] 685 + fn test_headers_get_case_insensitive() { 686 + let mut gc = crate::gc::Gc::new(); 687 + let headers = vec![ 688 + ("Content-Type".to_string(), "text/html".to_string()), 689 + ("X-Foo".to_string(), "bar".to_string()), 690 + ]; 691 + let hdr_ref = create_headers_object(&mut gc, &headers); 692 + let entries = read_header_entries(&gc, hdr_ref); 693 + assert_eq!(entries.len(), 2); 694 + // All stored lowercased 695 + assert!(entries 696 + .iter() 697 + .any(|(n, v)| n == "content-type" && v == "text/html")); 698 + assert!(entries.iter().any(|(n, v)| n == "x-foo" && v == "bar")); 699 + } 700 + 701 + #[test] 702 + fn test_headers_duplicate_combining() { 703 + let mut gc = crate::gc::Gc::new(); 704 + let headers = vec![ 705 + ("Set-Cookie".to_string(), "a=1".to_string()), 706 + ("Set-Cookie".to_string(), "b=2".to_string()), 707 + ]; 708 + let hdr_ref = create_headers_object(&mut gc, &headers); 709 + let entries = read_header_entries(&gc, hdr_ref); 710 + assert_eq!(entries.len(), 1); 711 + assert_eq!(entries[0].0, "set-cookie"); 712 + assert_eq!(entries[0].1, "a=1, b=2"); 713 + } 714 + }
+1
crates/js/src/lib.rs
··· 5 5 pub mod bytecode; 6 6 pub mod compiler; 7 7 pub mod dom_bridge; 8 + pub mod fetch; 8 9 pub mod gc; 9 10 pub mod lexer; 10 11 pub mod parser;
+33 -6
crates/js/src/vm.rs
··· 1276 1276 Ok(()) 1277 1277 } 1278 1278 1279 - /// Pump the event loop: drain due timers and microtasks. 1279 + /// Resolve/reject promises for completed fetch() requests. 1280 + fn drain_completed_fetches(&mut self) -> Result<(), RuntimeError> { 1281 + let completed = crate::fetch::take_completed_fetches(); 1282 + for fetch in completed { 1283 + match fetch.result { 1284 + Ok(result) => { 1285 + let response = crate::fetch::create_response_object(&mut self.gc, &result); 1286 + crate::builtins::resolve_promise_internal( 1287 + &mut self.gc, 1288 + fetch.promise, 1289 + Value::Object(response), 1290 + ); 1291 + } 1292 + Err(err_msg) => { 1293 + let err_val = Value::String(err_msg); 1294 + crate::builtins::reject_promise_internal(&mut self.gc, fetch.promise, err_val); 1295 + } 1296 + } 1297 + self.drain_microtasks()?; 1298 + } 1299 + Ok(()) 1300 + } 1301 + 1302 + /// Pump the event loop: drain due timers, completed fetches, and microtasks. 1280 1303 /// 1281 1304 /// Call this from tests or the platform event loop to fire any pending 1282 1305 /// timers whose delay has elapsed. Each timer callback is followed by 1283 1306 /// a microtask drain. 1284 1307 pub fn pump_event_loop(&mut self) -> Result<(), RuntimeError> { 1285 - self.drain_due_timers() 1308 + self.drain_due_timers()?; 1309 + self.drain_completed_fetches() 1286 1310 } 1287 1311 1288 - /// Run the event loop until all pending timers have fired. 1312 + /// Run the event loop until all pending timers and fetches have completed. 1289 1313 /// Useful in tests to deterministically execute all scheduled work. 1290 1314 /// `max_iterations` caps the loop to prevent infinite loops with 1291 1315 /// recurring intervals; pass 0 for unlimited. 1292 1316 pub fn run_event_loop(&mut self, max_iterations: usize) -> Result<(), RuntimeError> { 1293 1317 let mut iterations = 0; 1294 - while crate::timers::has_pending_timers() { 1318 + while crate::timers::has_pending_timers() || crate::fetch::has_pending_fetches() { 1295 1319 if max_iterations > 0 && iterations >= max_iterations { 1296 1320 break; 1297 1321 } 1298 1322 self.drain_due_timers()?; 1323 + self.drain_completed_fetches()?; 1299 1324 iterations += 1; 1300 - // If timers are not yet due, sleep briefly to avoid spinning. 1301 - if crate::timers::has_pending_timers() { 1325 + // If work is still pending, sleep briefly to avoid spinning. 1326 + if crate::timers::has_pending_timers() || crate::fetch::has_pending_fetches() { 1302 1327 std::thread::sleep(std::time::Duration::from_millis(1)); 1303 1328 } 1304 1329 } ··· 2108 2133 } 2109 2134 // Pending timer callbacks must be GC roots. 2110 2135 roots.extend(crate::timers::timer_gc_roots()); 2136 + // Pending fetch promises must be GC roots. 2137 + roots.extend(crate::fetch::fetch_gc_roots()); 2111 2138 roots 2112 2139 } 2113 2140