From 6ea429ec19d96889151ecc335f187ba44ff54cb0 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 16 Sep 2026 00:15:05 +0800 Subject: [PATCH] fix(worker): separate source imports from graph evaluation Traverse only evaluation dependencies reachable from each loading job. Resolve source imports once their root is compiled, and resume joined evaluation imports through linking instead of reading an uninstantiated namespace. Cover skipped dependency fetches, cached source reuse, concurrent phases, failure isolation, and shared root fetch failures. --- moli-renderer-v8/src/worker/module_runtime.rs | 173 +++++---- .../src/worker/thread/tests/modules.rs | 26 +- .../thread/tests/modules/source_phase.rs | 349 ++++++++++++++++++ 3 files changed, 473 insertions(+), 75 deletions(-) create mode 100644 moli-renderer-v8/src/worker/thread/tests/modules/source_phase.rs diff --git a/moli-renderer-v8/src/worker/module_runtime.rs b/moli-renderer-v8/src/worker/module_runtime.rs index 750baa2b0..e6e78239b 100644 --- a/moli-renderer-v8/src/worker/module_runtime.rs +++ b/moli-renderer-v8/src/worker/module_runtime.rs @@ -959,6 +959,7 @@ impl WorkerDynamicModuleResolver { fn take_joined_root_imports( &mut self, key: &WorkerModuleKey, + phase: Option, ) -> Vec { let mut joined = Vec::new(); let mut remaining = VecDeque::with_capacity(self.pending_imports.len()); @@ -967,7 +968,8 @@ impl WorkerDynamicModuleResolver { &job.state, WorkerDynamicModuleImportJobState::JoinedRoot { key: joined_key } if joined_key == key - ) { + ) && phase.is_none_or(|phase| job.phase == phase) + { joined.push(job); } else { remaining.push_back(job); @@ -1060,12 +1062,25 @@ fn advance_worker_dynamic_module_import( })?; let module_key = worker_module_key_for_attributes(&module_url, &job.attributes) .map_err(|message| format!("{message} for dynamic import `{}`", job.specifier))?; - job.root_key = Some(module_key.clone()); + if job.phase == ModuleImportPhase::Source && module_key.kind != WorkerModuleKind::WebAssembly { + return Err(WorkerDynamicModuleImportError::syntax_error(format!( + "source-phase dynamic import `{}` does not resolve to a WebAssembly module", + job.specifier + ))); + } if let Some(error) = graph.borrow().source_fetch_failure(&module_key) { return Err(WorkerDynamicModuleImportError::type_error(format!( "Failed to dynamically import module worker dependency `{module_url}`: {error}" ))); } + let existing_root_entry = graph.borrow().entry_for_key(&module_key); + if job.phase == ModuleImportPhase::Source && existing_root_entry.is_some() { + // A compiled source is available even while another job is loading or + // evaluating its dependencies. This job does not own that root's waiters. + job.resolved_entry = existing_root_entry; + return Ok(WorkerDynamicModuleImportAdvance::Complete); + } + job.root_key = Some(module_key.clone()); if context .get_slot::>() .is_some_and(|dynamic_imports| dynamic_imports.borrow().root_import_in_flight(&module_key)) @@ -1073,14 +1088,7 @@ fn advance_worker_dynamic_module_import( job.state = WorkerDynamicModuleImportJobState::JoinedRoot { key: module_key }; return Ok(WorkerDynamicModuleImportAdvance::WaitingFetches); } - if job.phase == ModuleImportPhase::Source && module_key.kind != WorkerModuleKind::WebAssembly { - return Err(WorkerDynamicModuleImportError::syntax_error(format!( - "source-phase dynamic import `{}` does not resolve to a WebAssembly module", - job.specifier - ))); - } let inherited_referrer_policy = graph.borrow().referrer_policy_for_url(&job.base_url); - let existing_root_entry = graph.borrow().entry_for_key(&module_key); let root_entry = match existing_root_entry { Some(entry) => entry, None => match load_worker_static_module_dependency(&job.base_url, &job.specifier)? { @@ -1119,18 +1127,19 @@ fn advance_worker_dynamic_module_import( }, }; job.resolved_entry = Some(root_entry); + if job.phase == ModuleImportPhase::Source { + return Ok(WorkerDynamicModuleImportAdvance::Complete); + } match continue_worker_module_graph( scope, &graph, + root_entry, &job.fetch_initiator_url, WorkerModuleGraphFetchCspSource::DynamicImportGraph, job.browser_request_metadata(), ) .map_err(|error| error.0.summary)? { - WorkerModuleGraphBuild::Ready if job.phase == ModuleImportPhase::Source => { - Ok(WorkerDynamicModuleImportAdvance::Complete) - } WorkerModuleGraphBuild::Ready => { finish_worker_dynamic_module_import_evaluation(scope, root_entry) } @@ -1190,6 +1199,18 @@ fn job_finish_fetch( ); } else { job.resolved_entry = Some(target_entry); + // Source imports only share the root fetch, not dependency loading or + // evaluation. Wake them before either of those steps can wait or fail. + if let Some(imports) = context.get_slot::>() { + let mut imports = imports.borrow_mut(); + let sources = + imports.take_joined_root_imports(&request.key, Some(ModuleImportPhase::Source)); + for mut source in sources { + source.root_key = None; + source.state = WorkerDynamicModuleImportJobState::Graph; + imports.pending_imports.push_back(source); + } + } } Ok(()) } @@ -1210,9 +1231,13 @@ fn job_resume_fetch_with_pending_keys( let root_entry = job .resolved_entry .ok_or_else(|| "dynamic import root module is not compiled".to_owned())?; + if job.phase == ModuleImportPhase::Source { + return Ok(WorkerDynamicModuleImportAdvance::Complete); + } match continue_worker_module_graph_with_pending_keys( scope, &graph, + root_entry, &request.initiator_url, request.csp_source(), request.graph_browser_request_metadata(), @@ -1220,12 +1245,6 @@ fn job_resume_fetch_with_pending_keys( ) .map_err(|error| error.0.summary)? { - WorkerModuleGraphBuild::Ready if job.phase == ModuleImportPhase::Source => { - if has_pending_requests { - return Ok(WorkerDynamicModuleImportAdvance::WaitingFetches); - } - Ok(WorkerDynamicModuleImportAdvance::Complete) - } WorkerModuleGraphBuild::Ready => { if has_pending_requests { return Ok(WorkerDynamicModuleImportAdvance::WaitingFetches); @@ -1329,10 +1348,23 @@ fn resolve_worker_dynamic_module_import( job: WorkerDynamicModuleImportJob, ) { let resolved_entry = job.resolved_entry; + let phase = job.phase; let joined_imports = take_worker_dynamic_module_imports_joined_to_root(scope, &job); resolve_single_worker_dynamic_module_import(scope, job); for mut joined_job in joined_imports { joined_job.resolved_entry = resolved_entry; + if phase == ModuleImportPhase::Source && joined_job.phase == ModuleImportPhase::Evaluation { + // Sharing a source fetch does not link or evaluate the module. Resume + // evaluation jobs from the cached root instead of reading its namespace. + let context = v8::Local::new(scope, &joined_job.context); + let imports = context + .get_slot::>() + .expect("dynamic import resolver should be installed"); + joined_job.root_key = None; + joined_job.state = WorkerDynamicModuleImportJobState::Graph; + imports.borrow_mut().pending_imports.push_back(joined_job); + continue; + } resolve_single_worker_dynamic_module_import(scope, joined_job); } } @@ -1413,7 +1445,7 @@ fn take_worker_dynamic_module_imports_joined_to_root( }; dynamic_imports .borrow_mut() - .take_joined_root_imports(root_key) + .take_joined_root_imports(root_key, None) } fn reject_worker_dynamic_module_import( @@ -1506,6 +1538,7 @@ impl WorkerModuleBootstrapJob { match continue_worker_module_graph( scope, &self.runtime.graph, + root_entry, &self.static_import_initiator_url, WorkerModuleGraphFetchCspSource::StaticModuleGraph, BrowserRequestMetadata::Fetch, @@ -1582,6 +1615,7 @@ impl WorkerModuleBootstrapJob { match continue_worker_module_graph_with_pending_keys( scope, &self.runtime.graph, + root_entry, &self.static_import_initiator_url, WorkerModuleGraphFetchCspSource::StaticModuleGraph, BrowserRequestMetadata::Fetch, @@ -1970,23 +2004,23 @@ impl WorkerModuleGraph { }); } - fn len(&self) -> usize { - self.records.len() - } - fn url(&self, entry: usize) -> &Url { &self.records[entry].source_url } - fn has_dependency( + fn dependency_entry( &self, entry: usize, specifier: &str, attributes: &ModuleAttributesKey, - ) -> bool { - self.records[entry].dependencies.iter().any(|dependency| { - dependency.specifier == specifier && dependency.attributes == *attributes - }) + ) -> Option { + self.records[entry] + .dependencies + .iter() + .find(|dependency| { + dependency.specifier == specifier && dependency.attributes == *attributes + }) + .map(|dependency| dependency.target_entry) } fn module_url_for(&self, module: v8::Local<'_, v8::Module>) -> Option { @@ -2046,13 +2080,7 @@ impl WorkerModuleGraph { attributes: &ModuleAttributesKey, ) -> Option { let referrer_entry = self.entry_for_module(referrer)?; - let dependency = self.records[referrer_entry] - .dependencies - .iter() - .find(|dependency| { - dependency.specifier == specifier && dependency.attributes == *attributes - })?; - Some(dependency.target_entry) + self.dependency_entry(referrer_entry, specifier, attributes) } fn resolve_static_dependency_record( @@ -2125,6 +2153,7 @@ fn ensure_worker_module_entry( fn continue_worker_module_graph( scope: &mut v8::PinScope<'_, '_>, graph: &Rc>, + root_entry: usize, fetch_initiator_url: &Url, csp_source: WorkerModuleGraphFetchCspSource, browser_request_metadata: BrowserRequestMetadata, @@ -2132,6 +2161,7 @@ fn continue_worker_module_graph( continue_worker_module_graph_with_pending_keys( scope, graph, + root_entry, fetch_initiator_url, csp_source, browser_request_metadata, @@ -2142,41 +2172,55 @@ fn continue_worker_module_graph( fn continue_worker_module_graph_with_pending_keys( scope: &mut v8::PinScope<'_, '_>, graph: &Rc>, + root_entry: usize, fetch_initiator_url: &Url, csp_source: WorkerModuleGraphFetchCspSource, browser_request_metadata: BrowserRequestMetadata, mut pending_keys: HashSet, ) -> WorkerModuleBootstrapResult { - let mut entry = 0; + // The module map also contains source-only and unrelated dynamic imports. + // Expand only evaluation edges reachable from this job's root. + let mut pending_entries = vec![root_entry]; + let mut visited = HashSet::new(); let mut pending_requests = Vec::new(); - while entry < graph.borrow().len() { + while let Some(entry) = pending_entries.pop() { + if !visited.insert(entry) { + continue; + } let url = graph.borrow().url(entry).clone(); let requests = graph.borrow().requests(entry); for request in requests { if graph .borrow() - .has_dependency(entry, &request.specifier, &request.attributes) + .dependency_entry(entry, &request.specifier, &request.attributes) + .is_none() { - continue; - } - match resolve_worker_module_dependency( - scope, - graph, - entry, - &url, - fetch_initiator_url, - csp_source, - browser_request_metadata, - request, - &mut pending_keys, - )? { - WorkerModuleGraphBuild::Ready => {} - WorkerModuleGraphBuild::NeedFetches(requests) => { - pending_requests.extend(requests.requests); + match resolve_worker_module_dependency( + scope, + graph, + entry, + &url, + fetch_initiator_url, + csp_source, + browser_request_metadata, + &request, + &mut pending_keys, + )? { + WorkerModuleGraphBuild::Ready => {} + WorkerModuleGraphBuild::NeedFetches(requests) => { + pending_requests.extend(requests.requests); + } } } + if request.phase == ModuleImportPhase::Evaluation + && let Some(dependency) = + graph + .borrow() + .dependency_entry(entry, &request.specifier, &request.attributes) + { + pending_entries.push(dependency); + } } - entry += 1; } if !pending_requests.is_empty() { return Ok(WorkerModuleGraphBuild::NeedFetches( @@ -2194,7 +2238,7 @@ fn resolve_worker_module_dependency( fetch_initiator_url: &Url, csp_source: WorkerModuleGraphFetchCspSource, browser_request_metadata: BrowserRequestMetadata, - request: WorkerModuleRequest, + request: &WorkerModuleRequest, pending_keys: &mut HashSet, ) -> WorkerModuleBootstrapResult { if request.phase == ModuleImportPhase::Source { @@ -2269,8 +2313,8 @@ fn resolve_worker_module_dependency( if let Some(target_entry) = existing_entry { graph.borrow_mut().add_dependency( entry, - request.specifier, - request.attributes, + request.specifier.clone(), + request.attributes.clone(), target_entry, ); return Ok(WorkerModuleGraphBuild::Ready); @@ -2287,8 +2331,8 @@ fn resolve_worker_module_dependency( fetch_initiator_url.clone(), csp_source, Some(entry), - request.specifier, - request.attributes, + request.specifier.clone(), + request.attributes.clone(), graph.borrow().credentials_mode(), referrer_policy, browser_request_metadata, @@ -2305,9 +2349,12 @@ fn resolve_worker_module_dependency( dependency_key.url.clone(), referrer_policy, )?; - graph - .borrow_mut() - .add_dependency(entry, request.specifier, request.attributes, target_entry); + graph.borrow_mut().add_dependency( + entry, + request.specifier.clone(), + request.attributes.clone(), + target_entry, + ); Ok(WorkerModuleGraphBuild::Ready) } diff --git a/moli-renderer-v8/src/worker/thread/tests/modules.rs b/moli-renderer-v8/src/worker/thread/tests/modules.rs index 6d363bcda..eaf585e99 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules.rs @@ -3,6 +3,8 @@ use crate::worker::handle::WorkerParentErrorEventKind; use crate::worker::{WorkerErrorPhase, WorkerScriptResourceKind}; use moli_crypto::sha256_hex; +mod source_phase; + const WORKER_WASM_IMPORT_PM: &[u8] = &[ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00, 0x02, 0x19, 0x01, 0x12, 0x2e, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2d, 0x68, @@ -2858,24 +2860,24 @@ async fn service_worker_module_rejects_async_dependencies_before_execution() { async fn service_worker_module_checks_only_wasm_evaluation_dependencies() { ensure_v8(); for source_only in [false, true] { - let (base_url, server) = spawn_path_response_http_server(vec![ - ( - "/worker/worker.wasm", - "HTTP/1.1 200 OK", - "application/wasm", - worker_wasm_import_pm_body(), - Duration::ZERO, - ), - ( + let mut responses = vec![( + "/worker/worker.wasm", + "HTTP/1.1 200 OK", + "application/wasm", + worker_wasm_import_pm_body(), + Duration::ZERO, + )]; + if !source_only { + responses.push(( "/worker/worker-helper.js", "HTTP/1.1 200 OK", "text/javascript", "console.log('unexpected helper execution'); export function pm() {} await 0;" .into(), Duration::ZERO, - ), - ]) - .await; + )); + } + let (base_url, server) = spawn_path_response_http_server(responses).await; let source = if source_only { "import source wasm from './worker.wasm'; console.log(wasm instanceof WebAssembly.Module);" } else { diff --git a/moli-renderer-v8/src/worker/thread/tests/modules/source_phase.rs b/moli-renderer-v8/src/worker/thread/tests/modules/source_phase.rs new file mode 100644 index 000000000..2d472b5cf --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/modules/source_phase.rs @@ -0,0 +1,349 @@ +use super::*; +use tokio::net::TcpStream; + +// Keep responses under the test's control: a source import must settle even +// while the evaluation graph's dependency response is withheld. +struct ModuleSourceServer { + url: String, + requests: tokio::sync::mpsc::UnboundedReceiver<(String, TcpStream)>, + task: JoinHandle<()>, +} + +impl ModuleSourceServer { + async fn start() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}", listener.local_addr().unwrap()); + let (sender, requests) = tokio::sync::mpsc::unbounded_channel(); + let task = tokio::spawn(async move { + loop { + let (mut stream, _) = listener.accept().await.unwrap(); + let head = read_http_request_head(&mut stream).await.unwrap(); + let path = head + .lines() + .next() + .unwrap() + .split_whitespace() + .nth(1) + .unwrap(); + if sender.send((path.to_owned(), stream)).is_err() { + break; + } + } + }); + Self { + url, + requests, + task, + } + } + + fn worker(&self, source: String, kind: WorkerScriptKind) -> WorkerTestHandle { + spawn_worker_with_request_client_and_kind( + source, + format!("{}/worker/main.js", self.url), + worker_test_request_client(), + kind, + ) + } + + async fn request(&mut self, path: &str) -> TcpStream { + let (actual, stream) = timeout(TIMEOUT, self.requests.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(actual, path); + stream + } + + async fn respond(&mut self, path: &str, status: &str, mime: &str, body: &str) { + Self::write(self.request(path).await, status, mime, body).await; + } + + async fn write(mut stream: TcpStream, status: &str, mime: &str, body: &str) { + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).await.unwrap(); + } + + async fn wasm(&mut self) { + self.respond( + "/worker/worker.wasm", + "200 OK", + "application/wasm", + &worker_wasm_import_pm_body(), + ) + .await; + } + + fn assert_no_more_requests(&mut self) { + assert!(self.requests.try_recv().is_err(), "unexpected module fetch"); + } +} + +impl Drop for ModuleSourceServer { + fn drop(&mut self) { + self.task.abort(); + } +} + +#[tokio::test] +async fn worker_source_imports_do_not_load_wasm_dependencies() { + ensure_v8(); + for (kind, static_import) in [ + (WorkerScriptKind::Module, true), + (WorkerScriptKind::Module, false), + (WorkerScriptKind::Classic, false), + ] { + let mut server = ModuleSourceServer::start().await; + let (prefix, source) = if static_import { + ("import source wasm from './worker.wasm';", "") + } else { + ("", "const wasm = await import.source('./worker.wasm');") + }; + let mut handle = server.worker(format!(r#" + {prefix} + (async () => {{ + {source} + let value; + new WebAssembly.Instance(wasm, {{ "./worker-helper.js": {{ pm: v => value = v }} }}); + const again = await import.source('./worker.wasm'); + postMessage([wasm instanceof WebAssembly.Module, value, wasm === again]); + }})().catch(e => postMessage({{error: e.name}})); + "#), kind); + server.wasm().await; + assert_eq!(recv_post_json(&mut handle).await, "[true,42,true]"); + handle.terminate_and_join(); + server.assert_no_more_requests(); + } +} + +#[tokio::test] +async fn worker_source_import_allows_later_evaluation_of_cached_wasm() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker(r#" + (async () => { + const source = await import.source('./worker.wasm'); + const first = await import('./worker.wasm'); + const second = await import('./worker.wasm'); + const again = await import.source('./worker.wasm'); + postMessage([source === again, first === second, self.evaluationValue, self.evaluations]); + })().catch(e => postMessage({error: e.name})); + "#.into(), WorkerScriptKind::Module); + server.wasm().await; + server.respond("/worker/worker-helper.js", "200 OK", "text/javascript", + "self.evaluations = (self.evaluations || 0) + 1; export function pm(v) { self.evaluationValue = v; }").await; + assert_eq!(recv_post_json(&mut handle).await, "[true,true,42,1]"); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_concurrent_source_imports_do_not_wait_for_evaluation_dependencies() { + ensure_v8(); + for order in ["source-first", "evaluation-first", "late-source"] { + let mut server = ModuleSourceServer::start().await; + let start = match order { + "source-first" => "const source = loadSource(); const evaluation = loadEvaluation();", + "evaluation-first" => { + "const evaluation = loadEvaluation(); const source = loadSource();" + } + _ => { + "const evaluation = loadEvaluation(); await new Promise(r => onmessage = r); const source = loadSource();" + } + }; + let mut handle = server.worker(format!(r#" + (async () => {{ + const loadSource = () => Promise.all([ + import.source('./worker.wasm'), import.source('./worker.wasm') + ]).then(([a, b]) => {{ + postMessage(['source', a === b, a instanceof WebAssembly.Module]); + return a; + }}); + const loadEvaluation = () => Promise.all([ + import('./worker.wasm'), import('./worker.wasm') + ]).then(([a, b]) => a === b); + {start} + const [wasm, sameNamespace] = await Promise.all([source, evaluation]); + postMessage([sameNamespace, self.evaluationValue, wasm === await import.source('./worker.wasm')]); + }})().catch(e => postMessage({{error: e.name}})); + "#), WorkerScriptKind::Module); + server.wasm().await; + let dependency = server.request("/worker/worker-helper.js").await; + if order == "late-source" { + handle.post_message(serialize_test_string("load source")); + } + assert_eq!( + recv_post_json(&mut handle).await, + r#"["source",true,true]"#, + "{order}" + ); + ModuleSourceServer::write( + dependency, + "200 OK", + "text/javascript", + "export function pm(v) { self.evaluationValue = v; }", + ) + .await; + assert_eq!( + recv_post_json(&mut handle).await, + "[true,42,true]", + "{order}" + ); + handle.terminate_and_join(); + server.assert_no_more_requests(); + } +} + +#[tokio::test] +async fn worker_source_import_survives_concurrent_evaluation_graph_failure() { + ensure_v8(); + for (status, body, expected_error) in [ + ("404 Not Found", "missing", "TypeError"), + ("200 OK", "export function {", "TypeError"), + ( + "200 OK", + "export function pm() {} throw new Error('evaluation failed');", + "Error", + ), + ] { + for source_first in [true, false] { + let mut server = ModuleSourceServer::start().await; + let start = if source_first { + "const source = import.source('./worker.wasm'); const evaluation = loadEvaluation();" + } else { + "const evaluation = loadEvaluation(); const source = import.source('./worker.wasm');" + }; + let mut handle = server.worker(format!(r#" + (async () => {{ + const loadEvaluation = () => import('./worker.wasm').then(() => 'unexpected', e => e.name); + {start} + const [wasm, error] = await Promise.all([source, evaluation]); + let value; + new WebAssembly.Instance(wasm, {{ './worker-helper.js': {{ pm: v => value = v }} }}); + postMessage([error, value, wasm === await import.source('./worker.wasm')]); + }})().catch(e => postMessage({{error: e.name}})); + "#), WorkerScriptKind::Module); + server.wasm().await; + server + .respond("/worker/worker-helper.js", status, "text/javascript", body) + .await; + assert_eq!( + recv_post_json(&mut handle).await, + format!(r#"["{expected_error}",42,true]"#) + ); + handle.terminate_and_join(); + server.assert_no_more_requests(); + } + } +} + +#[tokio::test] +async fn worker_source_import_can_reuse_wasm_after_failed_evaluation_loading() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker( + r#" + (async () => { + const error = await import('./worker.wasm').then(() => 'unexpected', e => e.name); + const wasm = await import.source('./worker.wasm'); + let value; + new WebAssembly.Instance(wasm, { './worker-helper.js': { pm: v => value = v } }); + postMessage([error, value]); + })().catch(e => postMessage({error: e.name})); + "# + .into(), + WorkerScriptKind::Module, + ); + server.wasm().await; + server + .respond( + "/worker/worker-helper.js", + "404 Not Found", + "text/javascript", + "missing", + ) + .await; + assert_eq!(recv_post_json(&mut handle).await, r#"["TypeError",42]"#); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_imports_do_not_expand_an_unrelated_failed_graph() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker( + r#" + (async () => { + const error = await import('./broken.js').then(() => 'unexpected', e => e.name); + const wasm = await import.source('./worker.wasm'); + const { value } = await import('./clean.js'); + postMessage([error, wasm instanceof WebAssembly.Module, value]); + })().catch(e => postMessage({error: e.name})); + "# + .into(), + WorkerScriptKind::Module, + ); + server + .respond( + "/worker/broken.js", + "200 OK", + "text/javascript", + "import './missing.js';", + ) + .await; + server + .respond( + "/worker/missing.js", + "404 Not Found", + "text/javascript", + "missing", + ) + .await; + server.wasm().await; + server + .respond( + "/worker/clean.js", + "200 OK", + "text/javascript", + "export const value = 7;", + ) + .await; + assert_eq!(recv_post_json(&mut handle).await, r#"["TypeError",true,7]"#); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_source_and_evaluation_imports_share_root_fetch_failures() { + ensure_v8(); + for (status, body) in [("404 Not Found", "missing"), ("200 OK", "invalid wasm")] { + for source_first in [true, false] { + let mut server = ModuleSourceServer::start().await; + let imports = if source_first { + "[import.source('./worker.wasm'), import('./worker.wasm')]" + } else { + "[import('./worker.wasm'), import.source('./worker.wasm')]" + }; + let mut handle = server.worker(format!(r#" + (async () => {{ + const results = await Promise.allSettled({imports}); + const again = await import.source('./worker.wasm').then(() => 'unexpected', e => e.name); + postMessage([results.map(r => [r.status, r.reason.name]), again]); + }})().catch(e => postMessage({{error: e.name}})); + "#), WorkerScriptKind::Module); + server + .respond("/worker/worker.wasm", status, "application/wasm", body) + .await; + assert_eq!( + recv_post_json(&mut handle).await, + r#"[[["rejected","TypeError"],["rejected","TypeError"]],"TypeError"]"# + ); + handle.terminate_and_join(); + server.assert_no_more_requests(); + } + } +}