mirror of
https://github.com/lexmount/moli.git
synced 2026-11-15 00:08:03 +00:00
refactor(workers): drive module maps with shared module trees
This commit is contained in:
@@ -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"),
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user