refactor(workers): drive module maps with shared module trees

This commit is contained in:
ldm0
2026-11-13 21:49:41 +08:00
committed by Donough Liu
parent d101a6a599
commit 5bfb303ccd
8 changed files with 1646 additions and 1423 deletions
+2 -1
View File
@@ -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)
@@ -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]
File diff suppressed because it is too large Load Diff
@@ -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<WorkerModuleGraphFetchRequest>,
waiters: HashSet<WorkerModuleWaiter>,
clients: Vec<module_tree::SingleModuleClientToken>,
},
Compiled {
entry: usize,
module: Box<WorkerModuleRecord>,
},
ParseError(Box<WorkerBootstrapError>),
Loaded(usize),
}
#[derive(Default)]
pub(super) struct WorkerModuleMap {
entries: HashMap<WorkerModuleKey, WorkerModuleMapEntry>,
fetch_keys: HashMap<WorkerModuleGraphFetchId, WorkerModuleKey>,
// Stable graph indices survive transitions of unrelated module-map entries.
compiled_keys: Vec<WorkerModuleKey>,
pub(super) struct WorkerModuleScript {
pub(super) key: WorkerModuleKey,
pub(super) response_url: url::Url,
pub(super) referrer_policy: Option<String>,
pub(super) creation: Result<WorkerModuleRecord, Box<WorkerBootstrapError>>,
}
impl WorkerModuleMap {
pub(super) fn compiled_entry(&self, key: &WorkerModuleKey) -> Option<usize> {
pub(super) fn loaded_entry(&self, key: &WorkerModuleKey) -> Option<usize> {
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<Box<WorkerBootstrapError>> {
match self.entries.get(key)? {
WorkerModuleMapEntry::ParseError(error) => Some(error.clone()),
_ => None,
}
}
pub(super) fn fetch_id(&self, key: &WorkerModuleKey) -> Option<WorkerModuleGraphFetchId> {
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<WorkerModuleGraphFetchBatch> {
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<WorkerModuleGraphFetchRequest>,
) -> 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<Item = WorkerModuleKey>,
) {
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<WorkerModuleWaiter>)> {
let key = self.fetch_keys.remove(&fetch_id)?;
let Some(WorkerModuleMapEntry::Fetching { request, waiters }) = self.entries.remove(&key)
) -> Option<(
WorkerModuleGraphFetchRequest,
Vec<module_tree::SingleModuleClientToken>,
)> {
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<WorkerBootstrapError>,
) {
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<Item = &WorkerModuleRecord> {
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());
}
}
@@ -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<WorkerDynamicModuleImportJob>),
}
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<usize, module_tree::ModuleLoadError>,
}
#[derive(Default)]
pub(super) struct WorkerModuleTrees {
pub(super) tasks: HashMap<module_tree::ModuleTreeId, WorkerModuleTreeTask>,
pub(super) notifications: VecDeque<WorkerSingleModuleCompletion>,
pub(super) fetches_to_start: Vec<WorkerModuleGraphFetchRequest>,
}
pub(super) struct WorkerModuleTreeHost<'a, 's, 'i> {
pub(super) scope: &'a mut v8::PinScope<'s, 'i>,
pub(super) modules: Rc<RefCell<WorkerModuleMap>>,
pub(super) work: Rc<RefCell<WorkerModuleTrees>>,
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<module_tree::ResolvedModuleRequest, module_tree::ModuleLoadError> {
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<module_tree::CompiledModuleSnapshot, module_tree::ModuleLoadError> {
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<module_tree::ModuleLoadError> {
creation_error(&self.modules.borrow(), entry.0 as usize)
}
fn module_dependencies(
&self,
entry: module_tree::ModuleEntryId,
) -> Result<module_tree::ModuleDependencySnapshot, module_tree::ModuleLoadError> {
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<module_tree::ModuleGraphHandle, module_tree::ModuleLoadError> {
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<module_tree::ModuleLoadError> {
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<WorkerModuleKey, module_tree::ModuleLoadError> {
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<module_tree::ModuleRequestRecord> {
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"),
}
}
+65 -92
View File
@@ -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<WorkerModuleGraphFetchCompletion>,
) {
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<RefCell<WorkerGlobalState>>,
module_graph_fetch_tx: &mpsc::UnboundedSender<WorkerModuleGraphFetchCompletion>,
) {
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<v8::Context>) {
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<RefCell<WorkerGlobalState>>,
module_graph_fetch_tx: &mpsc::UnboundedSender<WorkerModuleGraphFetchCompletion>,
) {
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<WorkerModulePendingBootstrap>,
requests: Option<WorkerModuleGraphFetchBatch>,
},
Pending(Box<WorkerModulePendingBootstrap>),
Failed {
error: Box<WorkerBootstrapError>,
phase: WorkerErrorPhase,
@@ -3513,13 +3492,7 @@ impl From<WorkerModuleBootstrapStart> 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,
@@ -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();
@@ -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();
}