From 41301ac9b94ff720fff1e351ce34c036e4eea4fc Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 28 Sep 2026 01:46:47 +0800 Subject: [PATCH] fix(modules): reuse module maps for runtime script roots Consult the module map before fetching runtime module script roots while preserving script-element Service Worker filtering and integrity checks. Keep shared fetches alive across document.open so imports in the retained realm settle without reviving retired script elements or their events. --- moli-core/tests/scripts/integrity.rs | 20 ++ .../tests/scripts/runtime_modulepreload.rs | 198 ++++++++++++++++++ moli-renderer-v8/src/dynamic_script_owner.rs | 17 +- moli-renderer-v8/src/module_runtime.rs | 4 +- .../src/module_runtime/graph_fetch_store.rs | 4 - moli-renderer-v8/src/module_runtime/tests.rs | 15 -- .../src/renderer_resource_scheduler.rs | 88 ++++++++ moli-renderer-v8/src/runtime/mod.rs | 4 +- moli-renderer-v8/src/runtime/page_vm/mod.rs | 4 +- .../page_vm/page_resource_completion.rs | 2 +- .../page_resource_completion/main_module.rs | 47 +++++ .../script_vm/native_module/module_fetch.rs | 37 ++++ 12 files changed, 410 insertions(+), 30 deletions(-) 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(),