Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123//! JavaScript bindings for the Cache API (`caches`, `Cache`).//!//! Storage lives in a thread-local [`CacheStorage`] keyed by the document//! origin currently associated with this VM. The embedder (the `browser`//! crate) is responsible for loading the origin's persisted caches at//! navigation time via [`install_cache_storage`] and saving them via//! [`snapshot_cache_storage`].//!//! Only `data:` URLs are decoded inline for `Cache.add` / `Cache.addAll`;//! HTTP fetches are deferred to a follow-up issue that wires this module to//! the network stack. Callers can populate caches today either by decoding a//! `data:` URL or by constructing a `Response` object themselves and calling//! `Cache.put`.
use std::cell::RefCell;
use crate::builtins::{ create_promise_object_pub, make_native, reject_promise_internal, resolve_promise_internal,};use crate::cache::{CacheQueryOptions, CacheRequest, CacheResponse, CacheStorage};use crate::gc::GcRef;use crate::vm::{HeapObject, NativeContext, ObjectData, Property, RuntimeError, Value, Vm};use we_url::data_url::parse_data_url;use we_url::{Origin, Url};
thread_local! { static STORAGE: RefCell<CacheStorage> = RefCell::new(CacheStorage::new()); static ORIGIN: RefCell<Option<Origin>> = const { RefCell::new(None) }; static BASE_URL: RefCell<Option<Url>> = const { RefCell::new(None) };}
// ── Embedder API ────────────────────────────────────────────────────────────
/// Replace the active cache storage with `storage`. Called when navigating/// to a new origin so the embedder can swap in that origin's persisted/// caches.pub fn install_cache_storage(storage: CacheStorage) { STORAGE.with(|s| *s.borrow_mut() = storage);}
/// Reset the thread-local Cache API state to an empty storage. Useful in/// tests to avoid bleed-through between cases.pub fn reset_cache_storage() { STORAGE.with(|s| *s.borrow_mut() = CacheStorage::new()); ORIGIN.with(|o| *o.borrow_mut() = None); BASE_URL.with(|b| *b.borrow_mut() = None);}
/// Set the origin associated with the current VM. Cache lookups will be/// scoped to this origin.pub fn set_cache_origin(origin: Origin) { ORIGIN.with(|o| *o.borrow_mut() = Some(origin));}
/// Set the base URL used to resolve relative request URLs passed to the/// Cache API. Should be set to the current document URL on the main thread,/// or the service worker script URL inside the worker.pub fn set_cache_base_url(url: Url) { BASE_URL.with(|b| *b.borrow_mut() = Some(url));}
/// Snapshot the current cache storage so the embedder can persist it.pub fn snapshot_cache_storage() -> CacheStorage { STORAGE.with(|s| s.borrow().clone())}
/// Run `f` with a mutable reference to the thread-local cache storage. Used/// by tests and embedders to inspect state directly.pub fn with_cache_storage_mut<R>(f: impl FnOnce(&mut CacheStorage) -> R) -> R { STORAGE.with(|s| f(&mut s.borrow_mut()))}
// ── JS init ─────────────────────────────────────────────────────────────────
/// Install `caches`, `Request`, and `Response` constructors on the given VM.////// `caches` and the `Cache` objects it hands out are implemented as a JS/// preamble (see [`CACHE_API_PREAMBLE`]) that wraps a small set of native/// helpers. We pass the cache name explicitly through every helper instead/// of going via `this`, because the compiler's method-call lowering shares/// the `this` global slot across nested argument evaluation — `cache.put(new/// Request(...), ...)` would clobber `this` while constructing the Request.pub fn init_cache_api(vm: &mut Vm) { register_native_helpers(vm); install_preamble(vm);}
type NativeHelper = fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>;
fn register_native_helpers(vm: &mut Vm) { let helpers: [(&str, NativeHelper); 12] = [ ("__we_caches_open", caches_open), ("__we_caches_has", caches_has), ("__we_caches_keys", caches_keys), ("__we_caches_delete", caches_delete), ("__we_caches_match", caches_match), ("__we_cache_match", cache_match), ("__we_cache_match_all", cache_match_all), ("__we_cache_put", cache_put), ("__we_cache_add", cache_add), ("__we_cache_add_all", cache_add_all), ("__we_cache_delete", cache_delete), ("__we_cache_keys", cache_keys), ]; for (name, callback) in helpers { let func = make_native(&mut vm.gc, name, callback); vm.set_global(name, Value::Function(func)); }}
const CACHE_API_PREAMBLE: &str = r#"function Request(input, init) { var r = Object.create(Request.prototype); init = init || {}; if (input !== null && typeof input === "object" && input.url !== undefined) { r.url = String(input.url); r.method = init.method !== undefined ? String(init.method) : input.method; r.headers = init.headers !== undefined ? init.headers : input.headers; r.body = init.body !== undefined ? init.body : input.body; } else { r.url = String(input); r.method = init.method !== undefined ? String(init.method) : "GET"; r.headers = init.headers || {}; r.body = init.body; } return r;}function Response(body, init) { var r = Object.create(Response.prototype); init = init || {}; r.status = init.status !== undefined ? Number(init.status) : 200; r.statusText = init.statusText !== undefined ? String(init.statusText) : ""; r.headers = init.headers || {}; r.body = body !== undefined ? body : null; r.url = init.url !== undefined ? String(init.url) : ""; r.type = init.type !== undefined ? String(init.type) : "default"; r.redirected = init.redirected === true; r.ok = r.status >= 200 && r.status < 300; return r;}
// Native helpers return array-shaped objects (numeric indices + length) that// are not real Arrays — they lack Array.prototype methods. Wrap them into a// proper Array so callers can use .join, .map, .forEach, etc.function __we_to_array(arrlike) { var out = []; var n = arrlike.length; for (var i = 0; i < n; i++) out.push(arrlike[i]); return out;}
function __we_make_cache(name) { return { match: function(req, opts) { return __we_cache_match(name, req, opts); }, matchAll: function(req, opts) { return __we_cache_match_all(name, req, opts).then(__we_to_array); }, put: function(req, res) { return __we_cache_put(name, req, res); }, add: function(req) { return __we_cache_add(name, req); }, addAll: function(reqs) { return __we_cache_add_all(name, reqs); }, delete: function(req, opts) { return __we_cache_delete(name, req, opts); }, keys: function(req, opts) { return __we_cache_keys(name, req, opts).then(__we_to_array); } };}
var caches = { open: function(name) { return __we_caches_open(String(name)).then(function() { return __we_make_cache(String(name)); }); }, has: function(name) { return __we_caches_has(String(name)); }, keys: function() { return __we_caches_keys().then(__we_to_array); }, delete: function(name) { return __we_caches_delete(String(name)); }, match: function(req, opts) { return __we_caches_match(req, opts); }};"#;
fn install_preamble(vm: &mut Vm) { let ast = match crate::parser::Parser::parse(CACHE_API_PREAMBLE) { Ok(ast) => ast, Err(e) => { eprintln!("cache API preamble parse error: {e}"); return; } }; let func = match crate::compiler::compile(&ast) { Ok(f) => f, Err(e) => { eprintln!("cache API preamble compile error: {e}"); return; } }; if let Err(e) = vm.execute(&func) { eprintln!("cache API preamble runtime error: {e}"); }}
// ── Promise helpers ─────────────────────────────────────────────────────────
fn promise_resolved(ctx: &mut NativeContext, value: Value) -> Value { let promise = create_promise_object_pub(ctx.gc, ctx.shapes); resolve_promise_internal(ctx.gc, ctx.shapes, promise, value); Value::Object(promise)}
fn promise_rejected(ctx: &mut NativeContext, message: String) -> Value { let promise = create_promise_object_pub(ctx.gc, ctx.shapes); reject_promise_internal(ctx.gc, ctx.shapes, promise, Value::String(message)); Value::Object(promise)}
// ── caches.* ────────────────────────────────────────────────────────────────
fn caches_open(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let name = match args.first() { Some(v) => v.to_js_string(ctx.gc), None => { return Ok(promise_rejected( ctx, "TypeError: caches.open requires a name".to_string(), )) } }; let origin = match current_origin() { Some(o) => o, None => { return Ok(promise_rejected( ctx, "InvalidStateError: no origin associated with this context".to_string(), )) } }; STORAGE.with(|s| { s.borrow_mut().open(&origin, &name); }); Ok(promise_resolved(ctx, Value::Undefined))}
fn caches_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let name = match args.first() { Some(v) => v.to_js_string(ctx.gc), None => return Ok(promise_resolved(ctx, Value::Boolean(false))), }; let origin = match current_origin() { Some(o) => o, None => return Ok(promise_resolved(ctx, Value::Boolean(false))), }; let exists = STORAGE.with(|s| s.borrow().has(&origin, &name)); Ok(promise_resolved(ctx, Value::Boolean(exists)))}
fn caches_keys(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { let empty = build_array(ctx, Vec::new()); return Ok(promise_resolved(ctx, empty)); } }; let names: Vec<String> = STORAGE.with(|s| s.borrow().keys(&origin)); let values: Vec<Value> = names.into_iter().map(Value::String).collect(); let array = build_array(ctx, values); Ok(promise_resolved(ctx, array))}
fn caches_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let name = match args.first() { Some(v) => v.to_js_string(ctx.gc), None => return Ok(promise_resolved(ctx, Value::Boolean(false))), }; let origin = match current_origin() { Some(o) => o, None => return Ok(promise_resolved(ctx, Value::Boolean(false))), }; let removed = STORAGE.with(|s| s.borrow_mut().delete(&origin, &name)); Ok(promise_resolved(ctx, Value::Boolean(removed)))}
fn caches_match(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => return Ok(promise_resolved(ctx, Value::Undefined)), }; let request = match build_request_from_arg(args.first(), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let options = parse_query_options(args.get(1), ctx); let cache_name = match args.get(1) { Some(Value::Object(obj)) => match ctx.gc.get(*obj) { Some(HeapObject::Object(data)) => { data.get_property("cacheName", ctx.shapes) .and_then(|p| match p.value { Value::String(s) => Some(s), Value::Undefined | Value::Null => None, other => Some(other.to_js_string(ctx.gc)), }) } _ => None, }, _ => None, }; let result = STORAGE.with(|s| { s.borrow() .match_request(&origin, cache_name.as_deref(), &request, &options) }); match result { Some(resp) => { let value = build_response_object(ctx, &resp); Ok(promise_resolved(ctx, value)) } None => Ok(promise_resolved(ctx, Value::Undefined)), }}
// ── Cache.* (cache name passed explicitly as first argument) ────────────────
fn take_cache_name(args: &[Value], ctx: &NativeContext) -> Option<String> { match args.first()? { Value::String(s) => Some(s.clone()), other => Some(other.to_js_string(ctx.gc)), }}
fn cache_match(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => return Ok(promise_resolved(ctx, Value::Undefined)), }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.match missing cache name".to_string(), )) } }; let request = match build_request_from_arg(args.get(1), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let options = parse_query_options(args.get(2), ctx); let result = STORAGE.with(|s| { s.borrow() .match_request(&origin, Some(&cache_name), &request, &options) }); match result { Some(resp) => { let value = build_response_object(ctx, &resp); Ok(promise_resolved(ctx, value)) } None => Ok(promise_resolved(ctx, Value::Undefined)), }}
fn cache_match_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { let empty = build_array(ctx, Vec::new()); return Ok(promise_resolved(ctx, empty)); } }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.matchAll missing cache name".to_string(), )) } }; let options = parse_query_options(args.get(2), ctx); let request_filter = match args.get(1) { Some(v) if !matches!(v, Value::Undefined | Value::Null) => { match build_request_from_arg(Some(v), ctx) { Ok(r) => Some(r), Err(e) => return Ok(promise_rejected(ctx, e)), } } _ => None, }; let responses: Vec<CacheResponse> = STORAGE.with(|s| { let storage = s.borrow(); let cache = match storage.get(&origin, &cache_name) { Some(c) => c, None => return Vec::new(), }; cache.match_all(request_filter.as_ref(), &options) }); let values: Vec<Value> = responses .into_iter() .map(|r| build_response_object(ctx, &r)) .collect(); let array = build_array(ctx, values); Ok(promise_resolved(ctx, array))}
fn cache_put(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { return Ok(promise_rejected( ctx, "InvalidStateError: no origin associated with this context".to_string(), )) } }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.put missing cache name".to_string(), )) } }; let request = match build_request_from_arg(args.get(1), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let response = match build_response_from_arg(args.get(2), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let put_result = STORAGE.with(|s| { let mut storage = s.borrow_mut(); storage.open(&origin, &cache_name).put(request, response) }); match put_result { Ok(()) => Ok(promise_resolved(ctx, Value::Undefined)), Err(e) => Ok(promise_rejected(ctx, e)), }}
fn cache_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { return Ok(promise_rejected( ctx, "InvalidStateError: no origin associated with this context".to_string(), )) } }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.add missing cache name".to_string(), )) } }; let request = match build_request_from_arg(args.get(1), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let response = match fetch_for_cache_add(&request) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let put_result = STORAGE.with(|s| { let mut storage = s.borrow_mut(); storage.open(&origin, &cache_name).put(request, response) }); match put_result { Ok(()) => Ok(promise_resolved(ctx, Value::Undefined)), Err(e) => Ok(promise_rejected(ctx, e)), }}
fn cache_add_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { return Ok(promise_rejected( ctx, "InvalidStateError: no origin associated with this context".to_string(), )) } }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.addAll missing cache name".to_string(), )) } }; let array_ref = match args.get(1) { Some(Value::Object(r)) => *r, _ => { return Ok(promise_rejected( ctx, "TypeError: Cache.addAll requires an array of requests".to_string(), )) } }; let items = read_array_values(ctx, array_ref); let mut staged: Vec<(CacheRequest, CacheResponse)> = Vec::with_capacity(items.len()); for item in items { let request = match build_request_from_arg(Some(&item), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let response = match fetch_for_cache_add(&request) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; staged.push((request, response)); } let result = STORAGE.with(|s| { let mut storage = s.borrow_mut(); let cache = storage.open(&origin, &cache_name); for (req, resp) in staged { cache.put(req, resp)?; } Ok(()) }); match result { Ok(()) => Ok(promise_resolved(ctx, Value::Undefined)), Err(e) => Ok(promise_rejected(ctx, e)), }}
fn cache_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => return Ok(promise_resolved(ctx, Value::Boolean(false))), }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.delete missing cache name".to_string(), )) } }; let request = match build_request_from_arg(args.get(1), ctx) { Ok(r) => r, Err(e) => return Ok(promise_rejected(ctx, e)), }; let options = parse_query_options(args.get(2), ctx); let removed = STORAGE.with(|s| { let mut storage = s.borrow_mut(); match storage.get_mut(&origin, &cache_name) { Some(cache) => cache.delete(&request, &options), None => false, } }); Ok(promise_resolved(ctx, Value::Boolean(removed)))}
fn cache_keys(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { let origin = match current_origin() { Some(o) => o, None => { let empty = build_array(ctx, Vec::new()); return Ok(promise_resolved(ctx, empty)); } }; let cache_name = match take_cache_name(args, ctx) { Some(n) => n, None => { return Ok(promise_rejected( ctx, "TypeError: Cache.keys missing cache name".to_string(), )) } }; let options = parse_query_options(args.get(2), ctx); let filter = match args.get(1) { Some(v) if !matches!(v, Value::Undefined | Value::Null) => { match build_request_from_arg(Some(v), ctx) { Ok(r) => Some(r), Err(e) => return Ok(promise_rejected(ctx, e)), } } _ => None, }; let requests: Vec<CacheRequest> = STORAGE.with(|s| { let storage = s.borrow(); match storage.get(&origin, &cache_name) { Some(cache) => cache.keys(filter.as_ref(), &options), None => Vec::new(), } }); let values: Vec<Value> = requests .into_iter() .map(|r| build_request_object(ctx, &r)) .collect(); let array = build_array(ctx, values); Ok(promise_resolved(ctx, array))}
// ── Argument coercion ───────────────────────────────────────────────────────
fn build_request_from_arg( value: Option<&Value>, ctx: &NativeContext,) -> Result<CacheRequest, String> { let value = match value { Some(v) => v, None => return Err("TypeError: request is required".to_string()), }; match value { Value::String(s) => parse_request_url(s), Value::Object(obj_ref) => extract_request_object(*obj_ref, ctx), other => parse_request_url(&other.to_js_string(ctx.gc)), }}
fn parse_request_url(url_str: &str) -> Result<CacheRequest, String> { let resolved = match BASE_URL.with(|b| b.borrow().clone()) { Some(base) => Url::parse_with_base(url_str, &base) .or_else(|_| Url::parse(url_str)) .map_err(|e| format!("TypeError: {e}"))?, None => Url::parse(url_str).map_err(|e| format!("TypeError: {e}"))?, }; Ok(CacheRequest::get(&resolved))}
fn extract_request_object(obj_ref: GcRef, ctx: &NativeContext) -> Result<CacheRequest, String> { let data = match ctx.gc.get(obj_ref) { Some(HeapObject::Object(d)) => d, _ => return Err("TypeError: invalid request object".to_string()), }; let url = data .get_property("url", ctx.shapes) .map(|p| p.value.to_js_string(ctx.gc)) .ok_or_else(|| "TypeError: request object is missing 'url'".to_string())?; let method = data .get_property("method", ctx.shapes) .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_else(|| "GET".to_string()); let mut headers: Vec<(String, String)> = Vec::new(); if let Some(prop) = data.get_property("headers", ctx.shapes) { if let Value::Object(h_ref) = prop.value { if let Some(HeapObject::Object(h_data)) = ctx.gc.get(h_ref) { for (key, hprop) in h_data.property_entries(ctx.shapes) { if hprop.enumerable { headers.push((key, hprop.value.to_js_string(ctx.gc))); } } } } } let resolved = match BASE_URL.with(|b| b.borrow().clone()) { Some(base) => Url::parse_with_base(&url, &base) .or_else(|_| Url::parse(&url)) .map_err(|e| format!("TypeError: {e}"))?, None => Url::parse(&url).map_err(|e| format!("TypeError: {e}"))?, }; Ok(CacheRequest { url: resolved.serialize(), method: method.to_ascii_uppercase(), headers, })}
fn build_response_from_arg( value: Option<&Value>, ctx: &NativeContext,) -> Result<CacheResponse, String> { let value = match value { Some(v) => v, None => return Err("TypeError: response is required".to_string()), }; let obj_ref = match value { Value::Object(r) => *r, _ => return Err("TypeError: response must be a Response object".to_string()), }; let data = match ctx.gc.get(obj_ref) { Some(HeapObject::Object(d)) => d, _ => return Err("TypeError: response must be a Response object".to_string()), }; let status = data .get_property("status", ctx.shapes) .map(|p| match p.value { Value::Number(n) => n as u16, _ => 200, }) .unwrap_or(200); let status_text = data .get_property("statusText", ctx.shapes) .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_default(); let response_type = data .get_property("type", ctx.shapes) .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_else(|| "default".to_string()); let response_url = data .get_property("url", ctx.shapes) .map(|p| p.value.to_js_string(ctx.gc)) .unwrap_or_default(); let redirected = data .get_property("redirected", ctx.shapes) .map(|p| matches!(p.value, Value::Boolean(true))) .unwrap_or(false); let mut headers: Vec<(String, String)> = Vec::new(); if let Some(prop) = data.get_property("headers", ctx.shapes) { if let Value::Object(h_ref) = prop.value { if let Some(HeapObject::Object(h_data)) = ctx.gc.get(h_ref) { for (key, hprop) in h_data.property_entries(ctx.shapes) { if hprop.enumerable { headers.push((key, hprop.value.to_js_string(ctx.gc))); } } } } } let body = match data.get_property("body", ctx.shapes).map(|p| p.value) { Some(Value::String(s)) => s.into_bytes(), Some(Value::Null) | Some(Value::Undefined) | None => Vec::new(), Some(other) => other.to_js_string(ctx.gc).into_bytes(), }; Ok(CacheResponse { status, status_text, headers, body, response_type, url: response_url, redirected, })}
fn parse_query_options(value: Option<&Value>, ctx: &NativeContext) -> CacheQueryOptions { let mut opts = CacheQueryOptions::default(); let obj_ref = match value { Some(Value::Object(r)) => *r, _ => return opts, }; if let Some(HeapObject::Object(data)) = ctx.gc.get(obj_ref) { if let Some(prop) = data.get_property("ignoreSearch", ctx.shapes) { opts.ignore_search = is_truthy(&prop.value); } if let Some(prop) = data.get_property("ignoreMethod", ctx.shapes) { opts.ignore_method = is_truthy(&prop.value); } if let Some(prop) = data.get_property("ignoreVary", ctx.shapes) { opts.ignore_vary = is_truthy(&prop.value); } } opts}
fn is_truthy(v: &Value) -> bool { match v { Value::Boolean(b) => *b, Value::Number(n) => *n != 0.0 && !n.is_nan(), Value::String(s) => !s.is_empty(), Value::Undefined | Value::Null => false, Value::Object(_) | Value::Function(_) => true, }}
// ── Cache.add helper: only data: URLs supported for now ─────────────────────
fn fetch_for_cache_add(request: &CacheRequest) -> Result<CacheResponse, String> { if request.method != "GET" { return Err(format!( "TypeError: only GET requests can be added to a cache (got {})", request.method )); } let url = Url::parse(&request.url).map_err(|e| format!("TypeError: {e}"))?; match url.scheme() { "data" => { let parsed = parse_data_url(&request.url) .map_err(|e| format!("TypeError: data URL: {e}"))?; let content_type = if parsed.mime_type.is_empty() { "text/plain".to_string() } else { parsed.mime_type.clone() }; Ok(CacheResponse { status: 200, status_text: "OK".to_string(), headers: vec![("Content-Type".to_string(), content_type)], body: parsed.data, response_type: "basic".to_string(), url: request.url.clone(), redirected: false, }) } other => Err(format!( "NetworkError: Cache.add for {other}: URLs requires a fetch implementation in this context" )), }}
// ── JS object construction ──────────────────────────────────────────────────
fn build_request_object(ctx: &mut NativeContext, request: &CacheRequest) -> Value { let mut data = ObjectData::new(); data.insert_property( "url".to_string(), Property::builtin(Value::String(request.url.clone())), ctx.shapes, ); data.insert_property( "method".to_string(), Property::builtin(Value::String(request.method.clone())), ctx.shapes, ); let mut headers = ObjectData::new(); for (name, value) in &request.headers { headers.insert_property( name.clone(), Property::builtin(Value::String(value.clone())), ctx.shapes, ); } let headers_ref = ctx.gc.alloc(HeapObject::Object(headers)); data.insert_property( "headers".to_string(), Property::builtin(Value::Object(headers_ref)), ctx.shapes, ); Value::Object(ctx.gc.alloc(HeapObject::Object(data)))}
fn build_response_object(ctx: &mut NativeContext, response: &CacheResponse) -> Value { let mut data = ObjectData::new(); data.insert_property( "status".to_string(), Property::builtin(Value::Number(response.status as f64)), ctx.shapes, ); data.insert_property( "statusText".to_string(), Property::builtin(Value::String(response.status_text.clone())), ctx.shapes, ); data.insert_property( "ok".to_string(), Property::builtin(Value::Boolean( response.status >= 200 && response.status < 300, )), ctx.shapes, ); data.insert_property( "url".to_string(), Property::builtin(Value::String(response.url.clone())), ctx.shapes, ); data.insert_property( "type".to_string(), Property::builtin(Value::String(response.response_type.clone())), ctx.shapes, ); data.insert_property( "redirected".to_string(), Property::builtin(Value::Boolean(response.redirected)), ctx.shapes, ); let body_string = String::from_utf8_lossy(&response.body).into_owned(); data.insert_property( "body".to_string(), Property::builtin(Value::String(body_string)), ctx.shapes, ); let mut headers = ObjectData::new(); for (name, value) in &response.headers { headers.insert_property( name.clone(), Property::builtin(Value::String(value.clone())), ctx.shapes, ); } let headers_ref = ctx.gc.alloc(HeapObject::Object(headers)); data.insert_property( "headers".to_string(), Property::builtin(Value::Object(headers_ref)), ctx.shapes, ); Value::Object(ctx.gc.alloc(HeapObject::Object(data)))}
fn build_array(ctx: &mut NativeContext, items: Vec<Value>) -> Value { let mut data = ObjectData::new(); for (idx, val) in items.iter().enumerate() { data.insert_property(idx.to_string(), Property::data(val.clone()), ctx.shapes); } data.insert_property( "length".to_string(), Property::builtin(Value::Number(items.len() as f64)), ctx.shapes, ); Value::Object(ctx.gc.alloc(HeapObject::Object(data)))}
fn read_array_values(ctx: &NativeContext, array_ref: GcRef) -> Vec<Value> { let data = match ctx.gc.get(array_ref) { Some(HeapObject::Object(d)) => d, _ => return Vec::new(), }; let length = data .get_property("length", ctx.shapes) .map(|p| match p.value { Value::Number(n) => n as usize, _ => 0, }) .unwrap_or(0); let mut out = Vec::with_capacity(length); for i in 0..length { if let Some(prop) = data.get_property(&i.to_string(), ctx.shapes) { out.push(prop.value); } } out}
fn current_origin() -> Option<Origin> { ORIGIN.with(|o| o.borrow().clone())}
// ── Tests ──────────────────────────────────────────────────────────────────
#[cfg(test)]mod tests { use super::*; use crate::{compiler, parser};
fn setup_vm() -> Vm { reset_cache_storage(); let origin = Url::parse("https://example.com/").unwrap().origin(); set_cache_origin(origin); set_cache_base_url(Url::parse("https://example.com/app/").unwrap()); let mut vm = Vm::new(); init_cache_api(&mut vm); vm }
fn run(vm: &mut Vm, source: &str) { let ast = parser::Parser::parse(source).expect("parse"); let func = compiler::compile(&ast).expect("compile"); vm.execute(&func).expect("execute"); vm.pump_event_loop().expect("event loop"); }
fn result_string(vm: &Vm) -> String { vm.get_global("result") .cloned() .unwrap_or(Value::Undefined) .to_js_string(&vm.gc) }
#[test] fn caches_open_creates_a_cache() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function(cache) { result = typeof cache.put + ":" + typeof cache.match; }); "#, ); assert_eq!(result_string(&vm), "function:function"); }
#[test] fn caches_has_keys_delete_round_trip() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function() { return caches.open("v2"); }).then(function() { return caches.keys(); }).then(function(names) { return caches.has("v1").then(function(h1) { return caches.has("v3").then(function(h3) { result = names[0] + "," + names[1] + ";" + h1 + "," + h3; }); }); }); "#, ); assert_eq!(result_string(&vm), "v1,v2;true,false");
run( &mut vm, r#" caches.delete("v1").then(function(was) { return caches.keys().then(function(names) { result = was + ";" + names.length + ";" + names[0]; }); }); "#, ); assert_eq!(result_string(&vm), "true;1;v2"); }
#[test] fn cache_put_and_match_round_trip_via_js() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function(cache) { cache.put( new Request("https://example.com/data"), new Response("hello, cache", { status: 200, statusText: "OK" }) ).then(function() { cache.match("https://example.com/data").then(function(response) { result = response.status + ":" + response.body; }); }); }); "#, ); assert_eq!(result_string(&vm), "200:hello, cache"); }
#[test] fn cache_put_rejects_non_get_methods_from_js() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function(cache) { var req = new Request("https://example.com/data", { method: "POST" }); var res = new Response("body"); return cache.put(req, res).then( function() { result = "resolved"; }, function(err) { result = String(err); } ); }); "#, ); let s = result_string(&vm); assert!(s.contains("POST"), "got: {s}"); }
#[test] fn cache_add_supports_data_urls_install_pattern() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("install").then(function(cache) { return cache.addAll([ "data:text/plain,hello", "data:text/plain,world" ]).then(function() { return cache.keys(); }); }).then(function(requests) { result = requests.length + ":" + requests[0].url + ":" + requests[1].url; }); "#, ); assert_eq!( result_string(&vm), "2:data:text/plain,hello:data:text/plain,world" ); }
#[test] fn cache_match_supports_ignore_search() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function(cache) { return cache.put( new Request("https://example.com/p?v=1"), new Response("payload") ).then(function() { return cache.match("https://example.com/p?v=2", { ignoreSearch: true }); }); }).then(function(r) { result = r ? r.body : "miss"; }); "#, ); assert_eq!(result_string(&vm), "payload"); }
#[test] fn cache_keys_returns_stored_requests() { let mut vm = setup_vm(); run( &mut vm, r#" result = ""; caches.open("v1").then(function(cache) { return cache.put(new Request("https://example.com/a"), new Response("1")) .then(function() { return cache.put(new Request("https://example.com/b"), new Response("2")); }) .then(function() { return cache.keys(); }); }).then(function(keys) { result = keys.length + ":" + keys[0].url + "," + keys[1].url; }); "#, ); assert_eq!( result_string(&vm), "2:https://example.com/a,https://example.com/b" ); }}