mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 08:00:49 +00:00
JSON/CSS compilation retained independent copies in the module map and the synthetic module record. Share immutable text with Arc<str> across fetched sources, the portable module tree, and synthetic evaluation records instead of copying String buffers during cloning and registration. Release the module-map source after successful JavaScript/JSON/CSS compilation. Make fetched-source retention an exhaustive ModuleKind policy, preserving Wasm and text-preload source semantics. Context lookups remain weak record references. Cover the actual module-tree compile path: the record keeps exactly one strong reference to the original text allocation after fetch temporaries are dropped, and destroys it when disposed. Verify record clones share the same source and Wasm entries retain their binary source. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast: 19,013 passed, 16 skipped
1213 lines
41 KiB
Rust
1213 lines
41 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use moli_module_script_tree::{
|
|
CompiledModuleSnapshot, CredentialsMode, FetchPriorityHint, FetchedModuleSource,
|
|
ModuleAttributesKey, ModuleDependencyEdge, ModuleDependencySnapshot, ModuleEntryId,
|
|
ModuleErrorConstructorKind, ModuleExternalRootInput, ModuleFetchMetadata, ModuleFetchOutcome,
|
|
ModuleFetchRequest, ModuleFetchResult, ModuleGraphHandle, ModuleGraphLevel, ModuleImportPhase,
|
|
ModuleKind, ModuleLoadError, ModuleLoadStage, ModuleMapKey, ModuleReferrer,
|
|
ModuleRequestContext, ModuleRequestRecord, ModuleRootInput, ModuleScriptTreeDrive,
|
|
ModuleScriptTreeHost, ModuleScriptTreeJob, ModuleScriptTreePoll, ModuleSource,
|
|
ModuleTreeAbortReason, ModuleTreeConfig, ModuleTreeId, ModuleTreeOwner, ModuleTreeState,
|
|
ReadyModule, ReferrerPolicy, RenderBlockingBehavior, ResolvedModuleRequest,
|
|
ScriptFetchSchedulerPriority, SingleModuleClientToken, SingleModuleFetchDisposition,
|
|
TextPosition,
|
|
};
|
|
use url::Url;
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum FakeEntry {
|
|
Fetching {
|
|
clients: Vec<SingleModuleClientToken>,
|
|
phase: ModuleImportPhase,
|
|
},
|
|
Ready {
|
|
entry: ModuleEntryId,
|
|
phase: ModuleImportPhase,
|
|
deps: Vec<ModuleRequestRecord>,
|
|
metadata: ModuleFetchMetadata,
|
|
},
|
|
Failed {
|
|
error: ModuleLoadError,
|
|
phase: ModuleImportPhase,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct FakeHost {
|
|
entries: HashMap<ModuleMapKey, FakeEntry>,
|
|
started: Vec<ModuleFetchRequest>,
|
|
queued: Vec<ModuleFetchResult>,
|
|
compile_errors: HashMap<ModuleMapKey, ModuleLoadError>,
|
|
link_calls: usize,
|
|
next_fetch_id: u64,
|
|
next_entry_id: u32,
|
|
fail_link: Option<ModuleLoadError>,
|
|
}
|
|
|
|
impl FakeHost {
|
|
fn new() -> Self {
|
|
Self { ..Self::default() }
|
|
}
|
|
|
|
fn ready(&mut self, key: ModuleMapKey, entry: ModuleEntryId, deps: Vec<ModuleRequestRecord>) {
|
|
self.next_entry_id = self.next_entry_id.max(entry.0);
|
|
self.entries.insert(
|
|
key,
|
|
FakeEntry::Ready {
|
|
entry,
|
|
phase: ModuleImportPhase::Evaluation,
|
|
deps,
|
|
metadata: ModuleFetchMetadata::default(),
|
|
},
|
|
);
|
|
}
|
|
|
|
fn fail_compile(&mut self, key: ModuleMapKey, error: ModuleLoadError) {
|
|
self.compile_errors.insert(key, error);
|
|
}
|
|
|
|
fn take_single_client(&self, key: &ModuleMapKey) -> SingleModuleClientToken {
|
|
match self.entries.get(key).expect("entry should exist") {
|
|
FakeEntry::Fetching { clients, .. } => {
|
|
*clients.first().expect("fetching entry should have client")
|
|
}
|
|
_ => panic!("entry should be fetching"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ModuleScriptTreeHost for FakeHost {
|
|
fn resolve_module_request(
|
|
&mut self,
|
|
specifier: &str,
|
|
base_url: &Url,
|
|
attributes: &ModuleAttributesKey,
|
|
requested_phase: ModuleImportPhase,
|
|
) -> Result<ResolvedModuleRequest, ModuleLoadError> {
|
|
let source_url = base_url
|
|
.join(specifier)
|
|
.map_err(|error| ModuleLoadError::new(ModuleLoadStage::Resolve, error.to_string()))?;
|
|
let kind = if attributes
|
|
.attributes
|
|
.iter()
|
|
.any(|(key, value)| key == "type" && value == "json")
|
|
{
|
|
ModuleKind::Json
|
|
} else {
|
|
ModuleKind::JavaScript
|
|
};
|
|
let key = ModuleMapKey::new(source_url.clone(), kind, attributes.clone());
|
|
Ok(ResolvedModuleRequest {
|
|
key,
|
|
source_url,
|
|
base_url: base_url.clone(),
|
|
kind,
|
|
attributes: attributes.clone(),
|
|
phase: requested_phase,
|
|
integrity: None,
|
|
})
|
|
}
|
|
|
|
fn start_or_join_single_module_fetch(
|
|
&mut self,
|
|
request: ModuleFetchRequest,
|
|
client: SingleModuleClientToken,
|
|
) -> SingleModuleFetchDisposition {
|
|
match self.entries.get_mut(&request.key) {
|
|
Some(FakeEntry::Fetching { clients, phase }) => {
|
|
clients.push(client);
|
|
*phase = phase.strongest(request.phase);
|
|
SingleModuleFetchDisposition::JoinedExistingFetch
|
|
}
|
|
Some(FakeEntry::Ready {
|
|
entry,
|
|
phase,
|
|
metadata,
|
|
..
|
|
}) => {
|
|
*phase = phase.strongest(request.phase);
|
|
let key = request.key.clone();
|
|
let result = ModuleFetchResult {
|
|
key: request.key,
|
|
client,
|
|
requested_phase: request.phase,
|
|
outcome: ready_outcome_with_metadata(key, *entry, metadata.clone()),
|
|
};
|
|
self.queued.push(result.clone());
|
|
SingleModuleFetchDisposition::Completed(result.outcome)
|
|
}
|
|
Some(FakeEntry::Failed { error, phase }) => {
|
|
*phase = phase.strongest(request.phase);
|
|
let result = ModuleFetchResult {
|
|
key: request.key,
|
|
client,
|
|
requested_phase: request.phase,
|
|
outcome: ModuleFetchOutcome::Failed(error.clone()),
|
|
};
|
|
self.queued.push(result.clone());
|
|
SingleModuleFetchDisposition::Completed(result.outcome)
|
|
}
|
|
None => {
|
|
self.entries.insert(
|
|
request.key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: vec![client],
|
|
phase: request.phase,
|
|
},
|
|
);
|
|
self.started.push(request);
|
|
let fetch_id = moli_module_script_tree::ModuleFetchId(self.next_fetch_id);
|
|
self.next_fetch_id += 1;
|
|
SingleModuleFetchDisposition::StartedNetworkFetch { fetch_id }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compile_module_source(
|
|
&mut self,
|
|
fetched_source: FetchedModuleSource,
|
|
phase: ModuleImportPhase,
|
|
) -> Result<CompiledModuleSnapshot, ModuleLoadError> {
|
|
let key = fetched_source.key;
|
|
if let Some(error) = self.compile_errors.get(&key).cloned() {
|
|
return Err(error);
|
|
}
|
|
self.next_entry_id += 1;
|
|
let entry = ModuleEntryId(self.next_entry_id);
|
|
self.entries.insert(
|
|
key.clone(),
|
|
FakeEntry::Ready {
|
|
entry,
|
|
phase,
|
|
deps: Vec::new(),
|
|
metadata: fetched_source.effective_fetch_metadata.clone(),
|
|
},
|
|
);
|
|
Ok(CompiledModuleSnapshot {
|
|
entry,
|
|
key,
|
|
base_url: fetched_source.base_url,
|
|
effective_fetch_metadata: fetched_source.effective_fetch_metadata,
|
|
requested_modules: Vec::new(),
|
|
phase,
|
|
has_parse_error: false,
|
|
parse_error: None,
|
|
})
|
|
}
|
|
|
|
fn module_dependencies(
|
|
&self,
|
|
entry: ModuleEntryId,
|
|
) -> Result<ModuleDependencySnapshot, ModuleLoadError> {
|
|
self.entries
|
|
.iter()
|
|
.find_map(|(key, entry_state)| match entry_state {
|
|
FakeEntry::Ready {
|
|
entry: ready_entry,
|
|
deps,
|
|
metadata,
|
|
..
|
|
} if *ready_entry == entry => Some(ModuleDependencySnapshot {
|
|
entry,
|
|
key: key.clone(),
|
|
base_url: key.url.clone(),
|
|
effective_fetch_metadata: metadata.clone(),
|
|
requested_modules: deps.clone(),
|
|
}),
|
|
_ => None,
|
|
})
|
|
.ok_or_else(|| {
|
|
ModuleLoadError::new(
|
|
ModuleLoadStage::DependencyDiscovery,
|
|
"missing dependency snapshot",
|
|
)
|
|
})
|
|
}
|
|
|
|
fn link_module_graph(
|
|
&mut self,
|
|
root: ModuleEntryId,
|
|
entries: &[ModuleEntryId],
|
|
dependency_edges: &[ModuleDependencyEdge],
|
|
) -> Result<ModuleGraphHandle, ModuleLoadError> {
|
|
self.link_calls += 1;
|
|
if let Some(error) = self.fail_link.clone() {
|
|
return Err(error);
|
|
}
|
|
Ok(ModuleGraphHandle {
|
|
root_entry: root,
|
|
entries: entries.to_vec(),
|
|
entry_phases: HashMap::new(),
|
|
dependency_edges: dependency_edges.to_vec(),
|
|
})
|
|
}
|
|
|
|
fn mark_module_failed(&mut self, _key: ModuleMapKey, _error: ModuleLoadError) -> ModuleEntryId {
|
|
ModuleEntryId(0)
|
|
}
|
|
}
|
|
|
|
fn url(input: &str) -> Url {
|
|
Url::parse(input).expect("test URL should parse")
|
|
}
|
|
|
|
fn assert_waiting_for_owned_clients(poll: ModuleScriptTreePoll, expected_count: usize) {
|
|
let ModuleScriptTreePoll::WaitingForSingleModuleClients(wait) = poll else {
|
|
panic!("expected pending single-module clients");
|
|
};
|
|
assert_eq!(wait.client_count, expected_count);
|
|
}
|
|
|
|
fn assert_waiting_for_clients(poll: ModuleScriptTreePoll, expected_count: usize) {
|
|
let ModuleScriptTreePoll::WaitingForSingleModuleClients(wait) = poll else {
|
|
panic!("expected pending single-module clients");
|
|
};
|
|
assert_eq!(wait.client_count, expected_count);
|
|
}
|
|
|
|
fn key(input: &str) -> ModuleMapKey {
|
|
ModuleMapKey::javascript(url(input))
|
|
}
|
|
|
|
fn ready_outcome(key: ModuleMapKey, entry: ModuleEntryId) -> ModuleFetchOutcome {
|
|
ready_outcome_with_metadata(key, entry, ModuleFetchMetadata::default())
|
|
}
|
|
|
|
fn ready_outcome_with_metadata(
|
|
key: ModuleMapKey,
|
|
entry: ModuleEntryId,
|
|
metadata: ModuleFetchMetadata,
|
|
) -> ModuleFetchOutcome {
|
|
ModuleFetchOutcome::Ready(Box::new(ReadyModule::new(
|
|
entry,
|
|
key.clone(),
|
|
key.url.clone(),
|
|
metadata,
|
|
)))
|
|
}
|
|
|
|
fn fetched_outcome(key: ModuleMapKey, source: ModuleSource) -> ModuleFetchOutcome {
|
|
ModuleFetchOutcome::Fetched(Box::new(FetchedModuleSource::new(
|
|
key.clone(),
|
|
key.clone(),
|
|
key.url.clone(),
|
|
key.url.clone(),
|
|
source,
|
|
ModuleFetchMetadata::default(),
|
|
)))
|
|
}
|
|
|
|
fn request(specifier: &str, phase: ModuleImportPhase) -> ModuleRequestRecord {
|
|
ModuleRequestRecord {
|
|
specifier: specifier.to_owned(),
|
|
attributes: ModuleAttributesKey::empty(),
|
|
phase,
|
|
position: TextPosition { line: 1, column: 1 },
|
|
}
|
|
}
|
|
|
|
fn syntax_error(key: ModuleMapKey, message: &str) -> ModuleLoadError {
|
|
ModuleLoadError::new(ModuleLoadStage::Compile, message)
|
|
.with_key(key)
|
|
.with_error_constructor(ModuleErrorConstructorKind::SyntaxError)
|
|
}
|
|
|
|
fn external_root(source_url: Url, phase: ModuleImportPhase) -> ModuleRootInput {
|
|
ModuleRootInput::External(ModuleExternalRootInput {
|
|
base_url: source_url.clone(),
|
|
initiator_url: source_url.clone(),
|
|
source_url,
|
|
attributes: ModuleAttributesKey::empty(),
|
|
phase,
|
|
kind_hint: Some(ModuleKind::JavaScript),
|
|
fetch_metadata: ModuleFetchMetadata {
|
|
integrity: Some("sha256-root".to_owned()),
|
|
nonce: Some("nonce".to_owned()),
|
|
charset: Some("utf-8".to_owned()),
|
|
fetch_priority: FetchPriorityHint::High,
|
|
..ModuleFetchMetadata::default()
|
|
},
|
|
referrer: ModuleReferrer::client(),
|
|
position: TextPosition::default(),
|
|
})
|
|
}
|
|
|
|
fn inline_root(root_key: ModuleMapKey, root_entry: ModuleEntryId) -> ModuleRootInput {
|
|
inline_root_with_metadata(root_key, root_entry, ModuleFetchMetadata::default())
|
|
}
|
|
|
|
fn inline_root_with_metadata(
|
|
root_key: ModuleMapKey,
|
|
root_entry: ModuleEntryId,
|
|
fetch_metadata: ModuleFetchMetadata,
|
|
) -> ModuleRootInput {
|
|
ModuleRootInput::Inline(moli_module_script_tree::ModuleInlineRootInput {
|
|
root_key,
|
|
root_entry,
|
|
source_url: url("https://example.test/app/root.mjs"),
|
|
base_url: url("https://example.test/app/root.mjs"),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
fetch_metadata,
|
|
referrer: ModuleReferrer::client(),
|
|
position: TextPosition::default(),
|
|
})
|
|
}
|
|
|
|
fn job(root: ModuleRootInput) -> ModuleScriptTreeJob {
|
|
job_with_config(
|
|
root,
|
|
ModuleTreeConfig {
|
|
tree_id: ModuleTreeId(7),
|
|
..ModuleTreeConfig::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
fn job_with_config(root: ModuleRootInput, config: ModuleTreeConfig) -> ModuleScriptTreeJob {
|
|
ModuleScriptTreeJob::new(
|
|
root,
|
|
ModuleTreeConfig {
|
|
tree_id: ModuleTreeId(7),
|
|
..config
|
|
},
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_fetch_tree_external_root_returns_top_level_fetch() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let mut job = job(external_root(
|
|
root_url.clone(),
|
|
ModuleImportPhase::Evaluation,
|
|
));
|
|
let mut host = FakeHost::new();
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("external root should need fetch");
|
|
};
|
|
|
|
assert_eq!(fetches.len(), 1);
|
|
assert_eq!(fetches[0].source_url, root_url);
|
|
assert_eq!(fetches[0].graph_level, ModuleGraphLevel::TopLevel);
|
|
assert_eq!(fetches[0].render_blocking, RenderBlockingBehavior::Blocking);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_inline_root_skips_root_fetch_and_fetches_descendants() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("inline root should fetch descendants");
|
|
};
|
|
|
|
assert_eq!(fetches.len(), 1);
|
|
assert_eq!(fetches[0].key, child_key);
|
|
assert_eq!(fetches[0].graph_level, ModuleGraphLevel::Dependent);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_descendant_fetch_options_use_effective_parent_metadata() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let metadata = ModuleFetchMetadata {
|
|
credentials_mode: CredentialsMode::Include,
|
|
referrer_policy: ReferrerPolicy::StrictOrigin,
|
|
integrity: Some("sha256-parent".to_owned()),
|
|
nonce: Some("parent-nonce".to_owned()),
|
|
charset: Some("utf-8".to_owned()),
|
|
fetch_priority: FetchPriorityHint::High,
|
|
scheduler_priority: Some(ScriptFetchSchedulerPriority::VeryHigh),
|
|
request_context: ModuleRequestContext::Script,
|
|
parser_inserted: true,
|
|
..ModuleFetchMetadata::default()
|
|
};
|
|
let mut host = FakeHost::new();
|
|
host.entries.insert(
|
|
root_key.clone(),
|
|
FakeEntry::Ready {
|
|
entry: ModuleEntryId(1),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
deps: vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
metadata: metadata.clone(),
|
|
},
|
|
);
|
|
let mut job = job(inline_root_with_metadata(
|
|
root_key,
|
|
ModuleEntryId(1),
|
|
metadata,
|
|
));
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("dependency should fetch");
|
|
};
|
|
|
|
let child_metadata = &fetches[0].fetch_metadata;
|
|
assert_eq!(child_metadata.credentials_mode, CredentialsMode::Include);
|
|
assert_eq!(child_metadata.referrer_policy, ReferrerPolicy::StrictOrigin);
|
|
assert_eq!(child_metadata.integrity, None);
|
|
assert_eq!(child_metadata.nonce.as_deref(), Some("parent-nonce"));
|
|
assert_eq!(child_metadata.charset, None);
|
|
assert_eq!(child_metadata.fetch_priority, FetchPriorityHint::Auto);
|
|
assert_eq!(child_metadata.scheduler_priority, None);
|
|
assert!(child_metadata.parser_inserted);
|
|
assert_eq!(
|
|
fetches[0].render_blocking,
|
|
RenderBlockingBehavior::NonBlocking
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_source_phase_does_not_fetch_descendants() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Source));
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("root source phase should fetch root");
|
|
};
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
let result = ModuleFetchResult {
|
|
key: root_key.clone(),
|
|
client: fetches[0].client,
|
|
requested_phase: ModuleImportPhase::Source,
|
|
outcome: ready_outcome(root_key, ModuleEntryId(1)),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Complete(graph) =
|
|
job.resume_single_module(&mut host, fetches[0].client, result)
|
|
else {
|
|
panic!("source phase root should complete without descendants");
|
|
};
|
|
|
|
assert!(graph.dependency_edges.is_empty());
|
|
assert_eq!(host.started.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn network_fetch_source_is_compiled_by_tree_before_link() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
let mut job = job(external_root(
|
|
root_url.clone(),
|
|
ModuleImportPhase::Evaluation,
|
|
));
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("root should fetch");
|
|
};
|
|
let result = ModuleFetchResult {
|
|
key: root_key.clone(),
|
|
client: fetches[0].client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(root_key, ModuleSource::Text("export {};".into())),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Complete(graph) =
|
|
job.resume_single_module(&mut host, fetches[0].client, result)
|
|
else {
|
|
panic!("fetched source should be compiled and linked by the tree");
|
|
};
|
|
|
|
assert_eq!(graph.root_entry, ModuleEntryId(1));
|
|
assert_eq!(host.link_calls, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_module_map_fetching_entry_joins_without_new_fetch() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let poll = job.poll(&mut host);
|
|
|
|
assert_waiting_for_clients(poll, 1);
|
|
assert_eq!(host.started.len(), 0);
|
|
assert_eq!(job.pending_client_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_module_map_ready_hit_queues_completion_without_reentrant_link() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
host.ready(root_key.clone(), ModuleEntryId(1), Vec::new());
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Evaluation));
|
|
|
|
let poll = job.poll(&mut host);
|
|
|
|
assert_waiting_for_owned_clients(poll, 1);
|
|
assert_eq!(host.queued.len(), 1);
|
|
assert_eq!(host.link_calls, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn drive_consumes_ready_module_map_completion_inside_tree() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
host.ready(root_key, ModuleEntryId(1), Vec::new());
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Evaluation));
|
|
|
|
let ModuleScriptTreeDrive::Complete(graph) = job.drive(&mut host) else {
|
|
panic!("ready module map completion should be driven inside module tree");
|
|
};
|
|
|
|
assert_eq!(graph.root_entry, ModuleEntryId(1));
|
|
assert_eq!(host.link_calls, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn drive_returns_joined_fetch_waiters_without_renderer_side_channel() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreeDrive::WaitingForSingleModuleClients(wait) = job.drive(&mut host) else {
|
|
panic!("joined module map fetch should wait for owner completion");
|
|
};
|
|
|
|
assert_eq!(wait.client_count, 1);
|
|
assert_eq!(wait.joined_fetches.len(), 1);
|
|
assert_eq!(wait.joined_fetches[0].key, child_key);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_module_map_failed_entry_is_sticky() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let error = ModuleLoadError::new(ModuleLoadStage::Fetch, "network failed");
|
|
let mut host = FakeHost::new();
|
|
host.entries.insert(
|
|
root_key,
|
|
FakeEntry::Failed {
|
|
error: error.clone(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Evaluation));
|
|
assert_waiting_for_owned_clients(job.poll(&mut host), 1);
|
|
let queued = host.queued.remove(0);
|
|
|
|
let ModuleScriptTreePoll::Failed(returned) =
|
|
job.resume_single_module(&mut host, queued.client, queued)
|
|
else {
|
|
panic!("sticky failure should fail tree");
|
|
};
|
|
|
|
assert_eq!(returned, error);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_single_module_client_completion_is_ignored() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Evaluation));
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("root should fetch");
|
|
};
|
|
let stale_client = SingleModuleClientToken {
|
|
tree_id: fetches[0].client.tree_id,
|
|
sequence: fetches[0].client.sequence + 1,
|
|
};
|
|
let result = ModuleFetchResult {
|
|
key: root_key.clone(),
|
|
client: stale_client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ready_outcome(root_key, ModuleEntryId(1)),
|
|
};
|
|
|
|
let poll = job.resume_single_module(&mut host, stale_client, result);
|
|
|
|
assert_eq!(poll, ModuleScriptTreePoll::IgnoredStaleCompletion);
|
|
assert_eq!(job.pending_client_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn canceled_tree_ignores_late_single_module_completion() {
|
|
let root_url = url("https://example.test/app/root.mjs");
|
|
let root_key = key(root_url.as_str());
|
|
let mut host = FakeHost::new();
|
|
let mut job = job(external_root(root_url, ModuleImportPhase::Evaluation));
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("root should fetch");
|
|
};
|
|
assert_eq!(
|
|
job.cancel(ModuleTreeAbortReason::ExplicitCancel),
|
|
ModuleScriptTreePoll::Aborted(ModuleTreeAbortReason::ExplicitCancel)
|
|
);
|
|
let result = ModuleFetchResult {
|
|
key: root_key.clone(),
|
|
client: fetches[0].client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ready_outcome(root_key, ModuleEntryId(1)),
|
|
};
|
|
|
|
let poll = job.resume_single_module(&mut host, fetches[0].client, result);
|
|
|
|
assert_eq!(poll, ModuleScriptTreePoll::IgnoredStaleCompletion);
|
|
}
|
|
|
|
#[test]
|
|
fn chromium_fetch_descendants_can_issue_sibling_fetches_in_parallel() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./a.mjs", ModuleImportPhase::Evaluation),
|
|
request("./b.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("fanout should fetch siblings");
|
|
};
|
|
|
|
assert_eq!(fetches.len(), 2);
|
|
assert_eq!(
|
|
fetches[0].source_url.as_str(),
|
|
"https://example.test/app/a.mjs"
|
|
);
|
|
assert_eq!(
|
|
fetches[1].source_url.as_str(),
|
|
"https://example.test/app/b.mjs"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_error_waits_for_pending_sibling_fetches_before_failing() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let a_key = key("https://example.test/app/a.mjs");
|
|
let b_key = key("https://example.test/app/b.mjs");
|
|
let a_error = syntax_error(a_key.clone(), "a parse failed");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./a.mjs", ModuleImportPhase::Evaluation),
|
|
request("./b.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
host.fail_compile(a_key.clone(), a_error.clone());
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
let ModuleScriptTreePoll::NeedFetches(mut fetches) = job.poll(&mut host) else {
|
|
panic!("root should fetch sibling dependencies");
|
|
};
|
|
assert_eq!(fetches.len(), 2);
|
|
let a_index = fetches
|
|
.iter()
|
|
.position(|fetch| fetch.key == a_key)
|
|
.expect("a.mjs fetch should be present");
|
|
let a_fetch = fetches.remove(a_index);
|
|
|
|
let wait = job.resume_single_module(
|
|
&mut host,
|
|
a_fetch.client,
|
|
ModuleFetchResult {
|
|
key: a_key.clone(),
|
|
client: a_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(a_key.clone(), ModuleSource::Text("bad".into())),
|
|
},
|
|
);
|
|
assert_waiting_for_clients(wait, 1);
|
|
assert_eq!(
|
|
job.state(),
|
|
ModuleTreeState::FetchingDependencies,
|
|
"tree should keep waiting for already-issued sibling fetches"
|
|
);
|
|
|
|
let b_fetch = fetches.pop().expect("b.mjs fetch should remain");
|
|
assert_eq!(b_fetch.key, b_key);
|
|
let ModuleScriptTreePoll::Failed(error) = job.resume_single_module(
|
|
&mut host,
|
|
b_fetch.client,
|
|
ModuleFetchResult {
|
|
key: b_key.clone(),
|
|
client: b_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(b_key, ModuleSource::Text("export {};".into())),
|
|
},
|
|
) else {
|
|
panic!("tree should fail with delayed parse error after sibling completion");
|
|
};
|
|
|
|
assert_eq!(error, a_error);
|
|
assert_eq!(host.link_calls, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cached_parse_error_waits_for_pending_sibling_network_failure() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let parse_error_key = key("https://example.test/app/parse-error.mjs");
|
|
let network_error_key = key("https://example.test/app/network-error.mjs");
|
|
let parse_error = syntax_error(parse_error_key.clone(), "cached parse error");
|
|
let network_error = ModuleLoadError::new(ModuleLoadStage::Fetch, "sibling network failure")
|
|
.with_key(network_error_key.clone());
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./parse-error.mjs", ModuleImportPhase::Evaluation),
|
|
request("./network-error.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
host.entries.insert(
|
|
parse_error_key,
|
|
FakeEntry::Failed {
|
|
error: parse_error,
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreeDrive::NeedFetches(mut fetches) = job.drive(&mut host) else {
|
|
panic!("cached parse error should wait for the pending sibling fetch");
|
|
};
|
|
assert_eq!(fetches.fetches.len(), 1);
|
|
let network_fetch = fetches.fetches.pop().expect("network fetch should exist");
|
|
assert_eq!(network_fetch.key, network_error_key);
|
|
assert_eq!(job.pending_client_count(), 1);
|
|
|
|
let ModuleScriptTreePoll::Failed(error) = job.resume_single_module(
|
|
&mut host,
|
|
network_fetch.client,
|
|
ModuleFetchResult {
|
|
key: network_error_key,
|
|
client: network_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ModuleFetchOutcome::Failed(network_error.clone()),
|
|
},
|
|
) else {
|
|
panic!("network failure should take priority over the cached parse error");
|
|
};
|
|
|
|
assert_eq!(error, network_error);
|
|
assert_eq!(host.link_calls, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cached_parse_error_does_not_skip_successful_sibling_descendants() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let parse_error_key = key("https://example.test/app/parse-error.mjs");
|
|
let sibling_key = key("https://example.test/app/sibling.mjs");
|
|
let nested_network_error_key = key("https://example.test/app/nested-network-error.mjs");
|
|
let parse_error = syntax_error(parse_error_key.clone(), "cached parse error");
|
|
let nested_network_error =
|
|
ModuleLoadError::new(ModuleLoadStage::Fetch, "nested network failure")
|
|
.with_key(nested_network_error_key.clone());
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./parse-error.mjs", ModuleImportPhase::Evaluation),
|
|
request("./sibling.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
host.entries.insert(
|
|
parse_error_key,
|
|
FakeEntry::Failed {
|
|
error: parse_error,
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
let ModuleScriptTreeDrive::NeedFetches(mut root_fetches) = job.drive(&mut host) else {
|
|
panic!("cached parse error should wait for the successful sibling fetch");
|
|
};
|
|
let sibling_fetch = root_fetches
|
|
.fetches
|
|
.pop()
|
|
.expect("sibling fetch should exist");
|
|
assert_eq!(sibling_fetch.key, sibling_key);
|
|
host.ready(
|
|
sibling_key.clone(),
|
|
ModuleEntryId(2),
|
|
vec![request(
|
|
"./nested-network-error.mjs",
|
|
ModuleImportPhase::Evaluation,
|
|
)],
|
|
);
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(mut nested_fetches) = job.resume_single_module(
|
|
&mut host,
|
|
sibling_fetch.client,
|
|
ModuleFetchResult {
|
|
key: sibling_key.clone(),
|
|
client: sibling_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ready_outcome(sibling_key, ModuleEntryId(2)),
|
|
},
|
|
) else {
|
|
panic!("successful sibling descendants should still be discovered");
|
|
};
|
|
let nested_fetch = nested_fetches
|
|
.pop()
|
|
.expect("nested network fetch should exist");
|
|
assert_eq!(nested_fetch.key, nested_network_error_key);
|
|
|
|
let ModuleScriptTreePoll::Failed(error) = job.resume_single_module(
|
|
&mut host,
|
|
nested_fetch.client,
|
|
ModuleFetchResult {
|
|
key: nested_network_error_key,
|
|
client: nested_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ModuleFetchOutcome::Failed(nested_network_error.clone()),
|
|
},
|
|
) else {
|
|
panic!("nested network failure should take priority over the cached parse error");
|
|
};
|
|
|
|
assert_eq!(error, nested_network_error);
|
|
assert_eq!(host.link_calls, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_error_result_uses_module_discovery_order_not_completion_order() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let a_key = key("https://example.test/app/a.mjs");
|
|
let b_key = key("https://example.test/app/b.mjs");
|
|
let a_error = syntax_error(a_key.clone(), "a parse failed");
|
|
let b_error = syntax_error(b_key.clone(), "b parse failed");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./a.mjs", ModuleImportPhase::Evaluation),
|
|
request("./b.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
host.fail_compile(a_key.clone(), a_error.clone());
|
|
host.fail_compile(b_key.clone(), b_error);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
let ModuleScriptTreePoll::NeedFetches(mut fetches) = job.poll(&mut host) else {
|
|
panic!("root should fetch sibling dependencies");
|
|
};
|
|
assert_eq!(fetches.len(), 2);
|
|
let b_index = fetches
|
|
.iter()
|
|
.position(|fetch| fetch.key == b_key)
|
|
.expect("b.mjs fetch should be present");
|
|
let b_fetch = fetches.remove(b_index);
|
|
|
|
let wait = job.resume_single_module(
|
|
&mut host,
|
|
b_fetch.client,
|
|
ModuleFetchResult {
|
|
key: b_key.clone(),
|
|
client: b_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(b_key.clone(), ModuleSource::Text("bad b".into())),
|
|
},
|
|
);
|
|
assert_waiting_for_clients(wait, 1);
|
|
|
|
let a_fetch = fetches.pop().expect("a.mjs fetch should remain");
|
|
assert_eq!(a_fetch.key, a_key);
|
|
let ModuleScriptTreePoll::Failed(error) = job.resume_single_module(
|
|
&mut host,
|
|
a_fetch.client,
|
|
ModuleFetchResult {
|
|
key: a_key.clone(),
|
|
client: a_fetch.client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(a_key, ModuleSource::Text("bad a".into())),
|
|
},
|
|
) else {
|
|
panic!("tree should fail once all sibling fetches finish");
|
|
};
|
|
|
|
assert_eq!(error, a_error);
|
|
assert_eq!(host.link_calls, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn evaluation_phase_upgrades_source_phase_for_same_key() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![
|
|
request("./child.mjs", ModuleImportPhase::Source),
|
|
request("./child.mjs", ModuleImportPhase::Evaluation),
|
|
],
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("child should fetch once at strongest phase");
|
|
};
|
|
|
|
assert_eq!(fetches.len(), 1);
|
|
assert_eq!(fetches[0].key, child_key);
|
|
assert_eq!(fetches[0].phase, ModuleImportPhase::Evaluation);
|
|
}
|
|
|
|
#[test]
|
|
fn joined_fetch_completion_can_complete_graph() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
assert_waiting_for_clients(job.poll(&mut host), 1);
|
|
let client = host.take_single_client(&child_key);
|
|
host.ready(child_key.clone(), ModuleEntryId(2), Vec::new());
|
|
let result = ModuleFetchResult {
|
|
key: child_key.clone(),
|
|
client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ready_outcome(child_key, ModuleEntryId(2)),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Complete(graph) = job.resume_single_module(&mut host, client, result)
|
|
else {
|
|
panic!("joined fetch should complete graph");
|
|
};
|
|
|
|
assert_eq!(graph.root_entry, ModuleEntryId(1));
|
|
assert_eq!(graph.entries, vec![ModuleEntryId(1), ModuleEntryId(2)]);
|
|
}
|
|
|
|
#[test]
|
|
fn joined_fetch_ready_module_map_entry_advances_from_fanout_completion() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
assert_waiting_for_clients(job.poll(&mut host), 1);
|
|
let client = host.take_single_client(&child_key);
|
|
|
|
host.ready(child_key.clone(), ModuleEntryId(2), Vec::new());
|
|
let result = ModuleFetchResult {
|
|
key: child_key.clone(),
|
|
client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ready_outcome(child_key, ModuleEntryId(2)),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Complete(graph) = job.resume_single_module(&mut host, client, result)
|
|
else {
|
|
panic!("joined module map ready entry should complete graph from fanout completion");
|
|
};
|
|
|
|
assert_eq!(graph.root_entry, ModuleEntryId(1));
|
|
assert_eq!(graph.entries, vec![ModuleEntryId(1), ModuleEntryId(2)]);
|
|
}
|
|
|
|
#[test]
|
|
fn joined_fetch_fetched_module_map_entry_compiles_from_fanout_completion() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
assert_waiting_for_clients(job.poll(&mut host), 1);
|
|
let client = host.take_single_client(&child_key);
|
|
let result = ModuleFetchResult {
|
|
key: child_key.clone(),
|
|
client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: fetched_outcome(child_key, ModuleSource::Text("export {};".into())),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Complete(graph) = job.resume_single_module(&mut host, client, result)
|
|
else {
|
|
panic!("joined fetched module should compile and complete graph from fanout completion");
|
|
};
|
|
|
|
assert_eq!(graph.root_entry, ModuleEntryId(1));
|
|
assert_eq!(graph.entries, vec![ModuleEntryId(1), ModuleEntryId(2)]);
|
|
}
|
|
|
|
#[test]
|
|
fn joined_fetch_failed_module_map_entry_fails_from_fanout_completion() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let child_key = key("https://example.test/app/child.mjs");
|
|
let error = ModuleLoadError::new(ModuleLoadStage::Fetch, "child failed");
|
|
let mut host = FakeHost::new();
|
|
host.ready(
|
|
root_key.clone(),
|
|
ModuleEntryId(1),
|
|
vec![request("./child.mjs", ModuleImportPhase::Evaluation)],
|
|
);
|
|
host.entries.insert(
|
|
child_key.clone(),
|
|
FakeEntry::Fetching {
|
|
clients: Vec::new(),
|
|
phase: ModuleImportPhase::Evaluation,
|
|
},
|
|
);
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
assert_waiting_for_clients(job.poll(&mut host), 1);
|
|
let client = host.take_single_client(&child_key);
|
|
let result = ModuleFetchResult {
|
|
key: child_key,
|
|
client,
|
|
requested_phase: ModuleImportPhase::Evaluation,
|
|
outcome: ModuleFetchOutcome::Failed(error.clone()),
|
|
};
|
|
|
|
let ModuleScriptTreePoll::Failed(returned) =
|
|
job.resume_single_module(&mut host, client, result)
|
|
else {
|
|
panic!("joined module map failed entry should fail graph from fanout completion");
|
|
};
|
|
|
|
assert_eq!(returned, error);
|
|
}
|
|
|
|
#[test]
|
|
fn host_link_error_is_propagated_without_tree_specific_error_rewrite() {
|
|
let root_key = key("https://example.test/app/root.mjs");
|
|
let error = ModuleLoadError::new(ModuleLoadStage::Link, "link failed");
|
|
let mut host = FakeHost::new();
|
|
host.fail_link = Some(error.clone());
|
|
host.ready(root_key.clone(), ModuleEntryId(1), Vec::new());
|
|
let mut job = job(inline_root(root_key, ModuleEntryId(1)));
|
|
|
|
let ModuleScriptTreePoll::Failed(returned) = job.poll(&mut host) else {
|
|
panic!("link error should fail tree");
|
|
};
|
|
|
|
assert_eq!(returned, error);
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_module_script_owner_metadata_is_preserved_in_fetch_request() {
|
|
let root_url = url("https://example.test/app/runtime-module.mjs");
|
|
let mut job = ModuleScriptTreeJob::new(
|
|
external_root(root_url, ModuleImportPhase::Evaluation),
|
|
ModuleTreeConfig {
|
|
tree_id: ModuleTreeId(10),
|
|
owner: ModuleTreeOwner::runtime_module_script(),
|
|
..ModuleTreeConfig::default()
|
|
},
|
|
);
|
|
let mut host = FakeHost::new();
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("runtime module script should fetch root");
|
|
};
|
|
|
|
assert_eq!(
|
|
fetches[0].requester,
|
|
moli_module_script_tree::ModuleFetchRequester::RuntimeModuleScript
|
|
);
|
|
assert_eq!(
|
|
fetches[0].ordering,
|
|
moli_module_script_tree::ModuleFetchOrdering::Runtime
|
|
);
|
|
assert_eq!(job.state(), ModuleTreeState::FetchingRoot);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_import_owner_metadata_is_preserved_in_fetch_request() {
|
|
let root_url = url("https://example.test/app/dynamic.mjs");
|
|
let mut job = ModuleScriptTreeJob::new(
|
|
external_root(root_url, ModuleImportPhase::Evaluation),
|
|
ModuleTreeConfig {
|
|
tree_id: ModuleTreeId(9),
|
|
owner: ModuleTreeOwner::dynamic_import(),
|
|
..ModuleTreeConfig::default()
|
|
},
|
|
);
|
|
let mut host = FakeHost::new();
|
|
|
|
let ModuleScriptTreePoll::NeedFetches(fetches) = job.poll(&mut host) else {
|
|
panic!("dynamic import should fetch root");
|
|
};
|
|
|
|
assert_eq!(
|
|
fetches[0].requester,
|
|
moli_module_script_tree::ModuleFetchRequester::DynamicImport
|
|
);
|
|
assert_eq!(
|
|
fetches[0].ordering,
|
|
moli_module_script_tree::ModuleFetchOrdering::Runtime
|
|
);
|
|
assert_eq!(job.state(), ModuleTreeState::FetchingRoot);
|
|
}
|