diff --git a/moli-core/tests/scripts/integrity.rs b/moli-core/tests/scripts/integrity.rs
index 31be169631..4c9637e79e 100644
--- a/moli-core/tests/scripts/integrity.rs
+++ b/moli-core/tests/scripts/integrity.rs
@@ -52,12 +52,16 @@ impl IntegrityServers {
let origin = format!("http://{}", main.local_addr()?);
let cross_origin = format!("http://{}", cross.local_addr()?);
let requests = Arc::new(Mutex::new(Vec::new()));
+ let module_started = Arc::new(tokio::sync::Notify::new());
+ let module_release = Arc::new(tokio::sync::Semaphore::new(0));
let tasks = [main, cross]
.into_iter()
.map(|listener| {
let origin = origin.clone();
let cross_origin = cross_origin.clone();
let requests = Arc::clone(&requests);
+ let module_started = Arc::clone(&module_started);
+ let module_release = Arc::clone(&module_release);
tokio::spawn(async move {
let mut connections = JoinSet::new();
loop {
@@ -67,6 +71,8 @@ impl IntegrityServers {
let origin = origin.clone();
let cross_origin = cross_origin.clone();
let requests = Arc::clone(&requests);
+ let module_started = Arc::clone(&module_started);
+ let module_release = Arc::clone(&module_release);
connections.spawn(async move {
let mut request = Vec::new();
while !request.ends_with(b"\r\n\r\n") && request.len() < 16384 {
@@ -87,6 +93,15 @@ impl IntegrityServers {
};
let (status, headers, body) = fixture_response(&incoming, &origin, &cross_origin);
requests.lock().push(incoming);
+ // Hold a module response across document.open without a timed sleep.
+ if path == "/echo-origin.js?consumer-open" {
+ module_started.notify_one();
+ let _ = module_release.acquire().await;
+ } else if path == "/module-response-started" {
+ module_started.notified().await;
+ } else if path == "/module-response-release" {
+ module_release.close();
+ }
let response = format!(
"HTTP/1.1 {status}\r\n{headers}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len(),
@@ -147,6 +162,11 @@ fn fixture_response(
let (path, query) = request.path.split_once('?').unwrap_or((&request.path, ""));
let javascript = "Content-Type: text/javascript\r\n";
match path {
+ "/module-response-started" | "/module-response-release" => (
+ "200 OK",
+ "Content-Type: text/plain\r\nCache-Control: no-store\r\n".to_owned(),
+ String::new(),
+ ),
"/runtime-modulepreload.html" => (
"200 OK",
"Content-Type: text/html\r\n".to_owned(),
diff --git a/moli-core/tests/scripts/runtime_modulepreload.rs b/moli-core/tests/scripts/runtime_modulepreload.rs
index e8e7c091d3..ee0ca3026a 100644
--- a/moli-core/tests/scripts/runtime_modulepreload.rs
+++ b/moli-core/tests/scripts/runtime_modulepreload.rs
@@ -115,6 +115,204 @@ async fn runtime_modulepreloads_fetch_after_load_and_reuse_the_child_module_map(
check_runtime_modulepreloads(true).await
}
+#[tokio::test(flavor = "multi_thread")]
+async fn module_script_consumers_share_the_main_module_map() -> Result<()> {
+ let servers = IntegrityServers::spawn().await?;
+ let browser = Browser::new(AppConfig::default())?;
+ let mut page = browser
+ .fetch(&format!("{}/page.html", servers.origin))
+ .await?;
+ let probe = r#"(async () => {
+ const check = (value, message) => { if (!value) throw new Error(message); };
+ const attach = (tag, url, attributes = {}) => new Promise((resolve, reject) => {
+ const element = document.createElement(tag);
+ if (tag === 'link') { element.rel = 'modulepreload'; element.href = url; }
+ else { element.type = 'module'; element.src = url; }
+ Object.assign(element, attributes);
+ const timeout = setTimeout(() => reject(new Error('no event: ' + url)), 3000);
+ element.onload = element.onerror = event => { clearTimeout(timeout); resolve(event.type); };
+ document.head.append(element);
+ });
+ for (const [name, warmup, ordered] of [
+ ['preloaded', 'preload', false],
+ ['ordered', 'preload', true],
+ ['credentials', 'preload', false],
+ ['integrity', 'preload', false],
+ ['imported', 'import', false],
+ ['evaluated', 'script', false],
+ ['concurrent', 'concurrent-preload', false],
+ ['cold', 'none', false]
+ ]) {
+ const url = '/echo-origin.js?consumer-' + name;
+ const before = globalThis.sriExecutions || 0;
+ const pending = [];
+ if (warmup === 'preload') {
+ check(await attach('link', url, {crossOrigin: 'anonymous'}) === 'load', name + ' preload');
+ check((globalThis.sriExecutions || 0) === before, name + ' premature evaluation');
+ } else if (warmup === 'import') await import(url);
+ else if (warmup === 'script') check(await attach('script', url) === 'load', name + ' first script');
+ else if (warmup === 'concurrent-preload') pending.push(attach('link', url));
+ const attributes = {async: !ordered};
+ if (name === 'credentials') attributes.crossOrigin = 'use-credentials';
+ // The already-fetched module is reused before new fetch options are examined.
+ if (name === 'integrity') attributes.integrity = 'sha384-AAAA';
+ pending.push(attach('script', url, attributes), attach('script', url, attributes));
+ check((await Promise.all(pending)).every(event => event === 'load'), name + ' terminal events');
+ check(sriExecutions === before + 1, name + ' must evaluate exactly once');
+ const namespace = await import(url);
+ check(namespace === await import(url), name + ' namespace identity');
+ check(sriExecutions === before + 1, name + ' import must reuse evaluation');
+ }
+ // Fetch policy must still be enforced on roots that are absent from the map.
+ check(await attach('script', '/echo-origin.js?consumer-bad-sri', {integrity:'sha384-AAAA'}) === 'error', 'fresh SRI');
+ check(await attach('script', '/missing.js?consumer-missing') === 'error', 'fresh 404');
+ const beforeClassic = sriExecutions;
+ for (let i = 0; i < 2; i++)
+ check(await attach('script', '/echo-origin.js?consumer-classic', {type:''}) === 'load', 'classic load');
+ check(sriExecutions === beforeClassic + 2, 'classic scripts must each evaluate');
+ return true;
+ })()"#;
+ let result = tokio::time::timeout(
+ Duration::from_secs(15),
+ page.evaluate_runtime_expression_with_await_async(probe, true),
+ )
+ .await??;
+ assert_eq!(
+ result["value"],
+ true,
+ "{result}; requests={:?}",
+ servers.requests.lock()
+ );
+ let requests = servers.requests.lock();
+ for name in [
+ "preloaded",
+ "ordered",
+ "credentials",
+ "integrity",
+ "imported",
+ "evaluated",
+ "concurrent",
+ "cold",
+ "bad-sri",
+ "missing",
+ "classic",
+ ] {
+ let path = if name == "missing" {
+ "/missing.js?consumer-missing".to_owned()
+ } else {
+ format!("/echo-origin.js?consumer-{name}")
+ };
+ let matching: Vec<_> = requests
+ .iter()
+ .filter(|request| request.path == path)
+ .collect();
+ assert_eq!(
+ matching.len(),
+ if name == "classic" { 2 } else { 1 },
+ "{path}: {requests:?}"
+ );
+ assert_eq!(
+ matching[0].sec_fetch_dest.as_deref(),
+ Some("script"),
+ "{path}"
+ );
+ assert_eq!(
+ matching[0].sec_fetch_mode.as_deref(),
+ Some(if name == "classic" { "no-cors" } else { "cors" }),
+ "{path}"
+ );
+ }
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn module_script_map_reservations_preserve_service_worker_responses() -> Result<()> {
+ let servers = IntegrityServers::spawn().await?;
+ let browser = Browser::new(AppConfig::default())?;
+ let mut page = browser
+ .fetch(&format!("{}/page.html", servers.origin))
+ .await?;
+ let cases = serde_json::json!([
+ {"name":"synthetic", "src":"/sw-default.js", "type":"module", "integrity":INTEGRITY, "expected":"load"},
+ {"name":"cors", "src":"/sw-cors.js", "type":"module", "integrity":INTEGRITY, "expected":"load"},
+ {"name":"opaque", "src":"/sw-opaque.js", "type":"module", "expected":"error"}
+ ]);
+ let result = tokio::time::timeout(
+ Duration::from_secs(15),
+ page.evaluate_runtime_expression_with_await_async(&dynamic_probe(&cases, true), true),
+ )
+ .await??;
+ assert_integrity_results(result, &cases);
+ let result = tokio::time::timeout(
+ Duration::from_secs(10),
+ page.evaluate_runtime_expression_with_await_async(
+ r#"(async () => {
+ const before = sriExecutions;
+ await import('/sw-default.js');
+ const script = document.createElement('script');
+ script.type = 'module'; script.src = '/sw-default.js';
+ await new Promise((resolve, reject) => {
+ script.onload = resolve; script.onerror = reject; document.head.append(script);
+ });
+ return sriExecutions === before;
+ })()"#,
+ true,
+ ),
+ )
+ .await??;
+ assert_eq!(result["value"], true, "{result}");
+ assert!(
+ !servers
+ .requests
+ .lock()
+ .iter()
+ .any(|request| request.path.starts_with("/sw-")),
+ "intercepted roots must not reach the network"
+ );
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn module_script_fetch_settles_the_retained_map_after_document_open() -> Result<()> {
+ let servers = IntegrityServers::spawn().await?;
+ let browser = Browser::new(AppConfig::default())?;
+ let mut page = browser
+ .fetch(&format!("{}/page.html", servers.origin))
+ .await?;
+ let result = tokio::time::timeout(Duration::from_secs(15),
+ page.evaluate_runtime_expression_with_await_async(r#"(async () => {
+ const url = '/echo-origin.js?consumer-open';
+ globalThis.retiredModuleEvents = 0;
+ const script = document.createElement('script'); script.type = 'module'; script.src = url;
+ script.onload = script.onerror = () => retiredModuleEvents++;
+ document.head.append(script);
+ await fetch('/module-response-started');
+ document.open(); document.write('
replacement'); document.close();
+ const imported = import(url);
+ await fetch('/module-response-release');
+ await imported;
+ return JSON.stringify({executions: sriExecutions, retiredEvents: retiredModuleEvents,
+ entries: performance.getEntriesByName(new URL(url, location.href).href).length});
+ })()"#, true),
+ ).await??;
+ assert!(result["value"].is_string(), "{result}");
+ let actual: serde_json::Value = serde_json::from_str(result["value"].as_str().unwrap())?;
+ assert_eq!(
+ actual,
+ serde_json::json!({"executions":1,"retiredEvents":0,"entries":0})
+ );
+ assert_eq!(
+ servers
+ .requests
+ .lock()
+ .iter()
+ .filter(|request| request.path == "/echo-origin.js?consumer-open")
+ .count(),
+ 1
+ );
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread")]
async fn runtime_modulepreloads_keep_document_maps_and_retire_queued_child_starts() -> Result<()> {
let servers = IntegrityServers::spawn().await?;
diff --git a/moli-renderer-v8/src/dynamic_script_owner.rs b/moli-renderer-v8/src/dynamic_script_owner.rs
index 52f1bb087e..1c1ffc4143 100644
--- a/moli-renderer-v8/src/dynamic_script_owner.rs
+++ b/moli-renderer-v8/src/dynamic_script_owner.rs
@@ -1478,7 +1478,9 @@ impl DynamicScriptOwner {
load_delay_binding: Option,
) {
let ready_state = match script.source_kind {
- ScriptSourceKind::External => {
+ ScriptSourceKind::External
+ if !crate::script_vm::prepared_script_uses_external_module_graph(&script) =>
+ {
let tx = self.owner_event_sender();
let loader = loader.clone();
let script_for_load = script.clone();
@@ -1516,10 +1518,15 @@ impl DynamicScriptOwner {
});
DynamicScriptReadyState::Loading
}
- ScriptSourceKind::Inline => DynamicScriptReadyState::Ready {
- order: self.next_ready_order(),
- source_network_result: None,
- },
+ // Module roots must consult the Document's module map before any
+ // transport runs. The graph owner starts or joins that fetch and
+ // retains this script's ordering and load-delay lease.
+ ScriptSourceKind::Inline | ScriptSourceKind::External => {
+ DynamicScriptReadyState::Ready {
+ order: self.next_ready_order(),
+ source_network_result: None,
+ }
+ }
};
self.insert_script_with_ready_state(
diff --git a/moli-renderer-v8/src/module_runtime.rs b/moli-renderer-v8/src/module_runtime.rs
index 2d689d9826..7fb5bc7c22 100644
--- a/moli-renderer-v8/src/module_runtime.rs
+++ b/moli-renderer-v8/src/module_runtime.rs
@@ -372,7 +372,9 @@ impl ModuleOwnerState {
// dynamic-import resolver state belong to the live ScriptState. Only
// parser/script-element work belongs to the replaced Document.
self.document_modulator.clear_for_document_replacement();
- self.graph_fetches.clear();
+ // Network fetches populate the retained ScriptState module map. Their
+ // initiating script elements retire here, but imports in the same realm
+ // must still receive the result (as they do for modulepreload).
self.owner_event_tasks.update_ready_tasks(|events| {
for event in events.iter_mut() {
event.retain_for_document_replacement();
diff --git a/moli-renderer-v8/src/module_runtime/graph_fetch_store.rs b/moli-renderer-v8/src/module_runtime/graph_fetch_store.rs
index eb40e903db..9370228f53 100644
--- a/moli-renderer-v8/src/module_runtime/graph_fetch_store.rs
+++ b/moli-renderer-v8/src/module_runtime/graph_fetch_store.rs
@@ -25,10 +25,6 @@ impl fmt::Debug for NativeModuleGraphFetchStore {
}
impl NativeModuleGraphFetchStore {
- pub(crate) fn clear(&mut self) {
- self.inflight_module_script_fetches.clear();
- }
-
pub(crate) fn suspend_fetch(
&mut self,
continuation: ModuleScriptGraphFetchContinuation,
diff --git a/moli-renderer-v8/src/module_runtime/tests.rs b/moli-renderer-v8/src/module_runtime/tests.rs
index f811030070..47acb5d643 100644
--- a/moli-renderer-v8/src/module_runtime/tests.rs
+++ b/moli-renderer-v8/src/module_runtime/tests.rs
@@ -5,7 +5,6 @@ use url::Url;
use crate::dom::native::NativeNodeId;
use moli_module_script_tree as module_tree;
-use super::graph_fetch_store::NativeModuleGraphFetchStore;
use super::modulator::NativeDocumentModulator as NativeDocumentModulatorUnderTest;
use super::{
DynamicModuleFetchOwnerAdvance, ImportMapRegistryState, ModuleGraphHandle, ModuleLoadError,
@@ -57,20 +56,6 @@ fn import_map_registry_state_merges_late_maps_without_remapping_resolved_specifi
);
}
-#[test]
-fn native_module_graph_fetch_store_clear_does_not_reuse_inflight_fetch_load_ids() {
- let mut store = NativeModuleGraphFetchStore::default();
- let first = store.reserve_load_id();
-
- store.clear();
-
- let after_clear = store.reserve_load_id();
- assert_ne!(
- first, after_clear,
- "stale fetch completions can arrive after clear; load ids must stay unique"
- );
-}
-
#[test]
fn module_owner_document_replacement_preserves_script_state_module_environment() {
let mut owner = ModuleOwnerState::default();
diff --git a/moli-renderer-v8/src/renderer_resource_scheduler.rs b/moli-renderer-v8/src/renderer_resource_scheduler.rs
index 31d43edddf..eb0aa6bc9d 100644
--- a/moli-renderer-v8/src/renderer_resource_scheduler.rs
+++ b/moli-renderer-v8/src/renderer_resource_scheduler.rs
@@ -154,6 +154,94 @@ impl RendererResourceScheduler {
);
}
+ pub(crate) fn schedule_main_runtime_module_root_fetch(
+ &self,
+ loader: DocumentResourceLoader,
+ target: MainRuntimeModuleGraphFetchTarget,
+ script: crate::planning::PreparedScript,
+ service_worker: crate::dynamic_script_owner::DynamicScriptServiceWorkerContext,
+ ) {
+ let schedule = MainModuleFetchSchedule::Runtime(target);
+ let attribution = MainModuleFetchNetworkAttribution::new(
+ service_worker.document_url.clone(),
+ script.url.clone(),
+ );
+ let Some(load) = loader.register_load(
+ crate::network::loads::ResourceLoadKind::Script,
+ crate::network::loads::ResourceLoadDisposition::Ordinary,
+ None,
+ ) else {
+ let error = "Document detached before module root fetch registration".to_owned();
+ schedule.send_completion(
+ &self.completion_tx,
+ Err(error.clone()),
+ Some(Arc::new(Err(error))),
+ attribution,
+ );
+ return;
+ };
+ let completion_tx = self.completion_tx.clone();
+ let request_client = load.request_client();
+ let request_origin = loader.fetch_context().request_origin();
+ let runner = load.task_runner();
+ let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
+ load.attach_consumer_cancel(move || {
+ let _ = cancel_tx.send(());
+ });
+ runner.clone().spawn(async move {
+ // This is the script-element transport previously used before map
+ // lookup. Keep its Service Worker response filtering and SRI checks,
+ // but publish the result to the graph's single-module reservation.
+ let outcome = tokio::select! {
+ outcome = crate::planning::load_service_worker_aware_external_script_source_outcome(
+ &script, &request_origin, &request_client, runner, None, None,
+ service_worker.browser_context_runtime, service_worker.client_id,
+ service_worker.document_url,
+ ) => outcome,
+ _ = cancel_rx => {
+ let error = "module root fetch cancelled".to_owned();
+ load.finish();
+ schedule.send_completion(
+ &completion_tx, Err(error.clone()), Some(Arc::new(Err(error))), attribution,
+ );
+ return;
+ }
+ };
+ load.finish();
+ let result = outcome.source_result.map(|source| {
+ let response = outcome
+ .network_result
+ .as_deref()
+ .and_then(|result| result.as_ref().ok());
+ let (url, redirected, referrer_policy) = response.map_or_else(
+ || (script.url.clone(), false, None),
+ |response| {
+ (
+ response.final_url.clone(),
+ response.redirected,
+ crate::referrer_policy::response_referrer_policy_from_headers(
+ &response.headers,
+ ),
+ )
+ },
+ );
+ ModuleGraphFetchedSource::new(
+ url,
+ redirected,
+ outcome
+ .source_bytes
+ .filter(|_| crate::planning::is_webassembly_module_script_url(&script.url))
+ .map_or_else(
+ || crate::module_runtime::ModuleSource::text(source),
+ crate::module_runtime::ModuleSource::binary,
+ ),
+ )
+ .with_response_referrer_policy(referrer_policy)
+ });
+ schedule.send_completion(&completion_tx, result, outcome.network_result, attribution);
+ });
+ }
+
pub(crate) fn schedule_main_modulepreload_fetch(
&self,
loader: DocumentResourceLoader,
diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs
index 1a6dd85b2a..7986f0cac9 100644
--- a/moli-renderer-v8/src/runtime/mod.rs
+++ b/moli-renderer-v8/src/runtime/mod.rs
@@ -428,8 +428,8 @@ pub(crate) use self::page_vm::{
AuthorizedCurrentMainParserModuleGraphFetchCompletion,
AuthorizedCurrentMainRuntimeModuleGraphFetchCompletion,
AuthorizedCurrentPopupClassicScriptLoadCompletion,
- AuthorizedCurrentPopupDocumentLoadCompletion, AuthorizedLiveMainModulepreloadFetchCompletion,
- CurrentChildDocumentLoadApplication,
+ AuthorizedCurrentPopupDocumentLoadCompletion, AuthorizedLiveMainModuleGraphFetchCompletion,
+ AuthorizedLiveMainModulepreloadFetchCompletion, CurrentChildDocumentLoadApplication,
};
pub(in crate::runtime) use self::page_vm::{
PageVmCommittedNavigationBootstrap, PageVmDocumentCommitPreparation,
diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs
index ca9e4d5e86..09206cdd81 100644
--- a/moli-renderer-v8/src/runtime/page_vm/mod.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs
@@ -247,8 +247,8 @@ pub(crate) use page_resource_completion::{
AuthorizedCurrentMainParserModuleGraphFetchCompletion,
AuthorizedCurrentMainRuntimeModuleGraphFetchCompletion,
AuthorizedCurrentPopupClassicScriptLoadCompletion,
- AuthorizedCurrentPopupDocumentLoadCompletion, AuthorizedLiveMainModulepreloadFetchCompletion,
- CurrentChildDocumentLoadApplication,
+ AuthorizedCurrentPopupDocumentLoadCompletion, AuthorizedLiveMainModuleGraphFetchCompletion,
+ AuthorizedLiveMainModulepreloadFetchCompletion, CurrentChildDocumentLoadApplication,
};
pub(crate) use page_script_preparation_error::AuthorizedCurrentPageScriptPreparationError;
pub(crate) use page_service_worker_client_message::AuthorizedCurrentPageServiceWorkerClientMessage;
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_resource_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_resource_completion.rs
index e1a5db3fb4..87f42c8228 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_resource_completion.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_resource_completion.rs
@@ -27,7 +27,7 @@ pub(crate) use main_module::{
AuthorizedCurrentMainDynamicImportGraphFetchCompletion,
AuthorizedCurrentMainParserModuleGraphFetchCompletion,
AuthorizedCurrentMainRuntimeModuleGraphFetchCompletion,
- AuthorizedLiveMainModulepreloadFetchCompletion,
+ AuthorizedLiveMainModuleGraphFetchCompletion, AuthorizedLiveMainModulepreloadFetchCompletion,
};
impl PageVm {
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_resource_completion/main_module.rs b/moli-renderer-v8/src/runtime/page_vm/page_resource_completion/main_module.rs
index 0755bac4d1..b461fe7be8 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_resource_completion/main_module.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_resource_completion/main_module.rs
@@ -45,6 +45,25 @@ impl AuthorizedCurrentMainRuntimeModuleGraphFetchCompletion {
}
}
+/// Proof that a retired script's network terminal still belongs to an in-flight
+/// module-map request in the same root Page/ScriptState. It authorizes cache
+/// settlement only, without restoring the script element or its Document tasks.
+pub(crate) struct AuthorizedLiveMainModuleGraphFetchCompletion {
+ load_id: u64,
+ result: std::result::Result,
+}
+
+impl AuthorizedLiveMainModuleGraphFetchCompletion {
+ pub(crate) fn into_parts(
+ self,
+ ) -> (
+ u64,
+ std::result::Result,
+ ) {
+ (self.load_id, self.result)
+ }
+}
+
/// Proof that the Page lane executor matched one dynamic-import network
/// terminal against its complete `{root Document, import owner, load id}`
/// target.
@@ -95,6 +114,20 @@ impl PageVm {
completion.network_attribution().request_url(),
completion.network_result(),
);
+ if owner.root_document() == self.document_lifecycle.identity().document
+ && self
+ .vm()
+ .document_runtime
+ .has_inflight_native_module_script_fetch(completion.target().load_id())
+ {
+ self.vm_mut()
+ .apply_live_main_module_graph_fetch_completion(
+ AuthorizedLiveMainModuleGraphFetchCompletion {
+ load_id: completion.target().load_id(),
+ result: completion.into_result(),
+ },
+ )?;
+ }
return Ok(PageResourceCompletionTurnAction::discarded_stale(
source,
owner,
@@ -132,6 +165,20 @@ impl PageVm {
completion.network_attribution().request_url(),
completion.network_result(),
);
+ if owner.root_document() == self.document_lifecycle.identity().document
+ && self
+ .vm()
+ .document_runtime
+ .has_inflight_native_module_script_fetch(completion.target().load_id())
+ {
+ self.vm_mut()
+ .apply_live_main_module_graph_fetch_completion(
+ AuthorizedLiveMainModuleGraphFetchCompletion {
+ load_id: completion.target().load_id(),
+ result: completion.into_result(),
+ },
+ )?;
+ }
return Ok(PageResourceCompletionTurnAction::discarded_stale(
source,
owner,
diff --git a/moli-renderer-v8/src/script_vm/native_module/module_fetch.rs b/moli-renderer-v8/src/script_vm/native_module/module_fetch.rs
index 3d17a938eb..247f8dbc73 100644
--- a/moli-renderer-v8/src/script_vm/native_module/module_fetch.rs
+++ b/moli-renderer-v8/src/script_vm/native_module/module_fetch.rs
@@ -523,6 +523,14 @@ impl ScriptVm {
}
Ok(())
}
+ pub(crate) fn apply_live_main_module_graph_fetch_completion(
+ &mut self,
+ authorization: crate::runtime::AuthorizedLiveMainModuleGraphFetchCompletion,
+ ) -> Result<()> {
+ let (load_id, result) = authorization.into_parts();
+ self.complete_shared_module_map_fetch_result(load_id, result)
+ }
+
#[cfg(test)]
pub(super) fn complete_abandoned_module_script_graph_fetch(
&mut self,
@@ -1109,6 +1117,11 @@ impl ScriptVm {
load_ids.push(load_id);
scheduled.push((load_id, request));
}
+ let mut root_script = (matches!(fetch_schedule_owner, FetchScheduleOwner::Runtime { .. })
+ && scheduled
+ .iter()
+ .any(|(_, request)| request.is_top_level_tree_fetch()))
+ .then(|| continuation.script.clone());
let continuation = continuation.with_pending_graph_fetches(job, load_ids.first().copied());
tracing::debug!(
url = %continuation.script.url,
@@ -1146,6 +1159,30 @@ impl ScriptVm {
document_owner,
dynamic_script_owner_id,
} => {
+ if request.is_top_level_tree_fetch() {
+ let service_worker = {
+ let host = self._context_host.borrow();
+ crate::dynamic_script_owner::DynamicScriptServiceWorkerContext {
+ browser_context_runtime: host.browser_context_runtime(),
+ client_id: host.service_worker_client_id_for_window_fetch(None),
+ document_url: self.document_runtime.document_url().clone(),
+ }
+ };
+ self.resource_scheduler()
+ .schedule_main_runtime_module_root_fetch(
+ document_loader.clone(),
+ crate::page_resource_completion::MainRuntimeModuleGraphFetchTarget::new(
+ document_owner,
+ dynamic_script_owner_id,
+ load_id,
+ ),
+ root_script
+ .take()
+ .expect("runtime root fetch retains its prepared script"),
+ service_worker,
+ );
+ continue;
+ }
self.resource_scheduler()
.schedule_main_runtime_module_graph_fetch(
document_loader.clone(),