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(); + } + } +}