From 5bfb303ccd80aacce129dac0fe27fe0983fc8ce2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 13 Nov 2026 16:26:25 +0800 Subject: [PATCH] refactor(workers): drive module maps with shared module trees --- moli-module-script-tree/src/job.rs | 3 +- moli-module-script-tree/tests/module_tree.rs | 16 + moli-renderer-v8/src/worker/module_runtime.rs | 1786 ++++++----------- .../src/worker/module_runtime/module_map.rs | 278 +-- .../src/worker/module_runtime/tree.rs | 367 ++++ moli-renderer-v8/src/worker/thread/mod.rs | 157 +- .../worker/thread/tests/modules/json_parse.rs | 153 +- .../worker/thread/tests/modules/module_map.rs | 309 +++ 8 files changed, 1646 insertions(+), 1423 deletions(-) create mode 100644 moli-renderer-v8/src/worker/module_runtime/tree.rs diff --git a/moli-module-script-tree/src/job.rs b/moli-module-script-tree/src/job.rs index bb712bf2c3..0a2ebfc4ee 100644 --- a/moli-module-script-tree/src/job.rs +++ b/moli-module-script-tree/src/job.rs @@ -486,7 +486,8 @@ impl ModuleScriptTreeJob { }; self.state = ModuleTreeState::Linking; match host.link_module_graph(root_entry, &self.entries, &self.dependency_edges) { - Ok(graph) => { + Ok(mut graph) => { + graph.entry_phases = self.entry_phases.clone(); self.state = ModuleTreeState::Finished; self.terminal = Some(ModuleTreeTerminalState::Complete(graph.clone())); ModuleScriptTreePoll::Complete(graph) diff --git a/moli-module-script-tree/tests/module_tree.rs b/moli-module-script-tree/tests/module_tree.rs index d4d359b422..e50c3094e7 100644 --- a/moli-module-script-tree/tests/module_tree.rs +++ b/moli-module-script-tree/tests/module_tree.rs @@ -992,6 +992,22 @@ fn evaluation_phase_upgrades_source_phase_for_same_key() { assert_eq!(fetches.len(), 1); assert_eq!(fetches[0].key, child_key); assert_eq!(fetches[0].phase, ModuleImportPhase::Evaluation); + host.ready(child_key.clone(), ModuleEntryId(2), Vec::new()); + let ModuleScriptTreePoll::Complete(graph) = job.resume_single_module_outcome( + &mut host, + fetches[0].client, + ready_outcome(child_key, ModuleEntryId(2)), + ) else { + panic!("upgraded dependency should complete the graph"); + }; + assert_eq!( + graph.entry_phases[&ModuleEntryId(1)], + ModuleImportPhase::Evaluation + ); + assert_eq!( + graph.entry_phases[&ModuleEntryId(2)], + ModuleImportPhase::Evaluation + ); } #[test] diff --git a/moli-renderer-v8/src/worker/module_runtime.rs b/moli-renderer-v8/src/worker/module_runtime.rs index 539b55ec8d..d06a97c6ab 100644 --- a/moli-renderer-v8/src/worker/module_runtime.rs +++ b/moli-renderer-v8/src/worker/module_runtime.rs @@ -1,11 +1,17 @@ mod module_map; +mod tree; -use module_map::{WorkerModuleMap, WorkerModuleWaiter}; +use module_map::{WorkerModuleMapEntry, WorkerModuleScript}; +use moli_module_script_tree as module_tree; +use tree::{ + WorkerModuleTreeHost, WorkerModuleTreeTarget, WorkerModuleTreeTask, WorkerModuleTrees, + WorkerSingleModuleCompletion, +}; use crate::module_runtime::import_attributes_key as worker_import_attributes_key; use crate::module_runtime::module_request_attributes as worker_module_request_attributes; use std::cell::RefCell; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, VecDeque}; use std::pin::pin; use std::rc::Rc; @@ -19,9 +25,7 @@ use crate::module_runtime::{ throw_wasm_synthetic_module_error, wasm_dependency_export_value, }; use crate::util::{get_private_value, v8_string, v8str}; -use crate::wasm_module_support::{ - prepare_wasm_module_record, v8_exception_message_or, wasm_evaluation_import_modules, -}; +use crate::wasm_module_support::{prepare_wasm_module_record, wasm_evaluation_import_modules}; use moli_fetch::{BrowserRequestMetadata, RequestCredentialsMode}; use moli_webapi_declare::WebApiObject; use tokio::sync::mpsc; @@ -66,8 +70,9 @@ struct WorkerImportMetaDeclaration<'scope> { } pub(super) struct WorkerModuleGraphFetchCompletion { + key: WorkerModuleKey, fetch_id: WorkerModuleGraphFetchId, - result: Result, + result: Result, csp_violations: Vec, } @@ -86,16 +91,7 @@ pub(super) enum WorkerModuleSource { pub(super) struct WorkerModuleEvaluationCompletion { evaluation_id: WorkerModuleEvaluationId, - result: Result<(), String>, -} - -pub(super) enum WorkerDynamicModuleImportAdvance { - Complete, - NeedFetches(WorkerModuleGraphFetchBatch), - WaitingFetches, - WaitingEvaluation { - evaluation_id: WorkerModuleEvaluationId, - }, + result: Result<(), v8::Global>, } #[derive(Clone, Copy)] @@ -177,18 +173,6 @@ impl WorkerDynamicModuleImportError { stage: WorkerDynamicModuleImportErrorStage::Evaluate, } } - - fn clone_for_scope(&self, scope: &mut v8::PinScope<'_, '_>) -> Self { - Self { - message: self.message.clone(), - kind: self.kind, - rejection: self - .rejection - .as_ref() - .map(|value| v8::Global::new(scope, v8::Local::new(scope, value))), - stage: self.stage, - } - } } impl From for WorkerDynamicModuleImportError { @@ -215,9 +199,15 @@ impl From> for WorkerDynamicModuleImportError { impl WorkerModuleGraphFetchCompletion { pub(super) fn new( fetch_id: WorkerModuleGraphFetchId, + key: WorkerModuleKey, result: Result, ) -> Self { + let result = result.map_err(|message| { + module_tree::ModuleLoadError::new(module_tree::ModuleLoadStage::Fetch, message) + .with_key(tree::tree_key(&key)) + }); Self { + key, fetch_id, result, csp_violations: Vec::new(), @@ -242,8 +232,10 @@ impl WorkerModuleGraphFetchCompletion { self.csp_violations.iter() } - pub(super) fn result(&self) -> Result<&WorkerModuleFetchedSource, &str> { - self.result.as_ref().map_err(String::as_str) + pub(super) fn result( + &self, + ) -> Result<&WorkerModuleFetchedSource, &module_tree::ModuleLoadError> { + self.result.as_ref() } } @@ -339,7 +331,10 @@ impl WorkerModuleSource { } impl WorkerModuleEvaluationCompletion { - fn new(evaluation_id: WorkerModuleEvaluationId, result: Result<(), String>) -> Self { + fn new( + evaluation_id: WorkerModuleEvaluationId, + result: Result<(), v8::Global>, + ) -> Self { Self { evaluation_id, result, @@ -350,8 +345,8 @@ impl WorkerModuleEvaluationCompletion { self.evaluation_id } - pub(super) fn result(&self) -> Result<(), &str> { - self.result.as_ref().map(|_| ()).map_err(String::as_str) + pub(super) fn result(&self) -> Result<(), &v8::Global> { + self.result.as_ref().map(|_| ()) } } @@ -364,177 +359,132 @@ pub(super) fn evaluate_module_worker_bootstrap_source( referrer_policy: Option, evaluation_completion_tx: mpsc::UnboundedSender, ) -> WorkerModuleBootstrapStart { - let try_catch = pin!(v8::TryCatch::new(scope)); - let mut scope = try_catch.init(); let root_url = match Url::parse(script_url) { Ok(url) => url, Err(_) => { return WorkerModuleBootstrapStart::Failed(Box::new(worker_bootstrap_error( - &mut scope, + scope, script_url, "Module worker script URL is invalid", WorkerParentErrorEventKind::Event, ))); } }; - let static_import_initiator_url = - static_import_initiator_url.unwrap_or_else(|| root_url.clone()); - let runtime = - WorkerModuleRuntime::new(credentials_mode, referrer_policy, evaluation_completion_tx); + let runtime = WorkerModuleRuntime::new( + credentials_mode, + referrer_policy.clone(), + evaluation_completion_tx, + ); runtime.install_context_slot(scope.get_current_context()); - let mut bootstrap = runtime.start_bootstrap(source, root_url, static_import_initiator_url); - match bootstrap.advance(&mut scope) { - WorkerModuleAdvance::ReadyToInstantiate { root_entry } => { - match finish_worker_module_bootstrap( - &mut scope, - &bootstrap.runtime, - root_entry, - script_url, - ) { - Ok(WorkerModuleFinish::Complete) => WorkerModuleBootstrapStart::Complete, - Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }) => { - WorkerModuleBootstrapStart::Pending { - bootstrap: Box::new(WorkerModulePendingBootstrap::new_evaluation( - bootstrap, - evaluation_id, - )), - requests: None, - } - } - Err(error) => WorkerModuleBootstrapStart::Failed(error), + let key = WorkerModuleKey::javascript_or_wasm(root_url.clone()); + let root_entry = create_worker_module_script( + scope, + &runtime.modules, + &source, + key.clone(), + root_url.clone(), + referrer_policy.clone(), + ); + let root = module_tree::ModuleRootInput::Inline(module_tree::ModuleInlineRootInput { + root_key: tree::tree_key(&key), + root_entry: tree::entry_id(root_entry), + source_url: root_url.clone(), + base_url: root_url.clone(), + phase: module_tree::ModuleImportPhase::Evaluation, + fetch_metadata: tree::fetch_metadata(credentials_mode, referrer_policy.as_deref()), + referrer: module_tree::ModuleReferrer::client(), + position: module_tree::TextPosition::default(), + }); + let task = WorkerModuleTreeTask { + tree: module_tree::ModuleScriptTreeJob::new( + root, + module_tree::ModuleTreeConfig { + tree_id: tree::BOOTSTRAP_TREE, + owner: module_tree::ModuleTreeOwner { + kind: module_tree::ModuleTreeOwnerKind::WorkerModuleScript, + requester: module_tree::ModuleFetchRequester::RuntimeModuleScript, + ordering: module_tree::ModuleFetchOrdering::Runtime, + }, + custom_fetch_type: module_tree::ModuleScriptCustomFetchType::WorkerConstructor, + ..module_tree::ModuleTreeConfig::default() + }, + ), + target: WorkerModuleTreeTarget::Bootstrap, + options: WorkerModuleGraphFetchOptions { + initiator_url: static_import_initiator_url.unwrap_or(root_url), + csp_source: WorkerModuleGraphFetchCspSource::StaticModuleGraph, + browser_request_metadata: BrowserRequestMetadata::Fetch, + referrer_policy, + }, + }; + let mut bootstrap = WorkerModulePendingBootstrap { + script_url: script_url.to_owned(), + state: WorkerModulePendingBootstrapState::Tree(tree::BOOTSTRAP_TREE), + }; + match drive_worker_module_tree(scope, &runtime.modules, &runtime.trees, task, None) { + Some(terminal) => match bootstrap.resume_tree(scope, terminal.tree_id, terminal.result) { + WorkerModuleBootstrapResume::Complete => WorkerModuleBootstrapStart::Complete, + WorkerModuleBootstrapResume::Failed(error) => WorkerModuleBootstrapStart::Failed(error), + WorkerModuleBootstrapResume::WaitingEvaluation => { + WorkerModuleBootstrapStart::Pending(Box::new(bootstrap)) } - } - WorkerModuleAdvance::NeedFetches(requests) => { - let (bootstrap, requests) = - WorkerModulePendingBootstrap::new_fetches(bootstrap, requests); - WorkerModuleBootstrapStart::Pending { - bootstrap: Box::new(bootstrap), - requests, - } - } - WorkerModuleAdvance::Failed(error) => WorkerModuleBootstrapStart::Failed(error), + }, + None => WorkerModuleBootstrapStart::Pending(Box::new(bootstrap)), } } pub(super) enum WorkerModuleBootstrapStart { Complete, - Pending { - bootstrap: Box, - requests: Option, - }, + Pending(Box), Failed(Box), } pub(super) enum WorkerModuleBootstrapResume { Complete, - NeedFetches(WorkerModuleGraphFetchBatch), - WaitingFetches, WaitingEvaluation, Failed(Box), } pub(super) struct WorkerModulePendingBootstrap { - job: WorkerModuleBootstrapJob, + script_url: String, state: WorkerModulePendingBootstrapState, } enum WorkerModulePendingBootstrapState { - Fetch(HashSet), + Tree(module_tree::ModuleTreeId), Evaluation { evaluation_id: WorkerModuleEvaluationId, }, } impl WorkerModulePendingBootstrap { - fn new_fetches( - job: WorkerModuleBootstrapJob, - requests: WorkerModuleGraphFetchBatch, - ) -> (Self, Option) { - let keys = requests.pending_keys(); - let requests = job - .runtime - .graph - .borrow_mut() - .module_map - .subscribe(WorkerModuleWaiter::Bootstrap, requests); - ( - Self { - job, - state: WorkerModulePendingBootstrapState::Fetch(keys), - }, - requests, - ) - } - - fn new_evaluation( - job: WorkerModuleBootstrapJob, - evaluation_id: WorkerModuleEvaluationId, - ) -> Self { - Self { - job, - state: WorkerModulePendingBootstrapState::Evaluation { evaluation_id }, - } - } - - fn resume( + fn resume_tree( &mut self, scope: &mut v8::PinScope<'_, '_>, - key: &WorkerModuleKey, - load_error: Option<&str>, + tree_id: module_tree::ModuleTreeId, + result: Result, ) -> WorkerModuleBootstrapResume { - let WorkerModulePendingBootstrapState::Fetch(keys) = &mut self.state else { - unreachable!("only a fetching bootstrap subscribes to module-map fetches"); - }; assert!( - keys.remove(key), - "module map woke an unsubscribed bootstrap" + matches!(self.state, WorkerModulePendingBootstrapState::Tree(id) if id == tree_id), + "module tree completion must match the bootstrap client" ); - self.job - .runtime - .graph - .borrow_mut() - .module_map - .unsubscribe(WorkerModuleWaiter::Bootstrap, std::mem::take(keys)); - if let Some(message) = load_error { - return WorkerModuleBootstrapResume::Failed(Box::new(worker_bootstrap_error( - scope, - self.job.script_url(), - message, - WorkerParentErrorEventKind::Event, - ))); - } - match self.job.advance(scope) { - WorkerModuleAdvance::ReadyToInstantiate { root_entry } => { - match finish_worker_module_bootstrap( + let graph = match result { + Ok(graph) => graph, + Err(error) => { + return WorkerModuleBootstrapResume::Failed(worker_bootstrap_tree_error( scope, - &self.job.runtime, - root_entry, - self.job.script_url(), - ) { - Ok(WorkerModuleFinish::Complete) => WorkerModuleBootstrapResume::Complete, - Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }) => { - self.state = - WorkerModulePendingBootstrapState::Evaluation { evaluation_id }; - WorkerModuleBootstrapResume::WaitingEvaluation - } - Err(error) => WorkerModuleBootstrapResume::Failed(error), - } + &self.script_url, + error, + )); } - WorkerModuleAdvance::NeedFetches(requests) => { - self.state = WorkerModulePendingBootstrapState::Fetch(requests.pending_keys()); - match self - .job - .runtime - .graph - .borrow_mut() - .module_map - .subscribe(WorkerModuleWaiter::Bootstrap, requests) - { - Some(requests) => WorkerModuleBootstrapResume::NeedFetches(requests), - None => WorkerModuleBootstrapResume::WaitingFetches, - } + }; + match finish_worker_module_bootstrap(scope, &graph, &self.script_url) { + Ok(WorkerModuleFinish::Complete) => WorkerModuleBootstrapResume::Complete, + Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }) => { + self.state = WorkerModulePendingBootstrapState::Evaluation { evaluation_id }; + WorkerModuleBootstrapResume::WaitingEvaluation } - WorkerModuleAdvance::Failed(error) => WorkerModuleBootstrapResume::Failed(error), + Err(error) => WorkerModuleBootstrapResume::Failed(error), } } @@ -543,33 +493,21 @@ impl WorkerModulePendingBootstrap { scope: &mut v8::PinScope<'_, '_>, completion: WorkerModuleEvaluationCompletion, ) -> WorkerModuleBootstrapResume { - let WorkerModulePendingBootstrapState::Evaluation { evaluation_id } = self.state else { - return WorkerModuleBootstrapResume::Failed(Box::new(worker_bootstrap_error( - scope, - self.job.script_url(), - "Module worker evaluation completion arrived while waiting for graph fetch", - WorkerParentErrorEventKind::Event, - ))); - }; - if completion.evaluation_id != evaluation_id { - return WorkerModuleBootstrapResume::Failed(Box::new(worker_bootstrap_error( - scope, - self.job.script_url(), - &format!( - "Module worker evaluation completion id {} did not match pending id {evaluation_id}", - completion.evaluation_id - ), - WorkerParentErrorEventKind::Event, - ))); - } + assert!( + matches!(self.state, WorkerModulePendingBootstrapState::Evaluation { evaluation_id } if evaluation_id == completion.evaluation_id), + "module evaluation completion must match its bootstrap owner" + ); match completion.result { Ok(()) => WorkerModuleBootstrapResume::Complete, - Err(message) => WorkerModuleBootstrapResume::Failed(Box::new(worker_bootstrap_error( - scope, - self.job.script_url(), - &message, - WorkerParentErrorEventKind::ErrorEvent, - ))), + Err(reason) => { + let value = v8::Local::new(scope, reason); + WorkerModuleBootstrapResume::Failed(Box::new(worker_bootstrap_value_error( + scope, + &self.script_url, + value, + WorkerParentErrorEventKind::ErrorEvent, + ))) + } } } } @@ -583,14 +521,17 @@ enum WorkerModuleFinish { fn finish_worker_module_bootstrap( scope: &mut v8::PinScope<'_, '_>, - runtime: &WorkerModuleRuntime, - root_entry: usize, + graph: &module_tree::ModuleGraphHandle, script_url: &str, ) -> WorkerModuleBootstrapResult { let try_catch = pin!(v8::TryCatch::new(scope)); let mut scope = try_catch.init(); let scope = &mut scope; - let root_module_global = runtime.graph.borrow().module(root_entry).clone(); + let modules = scope + .get_current_context() + .get_slot::>() + .expect("worker module map installed"); + let root_module_global = modules.borrow().module(graph.root_entry.0 as usize).clone(); let root_module = v8::Local::new(scope, &root_module_global); // Include dependencies of synthetic Wasm modules, which V8's IsGraphAsync @@ -601,11 +542,11 @@ fn finish_worker_module_bootstrap( state.borrow().global_kind, super::thread::WorkerGlobalKind::Service { .. } ) - }) && runtime - .graph - .borrow() - .has_top_level_await(scope, root_entry) - { + }) && graph.entries.iter().any(|entry| { + graph.entry_phases.get(entry) == Some(&module_tree::ModuleImportPhase::Evaluation) + && v8::Local::new(scope, modules.borrow().module(entry.0 as usize)) + .has_top_level_await() + }) { let message = v8str(scope, "Top-level await is not allowed in service workers."); let exception = v8::Exception::type_error(scope, message); return Err(Box::new(worker_bootstrap_value_error( @@ -678,7 +619,8 @@ fn finish_worker_module_bootstrap( ))); } v8::PromiseState::Pending => { - let evaluation_id = runtime.reserve_evaluation_id(); + let evaluation_id = reserve_worker_module_evaluation_id(scope) + .expect("worker evaluation slot installed"); let promise = v8::Global::new(scope, promise); attach_worker_module_evaluation_reactions(scope, evaluation_id, promise)?; return Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }); @@ -689,9 +631,10 @@ fn finish_worker_module_bootstrap( Ok(WorkerModuleFinish::Complete) } +#[derive(Clone)] struct WorkerModuleRuntime { - graph: Rc>, - next_fetch_id: Rc>, + modules: Rc>, + trees: Rc>, next_evaluation_id: Rc>, dynamic_imports: Rc>, evaluation_completion_tx: mpsc::UnboundedSender, @@ -704,11 +647,11 @@ impl WorkerModuleRuntime { evaluation_completion_tx: mpsc::UnboundedSender, ) -> Self { Self { - graph: Rc::new(RefCell::new(WorkerModuleGraph::new( + modules: Rc::new(RefCell::new(WorkerModuleMap::new( credentials_mode, referrer_policy, ))), - next_fetch_id: Rc::new(RefCell::new(0)), + trees: Rc::new(RefCell::new(WorkerModuleTrees::default())), next_evaluation_id: Rc::new(RefCell::new(0)), dynamic_imports: Rc::new(RefCell::new(WorkerDynamicModuleResolver::default())), evaluation_completion_tx, @@ -716,36 +659,13 @@ impl WorkerModuleRuntime { } fn install_context_slot(&self, context: v8::Local<'_, v8::Context>) { - let _previous_graph = context.set_slot(self.graph.clone()); - let _previous_fetch_id = context.set_slot(Rc::new(WorkerModuleRuntimeFetchIdSlot { - next_fetch_id: Rc::clone(&self.next_fetch_id), - })); + let _previous_modules = context.set_slot(self.modules.clone()); + let _previous_trees = context.set_slot(self.trees.clone()); let _previous_evaluation = context.set_slot(Rc::new(WorkerModuleRuntimeEvaluationSlot { - next_evaluation_id: Rc::clone(&self.next_evaluation_id), + next_evaluation_id: self.next_evaluation_id.clone(), evaluation_completion_tx: self.evaluation_completion_tx.clone(), })); - let _previous_dynamic_imports = context.set_slot(self.dynamic_imports.clone()); - } - - fn start_bootstrap( - &self, - source: WorkerModuleSource, - root_url: Url, - static_import_initiator_url: Url, - ) -> WorkerModuleBootstrapJob { - WorkerModuleBootstrapJob { - runtime: self.clone(), - source, - root_url, - static_import_initiator_url, - } - } - - fn reserve_evaluation_id(&self) -> WorkerModuleEvaluationId { - let mut next_evaluation_id = self.next_evaluation_id.borrow_mut(); - let evaluation_id = *next_evaluation_id; - *next_evaluation_id += 1; - evaluation_id + let _previous_imports = context.set_slot(self.dynamic_imports.clone()); } } @@ -762,86 +682,164 @@ pub(super) fn install_classic_worker_dynamic_module_runtime( .install_context_slot(scope.get_current_context()); } -pub(super) fn run_next_worker_dynamic_module_import( +struct WorkerModuleTreeTerminal { + tree_id: module_tree::ModuleTreeId, + target: WorkerModuleTreeTarget, + result: Result, +} + +fn drive_worker_module_tree( scope: &mut v8::PinScope<'_, '_>, -) -> Option { - let context = scope.get_current_context(); - let dynamic_imports = context.get_slot::>()?; - let graph = context.get_slot::>()?; - let mut imports = dynamic_imports.borrow_mut(); - let index = imports.pending_imports.iter().position(|job| { - matches!( - job.state, - WorkerDynamicModuleImportJobState::Graph | WorkerDynamicModuleImportJobState::Failed(_) - ) - })?; - let mut job = imports - .pending_imports - .remove(index) - .expect("position came from pending_imports"); - drop(imports); - graph.borrow_mut().module_map.unsubscribe( - WorkerModuleWaiter::DynamicImport(job.id), - std::mem::take(&mut job.waiting_on), - ); - let state = std::mem::replace(&mut job.state, WorkerDynamicModuleImportJobState::Graph); - let advance = match state { - WorkerDynamicModuleImportJobState::Failed(error) => Err(error), - _ => advance_worker_dynamic_module_import(scope, &mut job), + modules: &Rc>, + work: &Rc>, + mut task: WorkerModuleTreeTask, + completion: Option, +) -> Option { + let tree_id = task.tree.tree_id(); + let drive = { + let mut host = WorkerModuleTreeHost { + scope, + modules: modules.clone(), + work: work.clone(), + options: &task.options, + }; + match completion { + Some(completion) => { + let outcome = match completion.result { + Ok(entry) => tree::ready_module(&modules.borrow(), entry), + Err(error) => module_tree::ModuleFetchOutcome::Failed(error), + }; + task.tree.resume_single_module_outcome_and_drive( + &mut host, + completion.client, + outcome, + ) + } + None => task.tree.drive(&mut host), + } }; - match advance { - Ok(WorkerDynamicModuleImportAdvance::Complete) => { - resolve_worker_dynamic_module_import(scope, job); - Some(WorkerDynamicModuleImportAdvance::Complete) + let result = match drive { + module_tree::ModuleScriptTreeDrive::Complete(graph) => Ok(graph), + module_tree::ModuleScriptTreeDrive::Failed(error) => { + modules.borrow_mut().detach_tree(tree_id); + Err(error) } - Ok(WorkerDynamicModuleImportAdvance::NeedFetches(requests)) => { - job.waiting_on = requests.pending_keys(); - job.state = WorkerDynamicModuleImportJobState::Fetch; - let requests = graph - .borrow_mut() - .module_map - .subscribe(WorkerModuleWaiter::DynamicImport(job.id), requests); - dynamic_imports.borrow_mut().pending_imports.push_back(job); - Some(match requests { - Some(requests) => WorkerDynamicModuleImportAdvance::NeedFetches(requests), - None => WorkerDynamicModuleImportAdvance::WaitingFetches, - }) + module_tree::ModuleScriptTreeDrive::Aborted(_) => { + modules.borrow_mut().detach_tree(tree_id); + Err(module_tree::ModuleLoadError::new( + module_tree::ModuleLoadStage::Abort, + "worker module tree was cancelled", + )) } - Ok(WorkerDynamicModuleImportAdvance::WaitingFetches) => { - dynamic_imports.borrow_mut().pending_imports.push_back(job); - Some(WorkerDynamicModuleImportAdvance::WaitingFetches) + module_tree::ModuleScriptTreeDrive::Pending(_) + | module_tree::ModuleScriptTreeDrive::NeedFetches(_) + | module_tree::ModuleScriptTreeDrive::WaitingForSingleModuleClients(_) + | module_tree::ModuleScriptTreeDrive::IgnoredStaleCompletion(_) => { + work.borrow_mut().tasks.insert(tree_id, task); + return None; } - Ok(WorkerDynamicModuleImportAdvance::WaitingEvaluation { evaluation_id }) => { - job.state = WorkerDynamicModuleImportJobState::Evaluation { evaluation_id }; - dynamic_imports.borrow_mut().pending_imports.push_back(job); - Some(WorkerDynamicModuleImportAdvance::WaitingEvaluation { evaluation_id }) + }; + Some(WorkerModuleTreeTerminal { + tree_id, + target: task.target, + result, + }) +} + +pub(super) fn run_next_worker_dynamic_module_import(scope: &mut v8::PinScope<'_, '_>) -> bool { + let context = scope.get_current_context(); + let Some(imports) = context.get_slot::>() else { + return false; + }; + let Some(job) = imports.borrow_mut().pending_imports.pop_front() else { + return false; + }; + let modules = context + .get_slot::>() + .expect("worker module map installed"); + let work = context + .get_slot::>() + .expect("worker module tree runtime installed"); + let url = match resolve_worker_module_specifier(&job.specifier, &job.base_url) { + Ok(url) => url, + Err(message) => { + reject_worker_dynamic_module_import(scope, job, message.into()); + return true; } - Err(error) => { - finish_failed_worker_dynamic_module_import(scope, job, error); - Some(WorkerDynamicModuleImportAdvance::Complete) + }; + let key = match worker_module_key_for_attributes(&url, &job.attributes) { + Ok(key) => key, + Err(message) => { + let error = WorkerDynamicModuleImportError::type_error(format!( + "{message} for dynamic import `{}`", + job.specifier, + )); + reject_worker_dynamic_module_import(scope, job, error); + return true; } + }; + let referrer_policy = modules.borrow().referrer_policy_for_url(&job.base_url); + let metadata = tree::fetch_metadata( + modules.borrow().credentials_mode, + referrer_policy.as_deref(), + ); + let root = module_tree::ModuleRootInput::External(module_tree::ModuleExternalRootInput { + source_url: url.clone(), + base_url: url, + initiator_url: job.fetch_initiator_url.clone(), + attributes: module_tree::ModuleAttributesKey::from_pairs(job.attributes.pairs().to_vec()), + phase: tree::tree_phase(job.phase), + kind_hint: Some(tree::tree_key(&key).kind), + fetch_metadata: metadata, + referrer: module_tree::ModuleReferrer::client(), + position: module_tree::TextPosition::default(), + }); + let options = WorkerModuleGraphFetchOptions { + initiator_url: job.fetch_initiator_url.clone(), + csp_source: WorkerModuleGraphFetchCspSource::DynamicImportGraph, + browser_request_metadata: job.browser_request_metadata(), + referrer_policy, + }; + let task = WorkerModuleTreeTask { + tree: module_tree::ModuleScriptTreeJob::new( + root, + module_tree::ModuleTreeConfig { + tree_id: job.tree_id, + owner: module_tree::ModuleTreeOwner::dynamic_import(), + custom_fetch_type: if job.audio_worklet_bootstrap { + module_tree::ModuleScriptCustomFetchType::WorkletAddModule + } else { + module_tree::ModuleScriptCustomFetchType::None + }, + ..module_tree::ModuleTreeConfig::default() + }, + ), + target: WorkerModuleTreeTarget::Import(Box::new(job)), + options, + }; + if let Some(terminal) = drive_worker_module_tree(scope, &modules, &work, task, None) { + let WorkerModuleTreeTarget::Import(job) = terminal.target else { + unreachable!("started an import tree"); + }; + finish_worker_dynamic_module_tree(scope, *job, terminal.result); } + true +} + +pub(super) fn take_worker_module_graph_fetches( + scope: &mut v8::PinScope<'_, '_>, +) -> Vec { + let Some(work) = scope + .get_current_context() + .get_slot::>() + else { + return Vec::new(); + }; + std::mem::take(&mut work.borrow_mut().fetches_to_start) } pub(super) struct WorkerModuleGraphFetchOutcome { - key: WorkerModuleKey, - bootstrap_waiting: bool, - load_error: Option, -} - -impl WorkerModuleGraphFetchOutcome { - pub(super) fn resume_bootstrap( - self, - scope: &mut v8::PinScope<'_, '_>, - bootstrap: Option<&mut WorkerModulePendingBootstrap>, - ) -> Option { - if self.bootstrap_waiting { - bootstrap - .map(|bootstrap| bootstrap.resume(scope, &self.key, self.load_error.as_deref())) - } else { - None - } - } + pub(super) bootstrap_client: bool, } pub(super) fn complete_worker_module_graph_fetch( @@ -849,347 +847,257 @@ pub(super) fn complete_worker_module_graph_fetch( completion: &WorkerModuleGraphFetchCompletion, ) -> Option { let context = scope.get_current_context(); - let graph = context.get_slot::>()?; - let (request, waiters) = graph + let modules = context.get_slot::>()?; + let work = context.get_slot::>()?; + let (request, clients) = modules .borrow_mut() - .module_map - .take_fetch(completion.fetch_id())?; - if let Ok(source) = &completion.result { - // Publish the record or original parse exception before waking any job, - // even when the graph which started this request has already failed. - let _ = cache_worker_module_fetched_source(scope, &graph, &request, source); + .take_fetch(&completion.key, completion.fetch_id)?; + let result = match &completion.result { + Ok(source) => Ok(create_worker_module_script( + scope, + &modules, + source.source(), + request.key.clone(), + source.final_url().clone(), + source.effective_referrer_policy(request.referrer_policy()), + )), + Err(error) => Err(error.clone()), + }; + let bootstrap_client = clients + .iter() + .any(|client| client.tree_id == tree::BOOTSTRAP_TREE); + let mut work = work.borrow_mut(); + for client in clients { + work.notifications.push_back(WorkerSingleModuleCompletion { + client, + result: result.clone(), + }); } - let load_error = completion.result().err(); - if let Some(imports) = context.get_slot::>() { - let mut imports = imports.borrow_mut(); - for job in &mut imports.pending_imports { - if waiters.contains(&WorkerModuleWaiter::DynamicImport(job.id)) - && job.waiting_on.remove(&request.key) - && matches!(job.state, WorkerDynamicModuleImportJobState::Fetch) + Some(WorkerModuleGraphFetchOutcome { bootstrap_client }) +} + +pub(super) fn resume_worker_module_trees( + scope: &mut v8::PinScope<'_, '_>, + mut bootstrap: Option<&mut WorkerModulePendingBootstrap>, +) -> Option { + let context = scope.get_current_context(); + let modules = context.get_slot::>()?; + let work = context.get_slot::>()?; + let mut bootstrap_result = None; + loop { + let Some(completion) = work.borrow_mut().notifications.pop_front() else { + break; + }; + let task = work.borrow_mut().tasks.remove(&completion.client.tree_id); + let Some(task) = task else { + continue; + }; + let Some(terminal) = + drive_worker_module_tree(scope, &modules, &work, task, Some(completion)) + else { + continue; + }; + match terminal.target { + WorkerModuleTreeTarget::Import(job) => { + finish_worker_dynamic_module_tree(scope, *job, terminal.result) + } + WorkerModuleTreeTarget::Bootstrap => { + if let Some(bootstrap) = bootstrap.as_deref_mut() { + bootstrap_result = + Some(bootstrap.resume_tree(scope, terminal.tree_id, terminal.result)); + } + } + } + } + bootstrap_result +} + +fn worker_bootstrap_tree_error( + scope: &mut v8::PinScope<'_, '_>, + script_url: &str, + error: module_tree::ModuleLoadError, +) -> Box { + let modules = scope + .get_current_context() + .get_slot::>() + .expect("worker module map installed"); + if let Some(entry) = error.module_entry + && let Err(stored) = &modules.borrow().script(entry.0 as usize).creation + { + return stored.clone(); + } + Box::new(worker_bootstrap_error( + scope, + script_url, + &error.message, + WorkerParentErrorEventKind::Event, + )) +} + +fn worker_dynamic_tree_error( + scope: &mut v8::PinScope<'_, '_>, + error: module_tree::ModuleLoadError, +) -> WorkerDynamicModuleImportError { + let modules = scope + .get_current_context() + .get_slot::>() + .expect("worker module map installed"); + if let Some(entry) = error.module_entry + && let Err(stored) = &modules.borrow().script(entry.0 as usize).creation + { + return stored.clone().into(); + } + if error.error_constructor == Some(module_tree::ModuleErrorConstructorKind::SyntaxError) { + WorkerDynamicModuleImportError::syntax_error(error.message) + } else { + WorkerDynamicModuleImportError::type_error(error.message) + } +} + +fn finish_worker_dynamic_module_tree( + scope: &mut v8::PinScope<'_, '_>, + job: WorkerDynamicModuleImportJob, + result: Result, +) { + let graph = match result { + Ok(graph) => graph, + Err(error) => { + let error = worker_dynamic_tree_error(scope, error); + reject_worker_dynamic_module_import(scope, job, error); + return; + } + }; + let root_entry = graph.root_entry.0 as usize; + if job.phase == ModuleImportPhase::Source { + resolve_worker_dynamic_module_import(scope, job, root_entry); + return; + } + match finish_worker_dynamic_module_import_evaluation(scope, root_entry) { + Ok(WorkerModuleFinish::Complete) => { + resolve_worker_dynamic_module_import(scope, job, root_entry) + } + Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }) => { + let imports = scope + .get_current_context() + .get_slot::>() + .expect("worker import resolver installed"); + imports.borrow_mut().evaluations.insert( + evaluation_id, + WorkerDynamicModuleEvaluation { job, root_entry }, + ); + } + Err(error) => { + if job.audio_worklet_bootstrap + && error.stage == WorkerDynamicModuleImportErrorStage::Evaluate { - job.state = match load_error { - Some(message) => WorkerDynamicModuleImportJobState::Failed( - WorkerDynamicModuleImportError::type_error(format!( - "Failed to dynamically import module worker dependency `{}`: {message}", - request.url(), - )), - ), - None => WorkerDynamicModuleImportJobState::Graph, - }; + resolve_worker_dynamic_module_import(scope, job, root_entry); + } else { + reject_worker_dynamic_module_import(scope, job, error); } } } - Some(WorkerModuleGraphFetchOutcome { - key: request.key, - bootstrap_waiting: waiters.contains(&WorkerModuleWaiter::Bootstrap), - load_error: load_error.map(str::to_owned), - }) } pub(super) fn worker_has_pending_dynamic_module_imports(scope: &mut v8::PinScope<'_, '_>) -> bool { let context = scope.get_current_context(); - let Some(dynamic_imports) = context.get_slot::>() else { - return false; - }; - !dynamic_imports.borrow().pending_imports.is_empty() + context + .get_slot::>() + .is_some_and(|imports| { + let imports = imports.borrow(); + !imports.pending_imports.is_empty() || !imports.evaluations.is_empty() + }) || context - .get_slot::>() - .is_some_and(|graph| graph.borrow().module_map.has_pending_fetches()) + .get_slot::>() + .is_some_and(|work| { + let work = work.borrow(); + !work.tasks.is_empty() + || !work.notifications.is_empty() + || !work.fetches_to_start.is_empty() + }) + || context + .get_slot::>() + .is_some_and(|modules| modules.borrow().has_pending_fetches()) } pub(super) fn worker_has_runnable_dynamic_module_imports(scope: &mut v8::PinScope<'_, '_>) -> bool { - let context = scope.get_current_context(); - let Some(dynamic_imports) = context.get_slot::>() else { - return false; - }; - dynamic_imports.borrow().pending_imports.iter().any(|job| { - matches!( - job.state, - WorkerDynamicModuleImportJobState::Graph | WorkerDynamicModuleImportJobState::Failed(_) - ) - }) + scope + .get_current_context() + .get_slot::>() + .is_some_and(|imports| !imports.borrow().pending_imports.is_empty()) +} + +pub(super) fn retire_worker_module_runtime(context: v8::Local<'_, v8::Context>) { + if let Some(work) = context.get_slot::>() { + let mut work = work.borrow_mut(); + for (_, mut task) in work.tasks.drain() { + task.tree + .cancel(module_tree::ModuleTreeAbortReason::ContextDestroyed); + } + work.notifications.clear(); + work.fetches_to_start.clear(); + } + if let Some(imports) = context.get_slot::>() { + let mut imports = imports.borrow_mut(); + imports.pending_imports.clear(); + imports.evaluations.clear(); + } + if let Some(modules) = context.get_slot::>() { + let mut modules = modules.borrow_mut(); + modules.entries.clear(); + modules.scripts.clear(); + modules.module_to_entries.clear(); + modules.pending_fetch_count = 0; + } } pub(super) fn resume_worker_dynamic_module_evaluation( scope: &mut v8::PinScope<'_, '_>, completion: &WorkerModuleEvaluationCompletion, ) -> bool { - let context = scope.get_current_context(); - let Some(dynamic_imports) = context.get_slot::>() else { + let Some(imports) = scope + .get_current_context() + .get_slot::>() + else { return false; }; - let mut imports = dynamic_imports.borrow_mut(); - let Some(index) = imports.pending_imports.iter().position(|job| { - matches!( - job.state, - WorkerDynamicModuleImportJobState::Evaluation { evaluation_id } - if evaluation_id == completion.evaluation_id - ) - }) else { + let Some(evaluation) = imports + .borrow_mut() + .evaluations + .remove(&completion.evaluation_id) + else { return false; }; - let job = imports - .pending_imports - .remove(index) - .expect("position came from pending_imports"); - drop(imports); match completion.result() { - Ok(()) => resolve_worker_dynamic_module_import(scope, job), - Err(message) => finish_failed_worker_dynamic_module_import( - scope, - job, - WorkerDynamicModuleImportError { - message: message.to_owned(), - kind: WorkerDynamicModuleImportErrorKind::Type, - rejection: None, - stage: WorkerDynamicModuleImportErrorStage::Evaluate, - }, - ), + Ok(()) => { + resolve_worker_dynamic_module_import(scope, evaluation.job, evaluation.root_entry) + } + Err(reason) => { + if evaluation.job.audio_worklet_bootstrap { + resolve_worker_dynamic_module_import(scope, evaluation.job, evaluation.root_entry); + } else { + let value = v8::Local::new(scope, reason); + let error = WorkerDynamicModuleImportError::evaluation_rejection_value( + scope, + value, + "dynamic import module evaluation rejected", + ); + reject_worker_dynamic_module_import(scope, evaluation.job, error); + } + } } true } -impl Clone for WorkerModuleRuntime { - fn clone(&self) -> Self { - Self { - graph: Rc::clone(&self.graph), - next_fetch_id: Rc::clone(&self.next_fetch_id), - next_evaluation_id: Rc::clone(&self.next_evaluation_id), - dynamic_imports: Rc::clone(&self.dynamic_imports), - evaluation_completion_tx: self.evaluation_completion_tx.clone(), - } - } -} - -#[derive(Default)] -struct WorkerDynamicModuleResolver { - next_import_id: u64, - pending_imports: VecDeque, -} - -impl WorkerDynamicModuleResolver { - fn queue_import( - &mut self, - context: v8::Global, - resolver: v8::Global, - specifier: String, - base_url: Url, - fetch_initiator_url: Url, - attributes: ModuleAttributesKey, - phase: ModuleImportPhase, - audio_worklet_bootstrap: bool, - ) { - let id = self.next_import_id; - self.next_import_id = id.checked_add(1).expect("worker import id space exhausted"); - self.pending_imports - .push_back(WorkerDynamicModuleImportJob::new( - id, - context, - resolver, - specifier, - base_url, - fetch_initiator_url, - attributes, - phase, - audio_worklet_bootstrap, - )); - } - - fn root_evaluation_in_flight(&self, key: &WorkerModuleKey) -> bool { - self.pending_imports.iter().any(|job| { - job.root_key.as_ref() == Some(key) - && matches!( - job.state, - WorkerDynamicModuleImportJobState::Evaluation { .. } - ) - }) - } - - fn take_evaluation_waiters( - &mut self, - key: &WorkerModuleKey, - ) -> Vec { - let mut joined = Vec::new(); - let mut remaining = VecDeque::with_capacity(self.pending_imports.len()); - while let Some(job) = self.pending_imports.pop_front() { - if matches!( - job.state, - WorkerDynamicModuleImportJobState::JoinedEvaluation - ) && job.root_key.as_ref() == Some(key) - { - joined.push(job); - } else { - remaining.push_back(job); - } - } - self.pending_imports = remaining; - joined - } -} - -struct WorkerDynamicModuleImportJob { - id: u64, - waiting_on: HashSet, - context: v8::Global, - resolver: v8::Global, - specifier: String, - // Referencing module URL used only for specifier resolution. - base_url: Url, - // Worker inside-settings URL used for fetch security checks. - fetch_initiator_url: Url, - attributes: ModuleAttributesKey, - phase: ModuleImportPhase, - audio_worklet_bootstrap: bool, - root_key: Option, - resolved_entry: Option, - state: WorkerDynamicModuleImportJobState, -} - -enum WorkerDynamicModuleImportJobState { - Graph, - Failed(WorkerDynamicModuleImportError), - JoinedEvaluation, - Fetch, - Evaluation { - evaluation_id: WorkerModuleEvaluationId, - }, -} - -impl WorkerDynamicModuleImportJob { - fn new( - id: u64, - context: v8::Global, - resolver: v8::Global, - specifier: String, - base_url: Url, - fetch_initiator_url: Url, - attributes: ModuleAttributesKey, - phase: ModuleImportPhase, - audio_worklet_bootstrap: bool, - ) -> Self { - Self { - id, - waiting_on: HashSet::new(), - context, - resolver, - specifier, - base_url, - fetch_initiator_url, - attributes, - phase, - audio_worklet_bootstrap, - root_key: None, - resolved_entry: None, - state: WorkerDynamicModuleImportJobState::Graph, - } - } - - fn browser_request_metadata(&self) -> BrowserRequestMetadata { - if self.audio_worklet_bootstrap { - BrowserRequestMetadata::AudioWorklet - } else { - BrowserRequestMetadata::Fetch - } - } -} - -fn advance_worker_dynamic_module_import( - scope: &mut v8::PinScope<'_, '_>, - job: &mut WorkerDynamicModuleImportJob, -) -> WorkerDynamicModuleImportResult { - let context = scope.get_current_context(); - let graph = context - .get_slot::>() - .ok_or_else(|| "worker module graph is not available".to_owned())?; - let module_url = resolve_worker_module_specifier(&job.specifier, &job.base_url)?; - let module_key = worker_module_key_for_attributes(&module_url, &job.attributes) - .map_err(|message| format!("{message} for dynamic import `{}`", job.specifier))?; - if let Some(error) = graph.borrow().compile_error(&module_key) { - return Err(error.into()); - } - 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()); - let inherited_referrer_policy = graph.borrow().referrer_policy_for_url(&job.base_url); - let root_entry = match existing_root_entry { - Some(entry) => entry, - None => match load_worker_static_module_dependency(module_url, module_key.module_type)? { - WorkerModuleDependencyLoad::Source { url, source } => { - let key = - worker_module_key_for_attributes(&url, &job.attributes).map_err(|message| { - format!("{message} for dynamic import `{}`", job.specifier) - })?; - ensure_worker_module_entry( - scope, - &graph, - &source, - key, - url, - inherited_referrer_policy.clone(), - )? - } - WorkerModuleDependencyLoad::NeedFetch(_) => { - let existing_fetch_id = graph.borrow().pending_fetch_id(&module_key); - let fetch_id = existing_fetch_id - .unwrap_or_else(|| reserve_worker_module_graph_fetch_id(scope)); - return Ok(WorkerDynamicModuleImportAdvance::NeedFetches( - WorkerModuleGraphFetchBatch::single(WorkerModuleGraphFetchRequest::new( - fetch_id, - module_key, - job.fetch_initiator_url.clone(), - WorkerModuleGraphFetchCspSource::DynamicImportGraph, - graph.borrow().credentials_mode(), - inherited_referrer_policy, - job.browser_request_metadata(), - )), - )); - } - }, - }; - 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(), - )? { - WorkerModuleGraphBuild::Ready => { - if context - .get_slot::>() - .is_some_and(|imports| imports.borrow().root_evaluation_in_flight(&module_key)) - { - job.state = WorkerDynamicModuleImportJobState::JoinedEvaluation; - Ok(WorkerDynamicModuleImportAdvance::WaitingFetches) - } else { - finish_worker_dynamic_module_import_evaluation(scope, root_entry) - } - } - WorkerModuleGraphBuild::NeedFetches(requests) => { - Ok(WorkerDynamicModuleImportAdvance::NeedFetches(requests)) - } - } -} - fn finish_worker_dynamic_module_import_evaluation( scope: &mut v8::PinScope<'_, '_>, root_entry: usize, -) -> WorkerDynamicModuleImportResult { +) -> WorkerDynamicModuleImportResult { let try_catch = pin!(v8::TryCatch::new(scope)); let mut scope = try_catch.init(); let scope = &mut scope; let context = scope.get_current_context(); let graph = context - .get_slot::>() + .get_slot::>() .ok_or_else(|| "worker module graph is not available".to_owned())?; let root_module_global = graph.borrow().module(root_entry).clone(); let root_module = v8::Local::new(scope, &root_module_global); @@ -1216,8 +1124,6 @@ fn finish_worker_dynamic_module_import_evaluation( ); } } - } else if root_module.get_status() == v8::ModuleStatus::Evaluated { - return Ok(WorkerDynamicModuleImportAdvance::Complete); } else if root_module.get_status() == v8::ModuleStatus::Errored { return Err(WorkerDynamicModuleImportError::evaluation_rejection_value( scope, @@ -1244,7 +1150,7 @@ fn finish_worker_dynamic_module_import_evaluation( } if let Ok(promise) = v8::Local::::try_from(value) { match promise.state() { - v8::PromiseState::Fulfilled => Ok(WorkerDynamicModuleImportAdvance::Complete), + v8::PromiseState::Fulfilled => Ok(WorkerModuleFinish::Complete), v8::PromiseState::Rejected => { let reason = promise.result(scope); Err(WorkerDynamicModuleImportError::evaluation_rejection_value( @@ -1257,34 +1163,22 @@ fn finish_worker_dynamic_module_import_evaluation( let evaluation_id = reserve_worker_module_evaluation_id(scope)?; let promise = v8::Global::new(scope, promise); attach_worker_module_evaluation_reactions(scope, evaluation_id, promise)?; - Ok(WorkerDynamicModuleImportAdvance::WaitingEvaluation { evaluation_id }) + Ok(WorkerModuleFinish::PendingEvaluation { evaluation_id }) } } } else { - Ok(WorkerDynamicModuleImportAdvance::Complete) + Ok(WorkerModuleFinish::Complete) } } fn resolve_worker_dynamic_module_import( scope: &mut v8::PinScope<'_, '_>, job: WorkerDynamicModuleImportJob, -) { - let resolved_entry = job.resolved_entry; - let joined_imports = take_worker_dynamic_module_evaluation_waiters(scope, &job); - resolve_single_worker_dynamic_module_import(scope, job); - for mut joined_job in joined_imports { - joined_job.resolved_entry = resolved_entry; - resolve_single_worker_dynamic_module_import(scope, joined_job); - } -} - -fn resolve_single_worker_dynamic_module_import( - scope: &mut v8::PinScope<'_, '_>, - job: WorkerDynamicModuleImportJob, + entry: usize, ) { let context = v8::Local::new(scope, &job.context); let scope = &mut v8::ContextScope::new(scope, context); - let Some(graph) = context.get_slot::>() else { + let Some(graph) = context.get_slot::>() else { reject_worker_dynamic_module_import( scope, job, @@ -1292,18 +1186,10 @@ fn resolve_single_worker_dynamic_module_import( ); return; }; - let Some(entry) = job.resolved_entry else { - reject_worker_dynamic_module_import( - scope, - job, - WorkerDynamicModuleImportError::type_error("dynamic import module is not compiled"), - ); - return; - }; let resolver = v8::Local::new(scope, &job.resolver); let resolved_value = { let graph = graph.borrow(); - let record = graph.module_map.record(entry); + let record = graph.record(entry); match job.phase { ModuleImportPhase::Evaluation => { let module = v8::Local::new(scope, &record.module); @@ -1346,22 +1232,6 @@ fn resolve_single_worker_dynamic_module_import( crate::context_bootstrap::run_end_of_microtask_checkpoint_tasks(scope); } -fn take_worker_dynamic_module_evaluation_waiters( - scope: &mut v8::PinScope<'_, '_>, - job: &WorkerDynamicModuleImportJob, -) -> Vec { - let Some(root_key) = job.root_key.as_ref() else { - return Vec::new(); - }; - let context = v8::Local::new(scope, &job.context); - let Some(dynamic_imports) = context.get_slot::>() else { - return Vec::new(); - }; - dynamic_imports - .borrow_mut() - .take_evaluation_waiters(root_key) -} - fn reject_worker_dynamic_module_import( scope: &mut v8::PinScope<'_, '_>, job: WorkerDynamicModuleImportJob, @@ -1388,90 +1258,77 @@ fn reject_worker_dynamic_module_import( crate::context_bootstrap::run_end_of_microtask_checkpoint_tasks(scope); } -fn finish_failed_worker_dynamic_module_import( - scope: &mut v8::PinScope<'_, '_>, - job: WorkerDynamicModuleImportJob, - error: WorkerDynamicModuleImportError, -) { - let resolved_entry = job.resolved_entry; - let joined_imports = take_worker_dynamic_module_evaluation_waiters(scope, &job); - let joined_errors = joined_imports - .iter() - .map(|_| error.clone_for_scope(scope)) - .collect::>(); - finish_failed_single_worker_dynamic_module_import(scope, job, error); - for (mut joined_job, joined_error) in joined_imports.into_iter().zip(joined_errors) { - joined_job.resolved_entry = resolved_entry; - finish_failed_single_worker_dynamic_module_import(scope, joined_job, joined_error); +#[derive(Default)] +struct WorkerDynamicModuleResolver { + next_import_id: u64, + pending_imports: VecDeque, + evaluations: HashMap, +} + +impl WorkerDynamicModuleResolver { + fn queue_import( + &mut self, + context: v8::Global, + resolver: v8::Global, + specifier: String, + base_url: Url, + fetch_initiator_url: Url, + attributes: ModuleAttributesKey, + phase: ModuleImportPhase, + audio_worklet_bootstrap: bool, + ) { + self.next_import_id = self + .next_import_id + .checked_add(1) + .expect("worker module tree id space exhausted"); + self.pending_imports + .push_back(WorkerDynamicModuleImportJob { + tree_id: module_tree::ModuleTreeId(self.next_import_id), + context, + resolver, + specifier, + base_url, + fetch_initiator_url, + attributes, + phase, + audio_worklet_bootstrap, + }); } } -fn finish_failed_single_worker_dynamic_module_import( - scope: &mut v8::PinScope<'_, '_>, - job: WorkerDynamicModuleImportJob, - error: WorkerDynamicModuleImportError, -) { - if job.audio_worklet_bootstrap && error.stage == WorkerDynamicModuleImportErrorStage::Evaluate { - resolve_worker_dynamic_module_import(scope, job); - } else { - reject_worker_dynamic_module_import(scope, job, error); - } +struct WorkerDynamicModuleImportJob { + tree_id: module_tree::ModuleTreeId, + context: v8::Global, + resolver: v8::Global, + specifier: String, + base_url: Url, + fetch_initiator_url: Url, + attributes: ModuleAttributesKey, + phase: ModuleImportPhase, + audio_worklet_bootstrap: bool, } -pub(super) struct WorkerModuleBootstrapJob { - runtime: WorkerModuleRuntime, - source: WorkerModuleSource, - root_url: Url, - static_import_initiator_url: Url, -} - -impl WorkerModuleBootstrapJob { - fn script_url(&self) -> &str { - self.root_url.as_str() - } - - fn advance(&mut self, scope: &mut v8::PinScope<'_, '_>) -> WorkerModuleAdvance { - let root_key = WorkerModuleKey::javascript_or_wasm(self.root_url.clone()); - let root_referrer_policy = self - .runtime - .graph - .borrow() - .root_referrer_policy() - .map(str::to_owned); - let root_entry = match ensure_worker_module_entry( - scope, - &self.runtime.graph, - &self.source, - root_key, - self.root_url.clone(), - root_referrer_policy, - ) { - Ok(root_entry) => root_entry, - Err(error) => return WorkerModuleAdvance::Failed(error), - }; - match continue_worker_module_graph( - scope, - &self.runtime.graph, - root_entry, - &self.static_import_initiator_url, - WorkerModuleGraphFetchCspSource::StaticModuleGraph, - BrowserRequestMetadata::Fetch, - ) { - Ok(WorkerModuleGraphBuild::Ready) => { - WorkerModuleAdvance::ReadyToInstantiate { root_entry } - } - Ok(WorkerModuleGraphBuild::NeedFetches(requests)) => { - WorkerModuleAdvance::NeedFetches(requests) - } - Err(error) => WorkerModuleAdvance::Failed(error), +impl WorkerDynamicModuleImportJob { + fn browser_request_metadata(&self) -> BrowserRequestMetadata { + if self.audio_worklet_bootstrap { + BrowserRequestMetadata::AudioWorklet + } else { + BrowserRequestMetadata::Fetch } } } -enum WorkerModuleAdvance { - ReadyToInstantiate { root_entry: usize }, - NeedFetches(WorkerModuleGraphFetchBatch), - Failed(Box), +struct WorkerDynamicModuleEvaluation { + job: WorkerDynamicModuleImportJob, + root_entry: usize, +} + +#[derive(Clone)] +struct WorkerModuleGraphFetchOptions { + initiator_url: Url, + csp_source: WorkerModuleGraphFetchCspSource, + referrer_policy: Option, + browser_request_metadata: BrowserRequestMetadata, } #[derive(Clone)] @@ -1485,36 +1342,6 @@ pub(super) struct WorkerModuleGraphFetchRequest { browser_request_metadata: BrowserRequestMetadata, } -#[derive(Clone)] -pub(super) struct WorkerModuleGraphFetchBatch { - requests: Vec, -} - -impl WorkerModuleGraphFetchBatch { - fn new(requests: Vec) -> Self { - debug_assert!( - !requests.is_empty(), - "worker module graph fetch batch should not be empty" - ); - Self { requests } - } - - fn single(request: WorkerModuleGraphFetchRequest) -> Self { - Self::new(vec![request]) - } - - fn pending_keys(&self) -> HashSet { - self.requests - .iter() - .map(|request| request.key.clone()) - .collect() - } - - pub(super) fn iter(&self) -> impl Iterator { - self.requests.iter() - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum WorkerModuleGraphFetchCspSource { StaticModuleGraph, @@ -1542,6 +1369,10 @@ impl WorkerModuleGraphFetchRequest { } } + pub(super) fn module_key(&self) -> WorkerModuleKey { + self.key.clone() + } + pub(super) fn fetch_id(&self) -> WorkerModuleGraphFetchId { self.fetch_id } @@ -1586,27 +1417,27 @@ fn worker_module_browser_request_metadata( } } -struct WorkerModuleGraph { +struct WorkerModuleMap { credentials_mode: RequestCredentialsMode, root_referrer_policy: Option, - module_map: WorkerModuleMap, + entries: HashMap, + scripts: Vec, + next_fetch_id: WorkerModuleGraphFetchId, + pending_fetch_count: usize, module_to_entries: HashMap>, } struct WorkerModuleRecord { - key: WorkerModuleKey, - source_url: Url, source: WorkerModuleSource, json_value: Option>, module: v8::Global, requests: Vec, dependencies: Vec, wasm_module: Option, - referrer_policy: Option, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct WorkerModuleKey { +pub(super) struct WorkerModuleKey { url: Url, module_type: WorkerModuleType, attributes: ModuleAttributesKey, @@ -1701,110 +1532,30 @@ enum WorkerSyntheticModuleRecord { WebAssembly(WasmModuleRecord), } -impl WorkerModuleGraph { +impl WorkerModuleMap { fn new(credentials_mode: RequestCredentialsMode, root_referrer_policy: Option) -> Self { Self { credentials_mode, root_referrer_policy, - module_map: WorkerModuleMap::default(), + entries: HashMap::new(), + scripts: Vec::new(), + next_fetch_id: 0, + pending_fetch_count: 0, module_to_entries: HashMap::new(), } } - fn credentials_mode(&self) -> RequestCredentialsMode { - self.credentials_mode - } - - fn root_referrer_policy(&self) -> Option<&str> { - self.root_referrer_policy.as_deref() - } - - fn referrer_policy(&self, entry: usize) -> Option<&str> { - self.module_map.record(entry).referrer_policy.as_deref() - } - fn referrer_policy_for_url(&self, url: &Url) -> Option { - self.module_map - .records() - .find(|record| record.key.url == *url || record.source_url == *url) - .and_then(|record| record.referrer_policy.clone()) + self.scripts + .iter() + .filter(|script| script.creation.is_ok()) + .find(|script| script.key.url == *url || script.response_url == *url) + .and_then(|script| script.referrer_policy.clone()) .or_else(|| self.root_referrer_policy.clone()) } - fn entry_for_key(&self, key: &WorkerModuleKey) -> Option { - self.module_map.compiled_entry(key) - } - - fn pending_fetch_id(&self, key: &WorkerModuleKey) -> Option { - self.module_map.fetch_id(key) - } - - fn compile_error(&self, key: &WorkerModuleKey) -> Option> { - self.module_map.compile_error(key) - } - - fn insert( - &mut self, - key: WorkerModuleKey, - source_url: Url, - source: WorkerModuleSource, - json_value: Option>, - module: v8::Global, - identity_hash: i32, - requests: Vec, - wasm_module: Option, - referrer_policy: Option, - ) -> usize { - let entry = self.module_map.insert_compiled(WorkerModuleRecord { - key, - source_url, - source, - json_value, - module, - requests, - dependencies: Vec::new(), - wasm_module, - referrer_policy, - }); - self.module_to_entries - .entry(identity_hash) - .or_default() - .push(entry); - entry - } - fn module(&self, entry: usize) -> &v8::Global { - &self.module_map.record(entry).module - } - - fn has_top_level_await(&self, scope: &mut v8::PinScope<'_, '_>, root_entry: usize) -> bool { - let mut pending = vec![root_entry]; - let mut seen = HashSet::new(); - while let Some(entry) = pending.pop() { - if !seen.insert(entry) { - continue; - } - let module = v8::Local::new(scope, self.module(entry)); - if module.has_top_level_await() { - return true; - } - for request in &self.module_map.record(entry).requests { - if request.phase == ModuleImportPhase::Evaluation - && let Some(dependency) = self.resolve_static_dependency_entry( - module, - &request.specifier, - &request.attributes, - ) - { - pending.push(dependency); - } - } - } - false - } - - fn requests(&self, entry: usize) -> Vec { - self.module_map.record(entry).requests.clone() + &self.record(entry).module } fn add_dependency( @@ -1814,8 +1565,7 @@ impl WorkerModuleGraph { attributes: ModuleAttributesKey, target_entry: usize, ) { - self.module_map - .record_mut(entry) + self.record_mut(entry) .dependencies .push(WorkerModuleDependency { specifier, @@ -1824,18 +1574,13 @@ impl WorkerModuleGraph { }); } - fn url(&self, entry: usize) -> &Url { - &self.module_map.record(entry).source_url - } - fn dependency_entry( &self, entry: usize, specifier: &str, attributes: &ModuleAttributesKey, ) -> Option { - self.module_map - .record(entry) + self.record(entry) .dependencies .iter() .find(|dependency| { @@ -1846,7 +1591,7 @@ impl WorkerModuleGraph { fn module_url_for(&self, module: v8::Local<'_, v8::Module>) -> Option { let entry = self.entry_for_module(module)?; - Some(self.module_map.record(entry).source_url.clone()) + Some(self.script(entry).response_url.clone()) } fn wasm_instance_for_namespace<'s>( @@ -1854,7 +1599,11 @@ impl WorkerModuleGraph { scope: &mut v8::PinScope<'s, '_>, namespace: v8::Local<'s, v8::Object>, ) -> Option> { - for record in self.module_map.records() { + for record in self + .scripts + .iter() + .filter_map(|script| script.creation.as_ref().ok()) + { let Some(wasm_record) = record.wasm_module.as_ref() else { continue; }; @@ -1880,11 +1629,11 @@ impl WorkerModuleGraph { module: v8::Local<'_, v8::Module>, ) -> Option { let entry = self.entry_for_module(module)?; - let record = self.module_map.record(entry); + let record = self.record(entry); if let Some(wasm) = &record.wasm_module { return Some(WorkerSyntheticModuleRecord::WebAssembly(wasm.clone())); } - match record.key.module_type { + match self.script(entry).key.module_type { WorkerModuleType::Json => record .json_value .clone() @@ -1915,7 +1664,7 @@ impl WorkerModuleGraph { ) -> Option<&WorkerModuleRecord> { let dependency_entry = self.resolve_static_dependency_entry(referrer, specifier, attributes)?; - Some(self.module_map.record(dependency_entry)) + Some(self.record(dependency_entry)) } fn resolve_static_dependency( @@ -1937,259 +1686,53 @@ impl WorkerModuleGraph { .get(&identity_hash)? .iter() .copied() - .find(|entry| module == self.module_map.record(*entry).module) + .find(|entry| module == self.record(*entry).module) } } type WorkerModuleBootstrapResult = Result>; -enum WorkerModuleGraphBuild { - Ready, - NeedFetches(WorkerModuleGraphFetchBatch), -} - -fn cache_worker_module_fetched_source( +fn create_worker_module_script( scope: &mut v8::PinScope<'_, '_>, - graph: &Rc>, - request: &WorkerModuleGraphFetchRequest, - source: &WorkerModuleFetchedSource, -) -> WorkerModuleBootstrapResult { - let referrer_policy = source.effective_referrer_policy(request.referrer_policy()); - ensure_worker_module_entry( - scope, - graph, - source.source(), - request.key.clone(), - source.final_url().clone(), - referrer_policy, - ) -} - -fn ensure_worker_module_entry( - scope: &mut v8::PinScope<'_, '_>, - graph: &Rc>, + modules: &Rc>, source: &WorkerModuleSource, key: WorkerModuleKey, - source_url: Url, + response_url: Url, referrer_policy: Option, -) -> WorkerModuleBootstrapResult { - if let Some(error) = graph.borrow().compile_error(&key) { - return Err(error); +) -> usize { + if let Some(entry) = modules.borrow().loaded_entry(&key) { + return entry; } - let existing_entry = graph.borrow().entry_for_key(&key); - if let Some(entry) = existing_entry { - return Ok(entry); - } - let (module, identity_hash, requests, wasm_module, json_value) = - compile_worker_module_record(scope, source, &key, &source_url).inspect_err(|error| { - graph - .borrow_mut() - .module_map - .insert_parse_error(key.clone(), error.clone()); - })?; - Ok(graph.borrow_mut().insert( + let compiled = compile_worker_module_record(scope, source, &key, &response_url); + let (creation, identity_hash) = match compiled { + Ok((module, identity_hash, requests, wasm_module, json_value)) => ( + Ok(WorkerModuleRecord { + source: source.clone(), + json_value, + module, + requests, + dependencies: Vec::new(), + wasm_module, + }), + Some(identity_hash), + ), + Err(error) => (Err(error), None), + }; + let mut modules = modules.borrow_mut(); + let entry = modules.insert_script(WorkerModuleScript { key, - source_url, - source.clone(), - json_value, - module, - identity_hash, - requests, - wasm_module, + response_url, referrer_policy, - )) -} - -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, -) -> WorkerModuleBootstrapResult { - let mut pending_keys = HashSet::new(); - // 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 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() - .dependency_entry(entry, &request.specifier, &request.attributes) - .is_none() - { - 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); - } - } + creation, + }); + if let Some(identity_hash) = identity_hash { + modules + .module_to_entries + .entry(identity_hash) + .or_default() + .push(entry); } - if !pending_requests.is_empty() { - return Ok(WorkerModuleGraphBuild::NeedFetches( - WorkerModuleGraphFetchBatch::new(pending_requests), - )); - } - Ok(WorkerModuleGraphBuild::Ready) -} - -fn resolve_worker_module_dependency( - scope: &mut v8::PinScope<'_, '_>, - graph: &Rc>, - entry: usize, - url: &Url, - fetch_initiator_url: &Url, - csp_source: WorkerModuleGraphFetchCspSource, - browser_request_metadata: BrowserRequestMetadata, - request: &WorkerModuleRequest, - pending_keys: &mut HashSet, -) -> WorkerModuleBootstrapResult { - let dependency_url = - resolve_worker_module_specifier(&request.specifier, url).map_err(|error| { - Box::new(worker_bootstrap_error( - scope, - url.as_str(), - &format!( - "Failed to resolve module worker dependency `{}`: {error}", - request.specifier - ), - WorkerParentErrorEventKind::Event, - )) - })?; - let dependency_key = worker_module_key_for_attributes(&dependency_url, &request.attributes) - .map_err(|message| { - Box::new(worker_bootstrap_error( - scope, - url.as_str(), - &format!("{message} for import `{}`", request.specifier), - WorkerParentErrorEventKind::Event, - )) - })?; - if let Some(error) = graph.borrow().compile_error(&dependency_key) { - return Err(error); - } - let (dependency_key, dependency_source) = - match load_worker_static_module_dependency(dependency_url, dependency_key.module_type) - .map_err(|message| { - Box::new(worker_bootstrap_error( - scope, - url.as_str(), - &message, - WorkerParentErrorEventKind::Event, - )) - })? { - WorkerModuleDependencyLoad::Source { url, source } => { - let dependency_key = worker_module_key_for_attributes(&url, &request.attributes) - .map_err(|message| { - Box::new(worker_bootstrap_error( - scope, - url.as_str(), - &format!("{message} for import `{}`", request.specifier), - WorkerParentErrorEventKind::Event, - )) - })?; - (dependency_key, source) - } - WorkerModuleDependencyLoad::NeedFetch(dependency_url) => { - let dependency_key = - worker_module_key_for_attributes(&dependency_url, &request.attributes) - .map_err(|message| { - Box::new(worker_bootstrap_error( - scope, - url.as_str(), - &format!("{message} for import `{}`", request.specifier), - WorkerParentErrorEventKind::Event, - )) - })?; - let existing_entry = graph.borrow().entry_for_key(&dependency_key); - if let Some(target_entry) = existing_entry { - graph.borrow_mut().add_dependency( - entry, - request.specifier.clone(), - request.attributes.clone(), - target_entry, - ); - return Ok(WorkerModuleGraphBuild::Ready); - } - if !pending_keys.insert(dependency_key.clone()) { - return Ok(WorkerModuleGraphBuild::Ready); - } - let existing_fetch_id = graph.borrow().pending_fetch_id(&dependency_key); - let fetch_id = existing_fetch_id - .unwrap_or_else(|| reserve_worker_module_graph_fetch_id(scope)); - let referrer_policy = graph.borrow().referrer_policy(entry).map(str::to_owned); - return Ok(WorkerModuleGraphBuild::NeedFetches( - WorkerModuleGraphFetchBatch::single(WorkerModuleGraphFetchRequest::new( - fetch_id, - dependency_key, - fetch_initiator_url.clone(), - csp_source, - graph.borrow().credentials_mode(), - referrer_policy, - browser_request_metadata, - )), - )); - } - }; - let referrer_policy = graph.borrow().referrer_policy(entry).map(str::to_owned); - let target_entry = ensure_worker_module_entry( - scope, - graph, - &dependency_source, - dependency_key.clone(), - dependency_key.url.clone(), - referrer_policy, - )?; - graph.borrow_mut().add_dependency( - entry, - request.specifier.clone(), - request.attributes.clone(), - target_entry, - ); - Ok(WorkerModuleGraphBuild::Ready) -} - -fn reserve_worker_module_graph_fetch_id( - scope: &mut v8::PinScope<'_, '_>, -) -> WorkerModuleGraphFetchId { - let slot = scope - .get_current_context() - .get_slot::() - .expect("worker module runtime fetch id slot should be installed"); - let mut next_fetch_id = slot.next_fetch_id.borrow_mut(); - let fetch_id = *next_fetch_id; - *next_fetch_id = next_fetch_id - .checked_add(1) - .expect("worker module graph fetch id space exhausted"); - fetch_id + entry } fn reserve_worker_module_evaluation_id( @@ -2391,15 +1934,17 @@ fn compile_worker_wasm_module_record( )) })? .ok_or_else(|| { - let exception = v8_exception_message_or( - &scope, - scope.exception(), - "unknown wasm compile exception", + let exception = scope.exception(); + let message = scope.message(); + let mut report = build_exception_report_without_stack(&mut scope, exception, message); + report.summary = format!( + "v8 failed to compile WebAssembly module `{source_url}`: {}", + report.summary ); - Box::new(worker_bootstrap_error( - &mut scope, - source_url.as_str(), - &format!("v8 failed to compile WebAssembly module `{source_url}`: {exception}"), + report.source.get_or_insert_with(|| source_url.to_string()); + Box::new(( + report, + exception.map(|value| v8::Global::new(&scope, value)), WorkerParentErrorEventKind::Event, )) })?; @@ -2525,7 +2070,7 @@ fn load_worker_static_module_dependency( source, }) } - "http" | "https" => Ok(WorkerModuleDependencyLoad::NeedFetch(dependency_url)), + "http" | "https" => Ok(WorkerModuleDependencyLoad::Network), scheme => Err(format!( "Module worker dependency scheme `{scheme}` is not allowed" )), @@ -2555,11 +2100,7 @@ enum WorkerModuleDependencyLoad { url: Url, source: WorkerModuleSource, }, - NeedFetch(Url), -} - -struct WorkerModuleRuntimeFetchIdSlot { - next_fetch_id: Rc>, + Network, } struct WorkerModuleRuntimeEvaluationSlot { @@ -2624,7 +2165,7 @@ fn worker_resolve_static_module_callback<'s>( referrer: v8::Local<'s, v8::Module>, ) -> Option> { v8::callback_scope!(unsafe scope, context); - let graph = context.get_slot::>()?; + let graph = context.get_slot::>()?; let specifier = specifier.to_rust_string_lossy(scope); let attributes = worker_import_attributes_key(scope, import_attributes); let graph = graph.borrow(); @@ -2639,7 +2180,7 @@ fn worker_resolve_static_source_callback<'s>( referrer: v8::Local<'s, v8::Module>, ) -> Option> { v8::callback_scope!(unsafe scope, context); - let graph = context.get_slot::>()?; + let graph = context.get_slot::>()?; let specifier = specifier.to_rust_string_lossy(scope); let attributes = worker_import_attributes_key(scope, import_attributes); let graph = graph.borrow(); @@ -2700,11 +2241,7 @@ fn worker_module_evaluation_rejected_callback<'s>( let Some(evaluation_id) = worker_module_evaluation_reaction_id(scope, args.data()) else { return; }; - let reason = args - .get(0) - .to_string(scope) - .map(|value| value.to_rust_string_lossy(scope)) - .unwrap_or_else(|| "unknown module worker top-level await rejection".to_owned()); + let reason = v8::Global::new(scope, args.get(0)); let context = scope.get_current_context(); let Some(slot) = context.get_slot::() else { return; @@ -2734,7 +2271,7 @@ pub(super) unsafe extern "C" fn worker_initialize_import_meta_object_callback( ) { v8::callback_scope!(unsafe scope, context); let module_url = context - .get_slot::>() + .get_slot::>() .and_then(|graph| graph.borrow().module_url_for(module)) .or_else(|| worker_current_script_url(scope)); let Some(module_url) = module_url else { return }; @@ -2750,7 +2287,7 @@ pub(crate) fn worker_wasm_instance_for_namespace<'s>( ) -> Option> { let graph = scope .get_current_context() - .get_slot::>()?; + .get_slot::>()?; graph.borrow().wasm_instance_for_namespace(scope, namespace) } @@ -2801,7 +2338,7 @@ fn worker_synthetic_module_evaluation_steps<'s>( ) -> Option> { v8::callback_scope!(unsafe scope, context); let synthetic = context - .get_slot::>() + .get_slot::>() .and_then(|graph| graph.borrow().synthetic_module_record_for(module))?; match synthetic { WorkerSyntheticModuleRecord::Text(source) => { @@ -2845,7 +2382,7 @@ fn worker_wasm_import_value<'s>( ) -> Option> { let Some(graph) = scope .get_current_context() - .get_slot::>() + .get_slot::>() else { return throw_wasm_link_error(scope, "worker wasm import graph is not available"); }; @@ -2858,8 +2395,8 @@ fn worker_wasm_import_value<'s>( return throw_wasm_link_error(scope, "worker wasm import dependency is not available"); }; ( - graph.module_map.record(entry).module.clone(), - graph.module_map.record(entry).wasm_module.clone(), + graph.record(entry).module.clone(), + graph.record(entry).wasm_module.clone(), ) }; let dependency = v8::Local::new(scope, &dependency_module); @@ -2876,7 +2413,7 @@ fn worker_wasm_import_value<'s>( fn ensure_worker_dependency_module_namespace_ready<'s>( scope: &mut v8::PinScope<'s, '_>, - graph: &RefCell, + graph: &RefCell, module: v8::Local<'s, v8::Module>, ) -> Option<()> { let mut dependency_modules_for = @@ -2884,7 +2421,6 @@ fn ensure_worker_dependency_module_namespace_ready<'s>( let graph = graph.borrow(); let entry = graph.entry_for_module(dependency)?; graph - .module_map .record(entry) .requests .iter() diff --git a/moli-renderer-v8/src/worker/module_runtime/module_map.rs b/moli-renderer-v8/src/worker/module_runtime/module_map.rs index a196375fe1..80ee8d8fe4 100644 --- a/moli-renderer-v8/src/worker/module_runtime/module_map.rs +++ b/moli-renderer-v8/src/worker/module_runtime/module_map.rs @@ -1,166 +1,204 @@ -use std::collections::{HashMap, HashSet, hash_map::Entry}; +use std::collections::hash_map::Entry; use super::{ - WorkerBootstrapError, WorkerModuleGraphFetchBatch, WorkerModuleGraphFetchId, - WorkerModuleGraphFetchRequest, WorkerModuleKey, WorkerModuleRecord, + WorkerBootstrapError, WorkerModuleGraphFetchId, WorkerModuleGraphFetchOptions, + WorkerModuleGraphFetchRequest, WorkerModuleKey, WorkerModuleMap, WorkerModuleRecord, + module_tree, }; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub(super) enum WorkerModuleWaiter { - Bootstrap, - DynamicImport(u64), -} - -enum WorkerModuleMapEntry { +pub(super) enum WorkerModuleMapEntry { Fetching { request: Box, - waiters: HashSet, + clients: Vec, }, - Compiled { - entry: usize, - module: Box, - }, - ParseError(Box), + Loaded(usize), } -#[derive(Default)] -pub(super) struct WorkerModuleMap { - entries: HashMap, - fetch_keys: HashMap, - // Stable graph indices survive transitions of unrelated module-map entries. - compiled_keys: Vec, +pub(super) struct WorkerModuleScript { + pub(super) key: WorkerModuleKey, + pub(super) response_url: url::Url, + pub(super) referrer_policy: Option, + pub(super) creation: Result>, } impl WorkerModuleMap { - pub(super) fn compiled_entry(&self, key: &WorkerModuleKey) -> Option { + pub(super) fn loaded_entry(&self, key: &WorkerModuleKey) -> Option { match self.entries.get(key)? { - WorkerModuleMapEntry::Compiled { entry, .. } => Some(*entry), - _ => None, + WorkerModuleMapEntry::Loaded(entry) => Some(*entry), + WorkerModuleMapEntry::Fetching { .. } => None, } } - pub(super) fn compile_error(&self, key: &WorkerModuleKey) -> Option> { - match self.entries.get(key)? { - WorkerModuleMapEntry::ParseError(error) => Some(error.clone()), - _ => None, - } - } - - pub(super) fn fetch_id(&self, key: &WorkerModuleKey) -> Option { - match self.entries.get(key)? { - WorkerModuleMapEntry::Fetching { request, .. } => Some(request.fetch_id), - _ => None, - } - } - - // Subscribe only after graph discovery succeeds. Each physical fetch is - // admitted once; importing jobs keep keys, never copies of these requests. - pub(super) fn subscribe( + pub(super) fn start_or_join_fetch( &mut self, - waiter: WorkerModuleWaiter, - requests: WorkerModuleGraphFetchBatch, - ) -> Option { - let mut new_requests = Vec::new(); - for request in requests.requests { - match self.entries.entry(request.key.clone()) { - Entry::Vacant(entry) => { - self.fetch_keys - .insert(request.fetch_id, request.key.clone()); - entry.insert(WorkerModuleMapEntry::Fetching { - request: Box::new(request.clone()), - waiters: HashSet::from([waiter]), - }); - new_requests.push(request); + key: WorkerModuleKey, + client: module_tree::SingleModuleClientToken, + options: &WorkerModuleGraphFetchOptions, + outbox: &mut Vec, + ) -> module_tree::SingleModuleFetchDisposition { + match self.entries.entry(key.clone()) { + Entry::Occupied(mut entry) => match entry.get_mut() { + WorkerModuleMapEntry::Loaded(entry) => { + let entry = *entry; + module_tree::SingleModuleFetchDisposition::Completed(super::tree::ready_module( + self, entry, + )) } - Entry::Occupied(mut entry) => { - let WorkerModuleMapEntry::Fetching { waiters, .. } = entry.get_mut() else { - unreachable!("only unresolved module keys can subscribe to a fetch"); - }; - waiters.insert(waiter); + WorkerModuleMapEntry::Fetching { clients, .. } => { + if !clients.contains(&client) { + clients.push(client); + } + module_tree::SingleModuleFetchDisposition::JoinedExistingFetch + } + }, + Entry::Vacant(entry) => { + let fetch_id = self.next_fetch_id; + self.next_fetch_id = fetch_id + .checked_add(1) + .expect("worker module fetch id space exhausted"); + let request = WorkerModuleGraphFetchRequest::new( + fetch_id, + key, + options.initiator_url.clone(), + options.csp_source, + self.credentials_mode, + options.referrer_policy.clone(), + options.browser_request_metadata, + ); + entry.insert(WorkerModuleMapEntry::Fetching { + request: Box::new(request.clone()), + clients: vec![client], + }); + self.pending_fetch_count += 1; + // The runtime owns the action even if this tree fails before + // returning NeedFetches. A fetch entry never depends on a job + // retaining and forwarding its transport request. + outbox.push(request); + module_tree::SingleModuleFetchDisposition::StartedNetworkFetch { + fetch_id: module_tree::ModuleFetchId(fetch_id), } - } - } - (!new_requests.is_empty()).then(|| WorkerModuleGraphFetchBatch::new(new_requests)) - } - - pub(super) fn unsubscribe( - &mut self, - waiter: WorkerModuleWaiter, - keys: impl IntoIterator, - ) { - for key in keys { - if let Some(WorkerModuleMapEntry::Fetching { waiters, .. }) = self.entries.get_mut(&key) - { - waiters.remove(&waiter); } } } pub(super) fn take_fetch( &mut self, + key: &WorkerModuleKey, fetch_id: WorkerModuleGraphFetchId, - ) -> Option<(WorkerModuleGraphFetchRequest, HashSet)> { - let key = self.fetch_keys.remove(&fetch_id)?; - let Some(WorkerModuleMapEntry::Fetching { request, waiters }) = self.entries.remove(&key) + ) -> Option<( + WorkerModuleGraphFetchRequest, + Vec, + )> { + if !matches!(self.entries.get(key), Some(WorkerModuleMapEntry::Fetching { request, .. }) if request.fetch_id == fetch_id) + { + return None; + } + let Some(WorkerModuleMapEntry::Fetching { request, clients }) = self.entries.remove(key) else { - unreachable!("fetch index must identify a fetching module-map entry"); + unreachable!("checked the exact module fetch above"); }; - // Load failures leave this key absent, permitting a later explicit - // import to retry. Successful fetches replace it with Compiled or - // ParseError before any of these waiters are woken. - Some((*request, waiters)) + self.pending_fetch_count -= 1; + Some((*request, clients)) + } + + pub(super) fn detach_tree(&mut self, tree_id: module_tree::ModuleTreeId) { + for entry in self.entries.values_mut() { + if let WorkerModuleMapEntry::Fetching { clients, .. } = entry { + clients.retain(|client| client.tree_id != tree_id); + } + } } pub(super) fn has_pending_fetches(&self) -> bool { - !self.fetch_keys.is_empty() + self.pending_fetch_count != 0 } - pub(super) fn insert_compiled(&mut self, module: WorkerModuleRecord) -> usize { - let entry = self.compiled_keys.len(); - self.compiled_keys.push(module.key.clone()); - self.entries.insert( - module.key.clone(), - WorkerModuleMapEntry::Compiled { - entry, - module: Box::new(module), - }, - ); + pub(super) fn insert_script(&mut self, script: WorkerModuleScript) -> usize { + let entry = self.scripts.len(); + self.entries + .insert(script.key.clone(), WorkerModuleMapEntry::Loaded(entry)); + self.scripts.push(script); entry } - pub(super) fn insert_parse_error( - &mut self, - key: WorkerModuleKey, - error: Box, - ) { - self.entries - .insert(key, WorkerModuleMapEntry::ParseError(error)); + pub(super) fn script(&self, entry: usize) -> &WorkerModuleScript { + &self.scripts[entry] } pub(super) fn record(&self, entry: usize) -> &WorkerModuleRecord { - let WorkerModuleMapEntry::Compiled { module, .. } = - &self.entries[&self.compiled_keys[entry]] - else { - unreachable!("compiled graph index must identify a compiled module"); - }; - module + self.scripts[entry].creation.as_ref().unwrap_or_else(|_| { + unreachable!("only successfully created module scripts have V8 records") + }) } pub(super) fn record_mut(&mut self, entry: usize) -> &mut WorkerModuleRecord { - let Some(WorkerModuleMapEntry::Compiled { module, .. }) = - self.entries.get_mut(&self.compiled_keys[entry]) + self.scripts[entry].creation.as_mut().unwrap_or_else(|_| { + unreachable!("only successfully created module scripts have V8 records") + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::worker::module_runtime::WorkerModuleGraphFetchCspSource; + use module_tree::{ModuleTreeId, SingleModuleClientToken, SingleModuleFetchDisposition}; + use moli_fetch::{BrowserRequestMetadata, RequestCredentialsMode}; + + #[test] + fn late_attempt_completion_cannot_settle_a_retry_or_retire_its_clients() { + let url = url::Url::parse("https://example.test/module.mjs").unwrap(); + let key = WorkerModuleKey::javascript_or_wasm(url.clone()); + let options = WorkerModuleGraphFetchOptions { + initiator_url: url, + csp_source: WorkerModuleGraphFetchCspSource::DynamicImportGraph, + referrer_policy: None, + browser_request_metadata: BrowserRequestMetadata::Fetch, + }; + let client = |id| SingleModuleClientToken { + tree_id: ModuleTreeId(id), + sequence: 0, + }; + let mut modules = WorkerModuleMap::new(RequestCredentialsMode::SameOrigin, None); + let mut outbox = Vec::new(); + let SingleModuleFetchDisposition::StartedNetworkFetch { fetch_id: first } = + modules.start_or_join_fetch(key.clone(), client(1), &options, &mut outbox) else { - unreachable!("compiled graph index must identify a compiled module"); + panic!("first attempt should start a request"); }; - module - } - - pub(super) fn records(&self) -> impl Iterator { - self.compiled_keys - .iter() - .map(|key| match &self.entries[key] { - WorkerModuleMapEntry::Compiled { module, .. } => module.as_ref(), - _ => unreachable!("compiled graph index must identify a compiled module"), - }) + let (_, original_clients) = modules.take_fetch(&key, first.0).unwrap(); + assert_eq!(original_clients, [client(1)]); + assert!(!modules.has_pending_fetches()); + let SingleModuleFetchDisposition::StartedNetworkFetch { fetch_id: retry } = + modules.start_or_join_fetch(key.clone(), client(2), &options, &mut outbox) + else { + panic!("a later explicit import should retry the failed load"); + }; + assert!(modules.take_fetch(&key, first.0).is_none()); + assert!(modules.has_pending_fetches()); + assert_eq!( + modules.start_or_join_fetch(key.clone(), client(3), &options, &mut outbox), + SingleModuleFetchDisposition::JoinedExistingFetch + ); + modules.detach_tree(ModuleTreeId(2)); + modules.detach_tree(ModuleTreeId(3)); + assert!( + modules.has_pending_fetches(), + "retiring trees does not orphan their physical request" + ); + assert_eq!( + modules.start_or_join_fetch(key.clone(), client(4), &options, &mut outbox), + SingleModuleFetchDisposition::JoinedExistingFetch + ); + let (_, retry_clients) = modules.take_fetch(&key, retry.0).unwrap(); + assert_eq!(retry_clients, [client(4)]); + assert_eq!( + original_clients, + [client(1)], + "old failure recipients remain immutable" + ); + assert_eq!(outbox.len(), 2, "one network action per attempt"); + assert!(modules.take_fetch(&key, retry.0).is_none()); + assert!(!modules.has_pending_fetches()); } } diff --git a/moli-renderer-v8/src/worker/module_runtime/tree.rs b/moli-renderer-v8/src/worker/module_runtime/tree.rs new file mode 100644 index 0000000000..eeec59c38b --- /dev/null +++ b/moli-renderer-v8/src/worker/module_runtime/tree.rs @@ -0,0 +1,367 @@ +use super::*; + +pub(super) const BOOTSTRAP_TREE: module_tree::ModuleTreeId = module_tree::ModuleTreeId(0); + +pub(super) enum WorkerModuleTreeTarget { + Bootstrap, + Import(Box), +} + +pub(super) struct WorkerModuleTreeTask { + pub(super) tree: module_tree::ModuleScriptTreeJob, + pub(super) target: WorkerModuleTreeTarget, + pub(super) options: WorkerModuleGraphFetchOptions, +} + +pub(super) struct WorkerSingleModuleCompletion { + pub(super) client: module_tree::SingleModuleClientToken, + pub(super) result: Result, +} + +#[derive(Default)] +pub(super) struct WorkerModuleTrees { + pub(super) tasks: HashMap, + pub(super) notifications: VecDeque, + pub(super) fetches_to_start: Vec, +} + +pub(super) struct WorkerModuleTreeHost<'a, 's, 'i> { + pub(super) scope: &'a mut v8::PinScope<'s, 'i>, + pub(super) modules: Rc>, + pub(super) work: Rc>, + pub(super) options: &'a WorkerModuleGraphFetchOptions, +} + +impl module_tree::ModuleScriptTreeHost for WorkerModuleTreeHost<'_, '_, '_> { + fn resolve_module_request( + &mut self, + specifier: &str, + base_url: &Url, + attributes: &module_tree::ModuleAttributesKey, + requested_phase: module_tree::ModuleImportPhase, + ) -> Result { + let url = resolve_worker_module_specifier(specifier, base_url).map_err(|message| { + module_tree::ModuleLoadError::new(module_tree::ModuleLoadStage::Resolve, message) + })?; + let native_attributes = ModuleAttributesKey::from_pairs(attributes.attributes.clone()); + let key = + worker_module_key_for_attributes(&url, &native_attributes).map_err(|message| { + module_tree::ModuleLoadError::new(module_tree::ModuleLoadStage::Resolve, message) + })?; + Ok(module_tree::ResolvedModuleRequest { + key: tree_key(&key), + source_url: url.clone(), + base_url: url, + kind: tree_kind(key.module_type), + attributes: attributes.clone(), + phase: requested_phase, + integrity: None, + }) + } + + fn start_or_join_single_module_fetch( + &mut self, + request: module_tree::ModuleFetchRequest, + client: module_tree::SingleModuleClientToken, + ) -> module_tree::SingleModuleFetchDisposition { + let key = match native_key(&request.key) { + Ok(key) => key, + Err(error) => { + return module_tree::SingleModuleFetchDisposition::Completed( + module_tree::ModuleFetchOutcome::Failed(error), + ); + } + }; + let existing = self.modules.borrow().entries.contains_key(&key); + if !existing { + match load_worker_static_module_dependency(key.url.clone(), key.module_type) { + Ok(WorkerModuleDependencyLoad::Source { url, source }) => { + let entry = create_worker_module_script( + self.scope, + &self.modules, + &source, + key, + url, + native_referrer_policy(request.fetch_metadata.referrer_policy) + .map(str::to_owned), + ); + return module_tree::SingleModuleFetchDisposition::Completed(ready_module( + &self.modules.borrow(), + entry, + )); + } + Ok(WorkerModuleDependencyLoad::Network) => {} + Err(message) => { + return module_tree::SingleModuleFetchDisposition::Completed( + module_tree::ModuleFetchOutcome::Failed( + module_tree::ModuleLoadError::new( + module_tree::ModuleLoadStage::Fetch, + message, + ) + .with_key(request.key), + ), + ); + } + } + } + let mut options = self.options.clone(); + options.referrer_policy = + native_referrer_policy(request.fetch_metadata.referrer_policy).map(str::to_owned); + // Keep the Worker inside-settings initiator. The common tree's parent + // URL is for module resolution, not for Worker fetch security checks. + self.modules.borrow_mut().start_or_join_fetch( + key, + client, + &options, + &mut self.work.borrow_mut().fetches_to_start, + ) + } + + fn compile_module_source( + &mut self, + source: module_tree::FetchedModuleSource, + phase: module_tree::ModuleImportPhase, + ) -> Result { + let key = native_key(&source.request_key)?; + let native_source = match source.source { + module_tree::ModuleSource::Text(text) => WorkerModuleSource::text(text.to_string()), + module_tree::ModuleSource::Binary(bytes) => WorkerModuleSource::binary(bytes.to_vec()), + }; + let entry = create_worker_module_script( + self.scope, + &self.modules, + &native_source, + key, + source.source_url, + native_referrer_policy(source.effective_fetch_metadata.referrer_policy) + .map(str::to_owned), + ); + let modules = self.modules.borrow(); + let script = modules.script(entry); + Ok(module_tree::CompiledModuleSnapshot { + entry: entry_id(entry), + key: tree_key(&script.key), + base_url: script.response_url.clone(), + effective_fetch_metadata: fetch_metadata( + modules.credentials_mode, + script.referrer_policy.as_deref(), + ), + requested_modules: script + .creation + .as_ref() + .map_or_else(|_| Vec::new(), |record| tree_requests(&record.requests)), + phase, + }) + } + + fn module_creation_error( + &self, + entry: module_tree::ModuleEntryId, + ) -> Option { + creation_error(&self.modules.borrow(), entry.0 as usize) + } + + fn module_dependencies( + &self, + entry: module_tree::ModuleEntryId, + ) -> Result { + let modules = self.modules.borrow(); + let script = modules.script(entry.0 as usize); + let record = script.creation.as_ref().map_err(|_| { + creation_error(&modules, entry.0 as usize).expect("stored creation failure") + })?; + Ok(module_tree::ModuleDependencySnapshot { + entry, + key: tree_key(&script.key), + base_url: script.response_url.clone(), + effective_fetch_metadata: fetch_metadata( + modules.credentials_mode, + script.referrer_policy.as_deref(), + ), + requested_modules: tree_requests(&record.requests), + }) + } + + fn link_module_graph( + &mut self, + root: module_tree::ModuleEntryId, + entries: &[module_tree::ModuleEntryId], + edges: &[module_tree::ModuleDependencyEdge], + ) -> Result { + let mut modules = self.modules.borrow_mut(); + for edge in edges { + let key = native_key(&edge.child_key)?; + let child = modules + .loaded_entry(&key) + .expect("tree links only created module scripts"); + let parent = edge.parent_entry.0 as usize; + let attributes = ModuleAttributesKey::from_pairs(edge.attributes.attributes.clone()); + if modules + .dependency_entry(parent, &edge.specifier, &attributes) + .is_none() + { + modules.add_dependency(parent, edge.specifier.clone(), attributes, child); + } + } + Ok(module_tree::ModuleGraphHandle { + root_entry: root, + entries: entries.to_vec(), + entry_phases: HashMap::new(), + dependency_edges: edges.to_vec(), + }) + } + + fn mark_module_failed( + &mut self, + key: module_tree::ModuleMapKey, + error: module_tree::ModuleLoadError, + ) -> module_tree::ModuleEntryId { + let key = native_key(&key) + .unwrap_or_else(|_| WorkerModuleKey::javascript_or_wasm(key.url.clone())); + if let Some(entry) = self.modules.borrow().loaded_entry(&key) { + return entry_id(entry); + } + let failure = Box::new(worker_bootstrap_error( + self.scope, + key.url.as_str(), + &error.message, + WorkerParentErrorEventKind::Event, + )); + let script = WorkerModuleScript { + response_url: key.url.clone(), + key, + referrer_policy: None, + creation: Err(failure), + }; + entry_id(self.modules.borrow_mut().insert_script(script)) + } +} + +pub(super) fn creation_error( + modules: &WorkerModuleMap, + entry: usize, +) -> Option { + let script = modules.script(entry); + let error = script.creation.as_ref().err()?; + Some( + module_tree::ModuleLoadError::new( + module_tree::ModuleLoadStage::Compile, + error.0.summary.clone(), + ) + .with_key(tree_key(&script.key)) + .with_module_entry(entry_id(entry)), + ) +} + +pub(super) fn ready_module( + modules: &WorkerModuleMap, + entry: usize, +) -> module_tree::ModuleFetchOutcome { + let script = modules.script(entry); + module_tree::ModuleFetchOutcome::Ready(Box::new(module_tree::ReadyModule::new( + entry_id(entry), + tree_key(&script.key), + script.response_url.clone(), + fetch_metadata(modules.credentials_mode, script.referrer_policy.as_deref()), + ))) +} + +pub(super) fn entry_id(entry: usize) -> module_tree::ModuleEntryId { + module_tree::ModuleEntryId( + u32::try_from(entry).expect("worker module entry id space exhausted"), + ) +} + +pub(super) fn tree_key(key: &WorkerModuleKey) -> module_tree::ModuleMapKey { + module_tree::ModuleMapKey::new( + key.url.clone(), + tree_kind(key.module_type), + module_tree::ModuleAttributesKey::from_pairs(key.attributes.pairs().to_vec()), + ) +} + +fn native_key( + key: &module_tree::ModuleMapKey, +) -> Result { + worker_module_key_for_attributes( + &key.url, + &ModuleAttributesKey::from_pairs(key.attributes.attributes.clone()), + ) + .map_err(|message| { + module_tree::ModuleLoadError::new(module_tree::ModuleLoadStage::Resolve, message) + }) +} + +fn tree_kind(kind: WorkerModuleType) -> module_tree::ModuleKind { + match kind { + WorkerModuleType::JavaScriptOrWebAssembly => module_tree::ModuleKind::JavaScript, + WorkerModuleType::Json => module_tree::ModuleKind::Json, + WorkerModuleType::Text => module_tree::ModuleKind::Text, + } +} + +pub(super) fn tree_phase(phase: ModuleImportPhase) -> module_tree::ModuleImportPhase { + match phase { + ModuleImportPhase::Evaluation => module_tree::ModuleImportPhase::Evaluation, + ModuleImportPhase::Source => module_tree::ModuleImportPhase::Source, + } +} + +fn tree_requests(requests: &[WorkerModuleRequest]) -> Vec { + requests + .iter() + .map(|request| module_tree::ModuleRequestRecord { + specifier: request.specifier.clone(), + attributes: module_tree::ModuleAttributesKey::from_pairs( + request.attributes.pairs().to_vec(), + ), + phase: tree_phase(request.phase), + position: module_tree::TextPosition::default(), + }) + .collect() +} + +pub(super) fn fetch_metadata( + credentials: RequestCredentialsMode, + policy: Option<&str>, +) -> module_tree::ModuleFetchMetadata { + module_tree::ModuleFetchMetadata { + credentials_mode: match credentials { + RequestCredentialsMode::Omit => module_tree::CredentialsMode::Omit, + RequestCredentialsMode::SameOrigin => module_tree::CredentialsMode::SameOrigin, + RequestCredentialsMode::Include => module_tree::CredentialsMode::Include, + }, + referrer_policy: match policy { + Some("no-referrer") => module_tree::ReferrerPolicy::NoReferrer, + Some("no-referrer-when-downgrade") => { + module_tree::ReferrerPolicy::NoReferrerWhenDowngrade + } + Some("origin") => module_tree::ReferrerPolicy::Origin, + Some("origin-when-cross-origin") => module_tree::ReferrerPolicy::OriginWhenCrossOrigin, + Some("same-origin") => module_tree::ReferrerPolicy::SameOrigin, + Some("strict-origin") => module_tree::ReferrerPolicy::StrictOrigin, + Some("strict-origin-when-cross-origin") => { + module_tree::ReferrerPolicy::StrictOriginWhenCrossOrigin + } + Some("unsafe-url") => module_tree::ReferrerPolicy::UnsafeUrl, + _ => module_tree::ReferrerPolicy::EmptyString, + }, + ..module_tree::ModuleFetchMetadata::default() + } +} + +fn native_referrer_policy(policy: module_tree::ReferrerPolicy) -> Option<&'static str> { + match policy { + module_tree::ReferrerPolicy::EmptyString => None, + module_tree::ReferrerPolicy::NoReferrer => Some("no-referrer"), + module_tree::ReferrerPolicy::NoReferrerWhenDowngrade => Some("no-referrer-when-downgrade"), + module_tree::ReferrerPolicy::Origin => Some("origin"), + module_tree::ReferrerPolicy::OriginWhenCrossOrigin => Some("origin-when-cross-origin"), + module_tree::ReferrerPolicy::SameOrigin => Some("same-origin"), + module_tree::ReferrerPolicy::StrictOrigin => Some("strict-origin"), + module_tree::ReferrerPolicy::StrictOriginWhenCrossOrigin => { + Some("strict-origin-when-cross-origin") + } + module_tree::ReferrerPolicy::UnsafeUrl => Some("unsafe-url"), + } +} diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index 76c4f58504..0d7f36c9ed 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -104,13 +104,14 @@ use super::inspector_task_runner::{ WorkerInspectorTask, WorkerInspectorTaskMode, WorkerInspectorTaskRunner, }; use super::module_runtime::{ - WorkerBootstrapError, WorkerDynamicModuleImportAdvance, WorkerModuleBootstrapResume, - WorkerModuleBootstrapStart, WorkerModuleEvaluationCompletion, WorkerModuleFetchedSource, - WorkerModuleGraphFetchBatch, WorkerModuleGraphFetchCompletion, WorkerModuleGraphFetchCspSource, - WorkerModuleGraphFetchRequest, WorkerModulePendingBootstrap, WorkerModuleSource, - complete_worker_module_graph_fetch, evaluate_module_worker_bootstrap_source, - install_classic_worker_dynamic_module_runtime, resume_worker_dynamic_module_evaluation, - run_next_worker_dynamic_module_import, worker_has_pending_dynamic_module_imports, + WorkerBootstrapError, WorkerModuleBootstrapResume, WorkerModuleBootstrapStart, + WorkerModuleEvaluationCompletion, WorkerModuleFetchedSource, WorkerModuleGraphFetchCompletion, + WorkerModuleGraphFetchCspSource, WorkerModuleGraphFetchRequest, WorkerModulePendingBootstrap, + WorkerModuleSource, complete_worker_module_graph_fetch, + evaluate_module_worker_bootstrap_source, install_classic_worker_dynamic_module_runtime, + resume_worker_dynamic_module_evaluation, resume_worker_module_trees, + retire_worker_module_runtime, run_next_worker_dynamic_module_import, + take_worker_module_graph_fetches, worker_has_pending_dynamic_module_imports, worker_has_runnable_dynamic_module_imports, }; @@ -601,6 +602,7 @@ fn start_worker_module_graph_fetch( completion_tx: mpsc::UnboundedSender, ) { let fetch_id = request.fetch_id(); + let completion_key = request.module_key(); let (enforce, report, endpoints, resource_kind) = match request.csp_source() { WorkerModuleGraphFetchCspSource::StaticModuleGraph => ( module_static_import_content_security_policies, @@ -635,8 +637,12 @@ fn start_worker_module_graph_fetch( ); if let Err(error) = csp_context.check_initial_request(request.url()) { let _ = completion_tx.send( - WorkerModuleGraphFetchCompletion::new(fetch_id, Err(error.to_string())) - .with_csp_violations(csp_context.take_violations()), + WorkerModuleGraphFetchCompletion::new( + fetch_id, + request.module_key(), + Err(error.to_string()), + ) + .with_csp_violations(csp_context.take_violations()), ); return; } @@ -649,8 +655,12 @@ fn start_worker_module_graph_fetch( .with_browser_request_metadata(browser_request_metadata), Err(error) => { let _ = completion_tx.send( - WorkerModuleGraphFetchCompletion::new(fetch_id, Err(error.to_string())) - .with_csp_violations(csp_context.take_violations()), + WorkerModuleGraphFetchCompletion::new( + fetch_id, + request.module_key(), + Err(error.to_string()), + ) + .with_csp_violations(csp_context.take_violations()), ); return; } @@ -721,7 +731,7 @@ fn start_worker_module_graph_fetch( .with_resource(resource) .with_response_referrer_policy(response_referrer_policy)) }); - let completion = WorkerModuleGraphFetchCompletion::new(fetch_id, result) + let completion = WorkerModuleGraphFetchCompletion::new(fetch_id, completion_key, result) .with_csp_violations(response_csp_context.take_violations()); let _ = completion_tx_for_callback.send(completion); }; @@ -733,6 +743,7 @@ fn start_worker_module_graph_fetch( let _ = completion_tx.send( WorkerModuleGraphFetchCompletion::new( fetch_id, + request.module_key(), Err("worker module dependency fetch rejected during shutdown".to_owned()), ) .with_csp_violations(csp_context.take_violations()), @@ -746,6 +757,7 @@ fn start_worker_module_graph_fetch( let _ = completion_tx.send( WorkerModuleGraphFetchCompletion::new( fetch_id, + request.module_key(), Err(format!( "failed to start module worker dependency fetch `{}`: {error}", request.url() @@ -756,28 +768,6 @@ fn start_worker_module_graph_fetch( } } -fn start_worker_module_graph_fetch_batch( - requests: WorkerModuleGraphFetchBatch, - state: &Rc>, - module_graph_fetch_tx: &mpsc::UnboundedSender, -) { - for request in requests.iter().cloned() { - start_worker_module_graph_fetch( - request, - state.borrow().loader.clone(), - state.borrow().network_partition_key.clone(), - state - .borrow() - .module_static_import_content_security_policies - .clone(), - state.borrow().content_security_policies.clone(), - state.borrow().content_security_report_only_policies.clone(), - state.borrow().content_security_reporting_endpoints.clone(), - module_graph_fetch_tx.clone(), - ); - } -} - fn run_worker_file_reading_task(isolate: &mut v8::OwnedIsolate, context: &v8::Global) { let scope = pin!(v8::HandleScope::new(isolate)); let scope = &mut scope.init(); @@ -841,18 +831,25 @@ fn flush_one_worker_indexed_db_task( drain_worker_dynamic_module_imports(scope, state, module_graph_fetch_tx); } -fn handle_worker_dynamic_module_import_advance( - advance: WorkerDynamicModuleImportAdvance, +fn flush_worker_module_fetches( + scope: &mut v8::PinScope<'_, '_>, state: &Rc>, module_graph_fetch_tx: &mpsc::UnboundedSender, ) { - match advance { - WorkerDynamicModuleImportAdvance::Complete - | WorkerDynamicModuleImportAdvance::WaitingFetches - | WorkerDynamicModuleImportAdvance::WaitingEvaluation { .. } => {} - WorkerDynamicModuleImportAdvance::NeedFetches(requests) => { - start_worker_module_graph_fetch_batch(requests, state, module_graph_fetch_tx); - } + for request in take_worker_module_graph_fetches(scope) { + start_worker_module_graph_fetch( + request, + state.borrow().loader.clone(), + state.borrow().network_partition_key.clone(), + state + .borrow() + .module_static_import_content_security_policies + .clone(), + state.borrow().content_security_policies.clone(), + state.borrow().content_security_report_only_policies.clone(), + state.borrow().content_security_reporting_endpoints.clone(), + module_graph_fetch_tx.clone(), + ); } } @@ -865,10 +862,10 @@ fn drain_worker_dynamic_module_imports( ) { const MAX_DYNAMIC_IMPORT_DRAIN_STEPS: usize = 1_000; for _ in 0..MAX_DYNAMIC_IMPORT_DRAIN_STEPS { - let Some(advance) = run_next_worker_dynamic_module_import(scope) else { + if !run_next_worker_dynamic_module_import(scope) { break; - }; - handle_worker_dynamic_module_import_advance(advance, state, module_graph_fetch_tx); + } + flush_worker_module_fetches(scope, state, module_graph_fetch_tx); } } @@ -1793,7 +1790,7 @@ async fn worker_main( let referrer_policy = { state.borrow().referrer_policy.clone() }; // ── Evaluate the worker script ───────────────────────────────────── - match evaluate_worker_bootstrap_script( + let start = evaluate_worker_bootstrap_script( scope, &script_source, &script_url, @@ -1802,17 +1799,13 @@ async fn worker_main( module_credentials_mode, referrer_policy, module_evaluation_tx.clone(), - ) { + ); + flush_worker_module_fetches(scope, &state, &module_graph_fetch_tx); + match start { WorkerBootstrapStart::Complete => { bootstrap_completion.mark_success(worker_bootstrap_success(scope, global, &state)); } - WorkerBootstrapStart::Pending { - bootstrap, - requests, - } => { - if let Some(requests) = requests { - start_worker_module_graph_fetch_batch(requests, &state, &module_graph_fetch_tx); - } + WorkerBootstrapStart::Pending(bootstrap) => { pending_module_bootstrap = Some(bootstrap); } WorkerBootstrapStart::Failed { error, phase } => { @@ -3074,16 +3067,16 @@ async fn worker_main( let loader = state.borrow().loader.clone(); dispatch_worker_csp_violation_event(scope, &loader, violation); } - if let Some(resume) = - outcome.resume_bootstrap(scope, pending_module_bootstrap.as_deref_mut()) + let resume = + resume_worker_module_trees(scope, pending_module_bootstrap.as_deref_mut()); + flush_worker_module_fetches(scope, &state, &module_graph_fetch_tx); + if outcome.bootstrap_client + && let Ok(source) = completion.result() + && let Some(resource) = source.resource().cloned() { - let module_script_resource = completion - .result() - .ok() - .and_then(|source| source.resource().cloned()); - if let Some(resource) = module_script_resource { - report_service_worker_module_script_resource(&state, resource); - } + report_service_worker_module_script_resource(&state, resource); + } + if let Some(resume) = resume { match resume { WorkerModuleBootstrapResume::Complete => { pending_module_bootstrap = None; @@ -3097,14 +3090,6 @@ async fn worker_main( ); forward_worker_script_loaded(&runtime_inspector, &parent_tx); } - WorkerModuleBootstrapResume::NeedFetches(requests) => { - start_worker_module_graph_fetch_batch( - requests, - &state, - &module_graph_fetch_tx, - ); - } - WorkerModuleBootstrapResume::WaitingFetches => {} WorkerModuleBootstrapResume::WaitingEvaluation => {} WorkerModuleBootstrapResume::Failed(error) => { let (report, exception, parent_event_kind) = *error; @@ -3144,7 +3129,7 @@ async fn worker_main( source.source().len() ) }) - .unwrap_or_else(|error| format!("error {error}")); + .unwrap_or_else(|error| format!("error {}", error.message)); trace!( url = %script_url, fetch_id = completion.fetch_id(), @@ -3184,14 +3169,6 @@ async fn worker_main( ); forward_worker_script_loaded(&runtime_inspector, &parent_tx); } - WorkerModuleBootstrapResume::NeedFetches(requests) => { - start_worker_module_graph_fetch_batch( - requests, - &state, - &module_graph_fetch_tx, - ); - } - WorkerModuleBootstrapResume::WaitingFetches => {} WorkerModuleBootstrapResume::WaitingEvaluation => {} WorkerModuleBootstrapResume::Failed(error) => { let (report, exception, parent_event_kind) = *error; @@ -3308,6 +3285,10 @@ async fn worker_main( // routes are destroyed. Ordinary transports are cancelled here; // explicitly keepalive loads are reduced to browser-runtime network-only // records and therefore cannot retain this WorkerGlobalScope. + // Drop persistent rejection values while their isolate is alive, including + // completions queued by a module that called close() before rejecting. + module_evaluation_rx.close(); + while module_evaluation_rx.try_recv().is_ok() {} inspector_task_runner.dispose(crate::runtime::WorkerInspectorError::Exited); let resource_loader = state.borrow().loader.clone(); resource_loader.begin_detach(); @@ -3319,6 +3300,7 @@ async fn worker_main( let scope = pin!(v8::HandleScope::new(isolate)); let scope = &mut scope.init(); let ctx = v8::Local::new(scope, &context); + retire_worker_module_runtime(ctx); crate::context_bootstrap::retire_indexed_db_context(ctx); runtime_inspector.context_destroyed(ctx); } @@ -3451,10 +3433,7 @@ fn worker_bootstrap_success<'s>( enum WorkerBootstrapStart { Complete, - Pending { - bootstrap: Box, - requests: Option, - }, + Pending(Box), Failed { error: Box, phase: WorkerErrorPhase, @@ -3513,13 +3492,7 @@ impl From for WorkerBootstrapStart { fn from(start: WorkerModuleBootstrapStart) -> Self { match start { WorkerModuleBootstrapStart::Complete => Self::Complete, - WorkerModuleBootstrapStart::Pending { - bootstrap, - requests, - } => Self::Pending { - bootstrap, - requests, - }, + WorkerModuleBootstrapStart::Pending(bootstrap) => Self::Pending(bootstrap), WorkerModuleBootstrapStart::Failed(error) => Self::Failed { error, phase: WorkerErrorPhase::Bootstrap, diff --git a/moli-renderer-v8/src/worker/thread/tests/modules/json_parse.rs b/moli-renderer-v8/src/worker/thread/tests/modules/json_parse.rs index 33f67ecbf9..c502a97b58 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules/json_parse.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules/json_parse.rs @@ -52,99 +52,82 @@ async fn worker_json_parse_errors_prevent_sibling_execution_and_remain_cached() } #[tokio::test] -async fn worker_json_parse_failure_preserves_pending_top_level_await() { - json_parse_failure_keeps_pending_sibling(true, false).await; -} - -#[tokio::test] -async fn worker_json_parse_failure_reuses_pending_and_completed_sibling() { - json_parse_failure_keeps_pending_sibling(false, false).await; -} - -#[tokio::test] -async fn worker_json_parse_failure_caches_sibling_after_graph_is_gone() { - for top_level_await in [true, false] { - json_parse_failure_keeps_pending_sibling(top_level_await, true).await; - } -} - -async fn json_parse_failure_keeps_pending_sibling( - top_level_await: bool, - sibling_response_first: bool, -) { +async fn worker_nested_json_errors_follow_declaration_order_and_reuse_native_exceptions() { ensure_v8(); - for imports in [ - r#"import './side.mjs'; import data from './bad.json' with {type:'json'};"#, - r#"import data from './bad.json' with {type:'json'}; import './side.mjs';"#, - ] { + for first_branch in ["a", "b"] { let mut server = ModuleSourceServer::start().await; - let source = r#" - self.runs = 0; - let graphError; - try { await import('./graph.mjs'); } - catch (error) { graphError = error; } - postMessage(['caught', graphError.name, self.runs]); - await fetch('./hold'); - const pendingSide = import('./side.mjs'); - postMessage('joining sibling'); - try { - const side = await pendingSide; - const again = await import('./side.mjs'); - const graphAgain = await import('./graph.mjs').catch(error => error); - postMessage([true, graphError.name, side.value, self.runs, - side === again, graphError === graphAgain]); - } catch (error) { postMessage({laterError: error.name}); } - "#; - let source = if top_level_await { - source.to_owned() - } else { - format!( - "(async () => {{ {source} }})().catch(error => postMessage({{error: error.name}}));" - ) - }; - let mut handle = server.worker(source, WorkerScriptKind::Module); + let mut handle = server.worker(r#" + (async () => { + self.runs = 0; + onmessage = async event => { + await import(`./bad-${event.data}.json`, { with: { type: 'json' } }).catch(error => error); + postMessage(['stored', event.data]); + }; + const chosen = await import('./root.mjs').catch(error => error); + const fromA = await import('./bad-a.json', { with: { type: 'json' } }).catch(error => error); + const fromB = await import('./bad-b.json', { with: { type: 'json' } }).catch(error => error); + postMessage([chosen === fromA, chosen === fromB, self.runs, chosen.name]); + })().catch(error => postMessage({ error: error.name })); + "#.into(), WorkerScriptKind::Module); server - .respond("/worker/graph.mjs", "200 OK", "text/javascript", imports) + .respond( + "/worker/root.mjs", + "200 OK", + "text/javascript", + "import './a.mjs'; import './b.mjs'; ++self.runs;", + ) .await; - let (sibling, invalid_json) = server - .request_pair("/worker/side.mjs", "/worker/bad.json") - .await; - ModuleSourceServer::write(invalid_json, "200 OK", "application/json", "{invalid}").await; - assert_eq!( - recv_post_json(&mut handle).await, - r#"["caught","SyntaxError",0]"# - ); - let start_independent_import = async { - server - .respond("/worker/hold", "200 OK", "text/plain", "release") - .await; - assert_eq!(recv_post_json(&mut handle).await, r#""joining sibling""#); + let (a, b) = server.request_pair("/worker/a.mjs", "/worker/b.mjs").await; + let (first, second, second_branch) = if first_branch == "a" { + (a, b, "b") + } else { + (b, a, "a") }; - let sibling_response = ModuleSourceServer::write( - sibling, + ModuleSourceServer::write( + first, "200 OK", "text/javascript", - "++self.runs; export const value = 42;", - ); - if sibling_response_first { - // No live import needs this response yet; retain its module record. - sibling_response.await; - start_independent_import.await; - } else { - // The independent import must reuse the original in-flight fetch. - start_independent_import.await; - sibling_response.await; - } - let observed = tokio::select! { - observed = recv_post_json(&mut handle) => observed, - duplicate = server.request("/worker/side.mjs") => { - ModuleSourceServer::write(duplicate, "503 Service Unavailable", "text/plain", "duplicate fetch").await; - recv_post_json(&mut handle).await - } - }; + &format!( + "import value from './bad-{first_branch}.json' with {{type:'json'}}; ++self.runs;" + ), + ) + .await; + server + .respond( + &format!("/worker/bad-{first_branch}.json"), + "200 OK", + "application/json", + "{invalid}", + ) + .await; + // A separate import settles only after the shared creation result has + // been stored. The other parent remains withheld until that barrier. + handle.post_message(serialize_test_string(first_branch)); assert_eq!( - observed, r#"[true,"SyntaxError",42,1,true,true]"#, - "{imports}" + recv_post_json(&mut handle).await, + format!(r#"["stored","{first_branch}"]"#) + ); + ModuleSourceServer::write( + second, + "200 OK", + "text/javascript", + &format!( + "import value from './bad-{second_branch}.json' with {{type:'json'}}; ++self.runs;" + ), + ) + .await; + server + .respond( + &format!("/worker/bad-{second_branch}.json"), + "200 OK", + "application/json", + "{invalid}", + ) + .await; + assert_eq!( + recv_post_json(&mut handle).await, + r#"[true,false,0,"SyntaxError"]"#, + "{first_branch} completed first" ); handle.terminate_and_join(); server.assert_no_more_requests(); diff --git a/moli-renderer-v8/src/worker/thread/tests/modules/module_map.rs b/moli-renderer-v8/src/worker/thread/tests/modules/module_map.rs index 419bcaddb6..db704fdc23 100644 --- a/moli-renderer-v8/src/worker/thread/tests/modules/module_map.rs +++ b/moli-renderer-v8/src/worker/thread/tests/modules/module_map.rs @@ -119,3 +119,312 @@ async fn worker_module_map_compiled_root_still_waits_for_shared_evaluation() { handle.terminate_and_join(); server.assert_no_more_requests(); } + +#[tokio::test] +async fn worker_module_load_failure_preserves_pending_top_level_await() { + module_load_failure_keeps_pending_sibling(true, false).await; +} + +#[tokio::test] +async fn worker_module_load_failure_reuses_pending_and_completed_sibling() { + module_load_failure_keeps_pending_sibling(false, false).await; +} + +#[tokio::test] +async fn worker_module_load_failure_caches_sibling_after_graph_is_gone() { + for top_level_await in [true, false] { + module_load_failure_keeps_pending_sibling(top_level_await, true).await; + } +} + +async fn module_load_failure_keeps_pending_sibling( + top_level_await: bool, + sibling_response_first: bool, +) { + ensure_v8(); + for imports in [ + r#"import './side.mjs'; import data from './bad.json' with {type:'json'};"#, + r#"import data from './bad.json' with {type:'json'}; import './side.mjs';"#, + ] { + let mut server = ModuleSourceServer::start().await; + let source = r#" + self.runs = 0; + let graphError; + try { await import('./graph.mjs'); } + catch (error) { graphError = error; } + postMessage(['caught', graphError.name, self.runs]); + await fetch('./hold'); + const pendingSide = import('./side.mjs'); + postMessage('joining sibling'); + try { + const side = await pendingSide; + const again = await import('./side.mjs'); + postMessage([true, graphError.name, side.value, self.runs, + side === again]); + } catch (error) { postMessage({laterError: error.name}); } + "#; + let source = if top_level_await { + source.to_owned() + } else { + format!( + "(async () => {{ {source} }})().catch(error => postMessage({{error: error.name}}));" + ) + }; + let mut handle = server.worker(source, WorkerScriptKind::Module); + server + .respond("/worker/graph.mjs", "200 OK", "text/javascript", imports) + .await; + let (sibling, invalid_json) = server + .request_pair("/worker/side.mjs", "/worker/bad.json") + .await; + ModuleSourceServer::write( + invalid_json, + "503 Service Unavailable", + "text/plain", + "unavailable", + ) + .await; + assert_eq!( + recv_post_json(&mut handle).await, + r#"["caught","TypeError",0]"# + ); + let start_independent_import = async { + server + .respond("/worker/hold", "200 OK", "text/plain", "release") + .await; + assert_eq!(recv_post_json(&mut handle).await, r#""joining sibling""#); + }; + let sibling_response = ModuleSourceServer::write( + sibling, + "200 OK", + "text/javascript", + "++self.runs; export const value = 42;", + ); + if sibling_response_first { + // No live import needs this response yet; retain its module record. + sibling_response.await; + start_independent_import.await; + } else { + // The independent import must reuse the original in-flight fetch. + start_independent_import.await; + sibling_response.await; + } + let observed = tokio::select! { + observed = recv_post_json(&mut handle) => observed, + duplicate = server.request("/worker/side.mjs") => { + ModuleSourceServer::write(duplicate, "503 Service Unavailable", "text/plain", "duplicate fetch").await; + recv_post_json(&mut handle).await + } + }; + assert_eq!(observed, r#"[true,"TypeError",42,1,true]"#, "{imports}"); + handle.terminate_and_join(); + server.assert_no_more_requests(); + } +} + +#[tokio::test] +async fn worker_module_tree_failure_preserves_admitted_network_actions() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker( + r#" + (async () => { + await import('./a.mjs').catch(() => {}); + await import('./b.mjs').catch(() => {}); + const failure = await import('./both.mjs').catch(error => error.name); + postMessage(['failed', failure]); + const retry = import('./a.mjs'); + postMessage('joined'); + postMessage((await retry).value); + })().catch(error => postMessage({ error: error.name })); + "# + .into(), + WorkerScriptKind::Module, + ); + server + .respond( + "/worker/a.mjs", + "200 OK", + "text/javascript", + "export { value } from './held.mjs';", + ) + .await; + server + .respond( + "/worker/held.mjs", + "503 Service Unavailable", + "text/plain", + "retry later", + ) + .await; + server + .respond( + "/worker/b.mjs", + "200 OK", + "text/javascript", + "import 'unresolvable-bare-specifier';", + ) + .await; + // Both records now exist. The common tree admits A's retry before B's + // resolution failure ends the graph in the same drive call. + server + .respond( + "/worker/both.mjs", + "200 OK", + "text/javascript", + "import './a.mjs'; import './b.mjs';", + ) + .await; + let held = server.request("/worker/held.mjs").await; + assert_eq!( + recv_post_json(&mut handle).await, + r#"["failed","TypeError"]"# + ); + assert_eq!(recv_post_json(&mut handle).await, r#""joined""#); + ModuleSourceServer::write( + held, + "200 OK", + "text/javascript", + "export const value = 42;", + ) + .await; + assert_eq!(recv_post_json(&mut handle).await, "42"); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_shared_module_evaluation_preserves_original_rejection_value() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker(r#" + (async () => { + self.evaluations = 0; + self.coercions = 0; + self.reason = { toString() { ++self.coercions; return 'coerced'; } }; + const capture = promise => promise.catch(error => error); + const [first, second] = await Promise.all([capture(import('./async.mjs')), capture(import('./async.mjs'))]); + const again = await capture(import('./async.mjs')); + postMessage([first === self.reason, second === self.reason, again === self.reason, + self.evaluations, self.coercions]); + })().catch(error => postMessage({ error: error.name })); + "#.into(), WorkerScriptKind::Module); + server + .respond( + "/worker/async.mjs", + "200 OK", + "text/javascript", + "++self.evaluations; await fetch('./eval-hold'); throw self.reason;", + ) + .await; + server + .respond("/worker/eval-hold", "200 OK", "text/plain", "release") + .await; + assert_eq!(recv_post_json(&mut handle).await, "[true,true,true,1,0]"); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_distinct_roots_and_direct_import_share_pending_dependency_evaluation() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker( + r#" + (async () => { + self.evaluations = 0; + let others; + let resolutions = 0; + onmessage = event => { + if (event.data === 'join') { + others = Promise.all([import('./b.mjs'), import('./dep.mjs')].map(promise => + promise.then(ns => { ++resolutions; return ns; }))); + postMessage('joined'); + } else if (event.data === 'probe') { + postMessage(['pending', resolutions]); + } + }; + const first = await import('./a.mjs'); + const [second, direct] = await others; + postMessage([first.value, second.value, direct.value, self.evaluations, + direct === await import('./dep.mjs')]); + })().catch(error => postMessage({ error: error.name })); + "# + .into(), + WorkerScriptKind::Module, + ); + server + .respond( + "/worker/a.mjs", + "200 OK", + "text/javascript", + "export { value } from './dep.mjs';", + ) + .await; + server + .respond( + "/worker/dep.mjs", + "200 OK", + "text/javascript", + "++self.evaluations; await fetch('./eval-hold'); export const value = 42;", + ) + .await; + let hold = server.request("/worker/eval-hold").await; + handle.post_message(serialize_test_string("join")); + assert_eq!(recv_post_json(&mut handle).await, r#""joined""#); + server + .respond( + "/worker/b.mjs", + "200 OK", + "text/javascript", + "export { value } from './dep.mjs';", + ) + .await; + handle.post_message(serialize_test_string("probe")); + assert_eq!(recv_post_json(&mut handle).await, r#"["pending",0]"#); + ModuleSourceServer::write(hold, "200 OK", "text/plain", "release").await; + assert_eq!(recv_post_json(&mut handle).await, "[42,42,42,1,true]"); + handle.terminate_and_join(); + server.assert_no_more_requests(); +} + +#[tokio::test] +async fn worker_close_releases_pending_module_rejection_handles() { + ensure_v8(); + let mut server = ModuleSourceServer::start().await; + let mut handle = server.worker( + r#" + import('./close.mjs').catch(() => postMessage('unexpected callback after close')); + "# + .into(), + WorkerScriptKind::Module, + ); + server + .respond( + "/worker/close.mjs", + "200 OK", + "text/javascript", + r#" + await fetch('./close-hold'); + self.close(); + throw { toString() { throw new Error('unexpected coercion after close'); } }; + "#, + ) + .await; + server + .respond("/worker/close-hold", "200 OK", "text/plain", "release") + .await; + while let Some(message) = timeout(TIMEOUT, handle.recv()) + .await + .expect("closed worker should exit") + { + match message { + WorkerToParentMessage::Post(_) | WorkerToParentMessage::Error { .. } => { + panic!("unexpected script activity during module retirement: {message:?}") + } + _ => {} + } + } + handle.terminate_and_join(); + server.assert_no_more_requests(); +}