From 7cf5dfbb34dd37bd2c8e8e75b070efd50adc4f86 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 23 Aug 2026 04:10:13 +0800 Subject: [PATCH] fix(renderer): isolate stale import completions --- moli-renderer-v8/src/document_runtime.rs | 4 +- .../runtime/page_vm/tests/stylesheet_task.rs | 125 ++++++++++++++++++ .../src/script_vm/stylesheet_page_tasks.rs | 10 +- .../src/stylesheet_runtime/completion.rs | 10 +- .../stylesheet_runtime/linked_import_graph.rs | 19 +++ .../src/stylesheet_runtime/mod.rs | 5 +- .../src/stylesheet_runtime/test_driver.rs | 17 ++- 7 files changed, 177 insertions(+), 13 deletions(-) diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs index 2502f84375..e789e7cd0b 100644 --- a/moli-renderer-v8/src/document_runtime.rs +++ b/moli-renderer-v8/src/document_runtime.rs @@ -93,8 +93,8 @@ pub(crate) use stylesheet_runtime::attribute_reprocesses_connected_stylesheet; use stylesheet_runtime::{ConnectedLinkReadinessFetchOptions, ConnectedLoadParameters}; pub(crate) use stylesheet_runtime::{ ConnectedLoadCompletion, LinkedStylesheetImportGraphCompletion, - LiveStylesheetImportLoadCompletion, fetch_complete_stylesheet_import_graph, - live_stylesheet_import_responses, + LiveStylesheetImportLoadCompletion, StylesheetImportCompletionAuthority, + fetch_complete_stylesheet_import_graph, live_stylesheet_import_responses, }; use stylesheet_runtime::{ ConnectedLoadOperation, LinkedStylesheetImportGraphs, QueuedConnectedStyleLoad, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/stylesheet_task.rs b/moli-renderer-v8/src/runtime/page_vm/tests/stylesheet_task.rs index 364ab56fea..2dd46c95cc 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/stylesheet_task.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/stylesheet_task.rs @@ -1079,6 +1079,131 @@ document.head.append(retired); .expect("stale stylesheet Networking completion test should run"); } +#[tokio::test(flavor = "current_thread")] +async fn stale_linked_import_completion_does_not_repopulate_replacement_graph() { + run_page_vm_async_test(async move { + let (base_url, server) = spawn_path_response_http_server(vec![ + ( + "/stale-root.css", + "HTTP/1.1 200 OK", + "@import './stale-child.css';".to_owned(), + Duration::ZERO, + ), + ( + "/stale-child.css", + "HTTP/1.1 200 OK", + ".retired-import { color: red; }".to_owned(), + Duration::from_millis(100), + ), + ]) + .await; + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let document_url = Url::parse(&format!("{base_url}/page.html"))?; + let (mut page_vm, _resource_source, mut wake_rx) = + page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url); + page_vm.vm_mut().eval(&format!( + r#" +globalThis.__staleLinkedImportEvents = []; +const retired = document.createElement("link"); +retired.rel = "stylesheet"; +retired.href = "{base_url}/stale-root.css"; +retired.addEventListener("load", () => __staleLinkedImportEvents.push("load")); +retired.addEventListener("error", () => __staleLinkedImportEvents.push("error")); +document.head.append(retired); +"queued" +"#, + ))?; + page_vm + .vm_mut() + .prime_document_lifecycle_processing_and_record_stylesheet_network_results(); + wait_for_stylesheet_source(&mut wake_rx, RendererOwnerWakeSource::NetworkingTask).await; + assert!( + page_vm + .run_exact_selected_page_task_for_test( + PageSelectedTaskTestSelector::StylesheetCompletion, + &loader, + ) + .await?, + "the root stylesheet terminal should start its linked import graph" + ); + assert_eq!( + page_vm + .vm() + .document_runtime + .linked_stylesheet_import_graph_count_for_test(), + 1, + "Document A should retain its in-flight linked import graph" + ); + let _ = page_vm.vm_mut().take_network_output(); + + wait_for_stylesheet_source(&mut wake_rx, RendererOwnerWakeSource::NetworkingTask).await; + let stale_task = page_vm + .take_stylesheet_networking_body_task_for_test() + .expect("the delayed linked import terminal should remain selectable"); + + page_vm.vm_mut().eval( + "document.open(); document.write('

replacement

