fix(renderer): isolate stale import completions

This commit is contained in:
ldm0
2026-08-23 04:45:50 +08:00
committed by Donough Liu
parent 09b70a9b40
commit 7cf5dfbb34
7 changed files with 177 additions and 13 deletions
+2 -2
View File
@@ -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,
@@ -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('<!doctype html><p>replacement</p>'); 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 {
@@ -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 {
@@ -21,13 +21,19 @@ pub(crate) struct LiveStylesheetImportLoadCompletion {
pub(in crate::document_runtime) network_results: Vec<ConnectedLoadNetworkResult>,
}
#[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();
@@ -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)]
@@ -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};
@@ -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