'); document.close();", + )?; + assert_eq!( + page_vm + .vm() + .document_runtime + .linked_stylesheet_import_graph_count_for_test(), + 0, + "document.open() should install an empty stylesheet graph state for Document B" + ); + assert!(!page_vm.vm().document_runtime.has_pending_style_loads()); + + let outcome = page_vm.apply_selected_page_stylesheet_networking_turn(stale_task)?; + assert_eq!( + outcome.action.target_effect, + PageStylesheetNetworkingTargetEffect::RecordedForStaleOwner + ); + assert_eq!( + page_vm + .vm() + .document_runtime + .linked_stylesheet_import_graph_count_for_test(), + 0, + "Document A's terminal must not create an entry in Document B's graph map" + ); + assert!(!page_vm.vm().document_runtime.has_pending_style_loads()); + assert!( + !page_vm.has_ready_dom_manipulation_task_for_test(), + "the stale graph must not publish a link event into Document B" + ); + assert_eq!( + page_vm.vm_mut().eval("__staleLinkedImportEvents.join(',')")?, + "" + ); + + let (records, _, _) = split_network_output_items(page_vm.vm_mut().take_network_output()); + assert_eq!(records.len(), 1, "the child fetch remains observable"); + let stale_child_url = Url::parse(&format!("{base_url}/stale-child.css"))?; + assert_eq!( + records[0].url(), + &stale_child_url, + "historical network accounting should retain the stale import request" + ); + assert_eq!( + page_vm.pending_subresource_request_count(), + 0, + "a stale import terminal must not derive work in Document B" + ); + page_vm + .finish_selected_page_networking_task( + PageNetworkingTurnAction::StylesheetCompletion(outcome.action), + &loader, + ) + .await?; + server.await.expect("stale linked import fixture server"); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("stale linked import completion test should run"); +} + #[tokio::test(flavor = "current_thread")] async fn connected_style_body_settles_its_lease_but_leaves_reactions_for_task_completion() { run_page_vm_async_test(async move { diff --git a/moli-renderer-v8/src/script_vm/stylesheet_page_tasks.rs b/moli-renderer-v8/src/script_vm/stylesheet_page_tasks.rs index 4278ae621b..0c4fdd86dd 100644 --- a/moli-renderer-v8/src/script_vm/stylesheet_page_tasks.rs +++ b/moli-renderer-v8/src/script_vm/stylesheet_page_tasks.rs @@ -1,5 +1,6 @@ use super::ScriptVm; use crate::{ + document_runtime::StylesheetImportCompletionAuthority, page_task_queue::{ PageConnectedStyleEventTargetEffect, PageConnectedStyleLoadDelayEffect, PageStylesheetNetworkingTargetEffect, RendererPageConnectedStyleEventTask, @@ -32,6 +33,11 @@ impl ScriptVm { ) -> PageStylesheetNetworkingTargetEffect { let owner = task.owner(); let current = self.stylesheet_task_owner_is_current(root_document, owner); + let import_authority = if current { + StylesheetImportCompletionAuthority::CurrentDocument + } else { + StylesheetImportCompletionAuthority::HistoricalOnly + }; match task.into_completion() { RendererPageStylesheetCompletion::Blocking(completion) => self .document_runtime @@ -41,10 +47,10 @@ impl ScriptVm { .apply_connected_style_load_completion(completion), RendererPageStylesheetCompletion::LinkedImport(completion) => self .document_runtime - .apply_linked_stylesheet_import_graph_completion(completion), + .apply_linked_stylesheet_import_graph_completion(completion, import_authority), RendererPageStylesheetCompletion::LiveImport(completion) => self .document_runtime - .apply_live_stylesheet_import_load_completion(completion, current), + .apply_live_stylesheet_import_load_completion(completion, import_authority), } self.record_ready_stylesheet_network_results(); if current { diff --git a/moli-renderer-v8/src/stylesheet_runtime/completion.rs b/moli-renderer-v8/src/stylesheet_runtime/completion.rs index f566e6cdda..0adc4d7791 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/completion.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/completion.rs @@ -21,13 +21,19 @@ pub(crate) struct LiveStylesheetImportLoadCompletion { pub(in crate::document_runtime) network_results: Vec, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum StylesheetImportCompletionAuthority { + CurrentDocument, + HistoricalOnly, +} + impl DocumentRuntime { pub(crate) fn apply_live_stylesheet_import_load_completion( &mut self, mut completion: LiveStylesheetImportLoadCompletion, - install_authority: bool, + authority: StylesheetImportCompletionAuthority, ) { - if !install_authority { + if authority == StylesheetImportCompletionAuthority::HistoricalOnly { for result in &mut completion.network_results { result.import_roots.clear(); result.source_owners.clear(); diff --git a/moli-renderer-v8/src/stylesheet_runtime/linked_import_graph.rs b/moli-renderer-v8/src/stylesheet_runtime/linked_import_graph.rs index 98d22c439b..0dd1a635c6 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/linked_import_graph.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/linked_import_graph.rs @@ -451,12 +451,23 @@ impl DocumentRuntime { pub(crate) fn apply_linked_stylesheet_import_graph_completion( &mut self, completion: LinkedStylesheetImportGraphCompletion, + authority: StylesheetImportCompletionAuthority, ) { let LinkedStylesheetImportGraphCompletion { fetch, graph, mut network_results, } = completion; + if authority == StylesheetImportCompletionAuthority::HistoricalOnly { + for result in &mut network_results { + result.import_roots.clear(); + result.source_owners.clear(); + } + self.stylesheet_lifecycle + .ready_connected_load_network_results + .extend(network_results); + return; + } let roots = self .linked_stylesheet_import_roots(&fetch) .into_iter() @@ -479,6 +490,14 @@ impl DocumentRuntime { .extend(network_results); self.note_stylesheet_import_graph_completion(&fetch, graph); } + + #[cfg(test)] + pub(crate) fn linked_stylesheet_import_graph_count_for_test(&self) -> usize { + self.stylesheet_lifecycle + .linked_stylesheet_import_graphs + .entries + .len() + } } #[cfg(test)] diff --git a/moli-renderer-v8/src/stylesheet_runtime/mod.rs b/moli-renderer-v8/src/stylesheet_runtime/mod.rs index aae26ceabe..f6022930cf 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/mod.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/mod.rs @@ -29,7 +29,10 @@ mod test_driver; pub(crate) use attributes::attribute_reprocesses_connected_stylesheet; pub(crate) use blocking::OwnerlessStylesheetAdmissionError; pub(super) use client_index::StylesheetLinkClientIndex; -pub(crate) use completion::{ConnectedLoadCompletion, LiveStylesheetImportLoadCompletion}; +pub(crate) use completion::{ + ConnectedLoadCompletion, LiveStylesheetImportLoadCompletion, + StylesheetImportCompletionAuthority, +}; pub(crate) use connected::{ConnectedStyleLoadPrimeResult, PreparedConnectedStyleLoad}; pub(crate) use import_graph::fetch_complete_stylesheet_import_graph; pub(super) use import_graph::{ConnectedStyleImportReadiness, connected_style_import_readiness}; diff --git a/moli-renderer-v8/src/stylesheet_runtime/test_driver.rs b/moli-renderer-v8/src/stylesheet_runtime/test_driver.rs index bc820dc173..55d97b58d3 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/test_driver.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/test_driver.rs @@ -18,10 +18,9 @@ impl DocumentRuntime { let Some(task) = task else { return false; }; - // Consume stale tasks too. Production records their network result but - // the exact owner checks prevent them from installing stylesheet state - // into a replacement Document; leaving the old task at the test source - // head would model neither behavior. + // This standalone fixture has no Page-level exact-owner arbiter, so + // completions are applied with current-owner authority. PageVm tests + // cover stale task handling through the production owner boundary. match task.into_completion() { crate::page_task_queue::RendererPageStylesheetCompletion::Blocking(completion) => { self.apply_blocking_stylesheet_completion(completion); @@ -30,10 +29,16 @@ impl DocumentRuntime { self.apply_connected_style_load_completion(completion); } crate::page_task_queue::RendererPageStylesheetCompletion::LinkedImport(completion) => { - self.apply_linked_stylesheet_import_graph_completion(completion); + self.apply_linked_stylesheet_import_graph_completion( + completion, + StylesheetImportCompletionAuthority::CurrentDocument, + ); } crate::page_task_queue::RendererPageStylesheetCompletion::LiveImport(completion) => { - self.apply_live_stylesheet_import_load_completion(completion, true); + self.apply_live_stylesheet_import_load_completion( + completion, + StylesheetImportCompletionAuthority::CurrentDocument, + ); } } true