From 3bfad58bae9b6bbcb6d734e1ecbe2d748cf4eb0a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 27 Jul 2026 06:52:03 +0800 Subject: [PATCH] fix(stylesheets): preserve parser event ordering --- ...esheet_href_mutation_uses_fresh_fetch.html | 13 +-- moli-core/tests/scripts.rs | 10 +++ moli-test-support/src/routes_core.rs | 88 ++++++++++++++++++- moli-test-support/src/server.rs | 6 ++ moli-test-support/src/server_routes.rs | 12 +++ 5 files changed, 122 insertions(+), 7 deletions(-) diff --git a/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html b/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html index 5d33988ac0..e79aaa7ae9 100644 --- a/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html +++ b/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html @@ -3,17 +3,18 @@ + diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs index 61d18e76aa..40e15f4ddb 100644 --- a/moli-core/tests/scripts.rs +++ b/moli-core/tests/scripts.rs @@ -830,6 +830,10 @@ async fn runtime_inserted_stylesheet_load_can_trigger_location_replace() -> Resu #[tokio::test(flavor = "multi_thread")] async fn runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch_for_parser_blocking_progress() -> Result<()> { + // The stale stylesheet response is gated until the following parser script + // requests its probe. The probe reports whether it arrived before the + // stale-response fallback fired, so this checks parser progress without a + // wall-clock threshold in page script. let server = FixtureServer::spawn().await?; let browser = Browser::new(AppConfig::default())?; @@ -845,6 +849,12 @@ async fn runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch_for_parser_b diagnostic_global(&page, "runtimeInsertedHrefMutationSawLate"), Some(&JsValueSnapshot::Bool(false)) ); + assert!( + page.subresource_network_records().iter().any(|record| { + record.url().path() == "/assets/runtime_inserted_stylesheet_href_mutation_fresh.css" + }), + "the retargeted stylesheet must start the fresh fetch" + ); server.shutdown().await; Ok(()) diff --git a/moli-test-support/src/routes_core.rs b/moli-test-support/src/routes_core.rs index 560ab5fa34..b936a6d13e 100644 --- a/moli-test-support/src/routes_core.rs +++ b/moli-test-support/src/routes_core.rs @@ -1,7 +1,7 @@ use super::*; use parking_lot::Mutex; use std::collections::HashMap; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock}; static NET_UPSTREAM_XHR_404_THEN_200_REQUESTS: AtomicUsize = AtomicUsize::new(0); @@ -21,6 +21,25 @@ static RUNTIME_OWNED_IN_ORDER_ERROR_AFTER_DCL_GATES: OnceLock< static RUNTIME_OWNED_IN_ORDER_LOAD_AFTER_DCL_GATES: OnceLock< Mutex>>, > = OnceLock::new(); +static RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES: OnceLock< + Mutex>>, +> = OnceLock::new(); + +struct RuntimeInsertedStylesheetHrefMutationGate { + release_stale_response: tokio::sync::Notify, + stale_request_started: AtomicBool, + stale_response_timed_out: AtomicBool, +} + +impl RuntimeInsertedStylesheetHrefMutationGate { + fn new() -> Self { + Self { + release_stale_response: tokio::sync::Notify::new(), + stale_request_started: AtomicBool::new(false), + stale_response_timed_out: AtomicBool::new(false), + } + } +} struct ConcurrentSharedStateRequestGuard; @@ -169,6 +188,34 @@ pub(crate) fn notify_runtime_owned_in_order_load_after_dcl_gate(host_key: &str) runtime_owned_in_order_load_after_dcl_gate(host_key).notify_one(); } +fn runtime_inserted_stylesheet_href_mutation_gate( + host_key: &str, +) -> Arc { + let gates = + RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get_or_init(|| Mutex::new(HashMap::new())); + let mut gates = gates.lock(); + gates + .entry(host_key.to_owned()) + .or_insert_with(|| Arc::new(RuntimeInsertedStylesheetHrefMutationGate::new())) + .clone() +} + +fn remove_runtime_inserted_stylesheet_href_mutation_gate(host_key: &str) { + let Some(gates) = RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get() else { + return; + }; + gates.lock().remove(host_key); +} + +pub(crate) fn clear_runtime_inserted_stylesheet_href_mutation_gate(host_key: &str) { + let Some(gates) = RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get() else { + return; + }; + if let Some(gate) = gates.lock().remove(host_key) { + gate.release_stale_response.notify_one(); + } +} + pub(super) async fn static_page() -> Html<&'static str> { Html(STATIC_HTML) } @@ -5526,6 +5573,45 @@ pub(super) async fn asset_dynamic_blocking_stylesheet_gated_css( css_response(BLOCKING_STYLESHEET_SLOW_CSS) } +pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_stale_css( + headers: HeaderMap, +) -> Response { + let host_key = request_host_key(&headers).unwrap_or_default(); + let gate = runtime_inserted_stylesheet_href_mutation_gate(&host_key); + gate.stale_request_started.store(true, Ordering::SeqCst); + let released_by_probe = tokio::time::timeout( + Duration::from_secs(2), + gate.release_stale_response.notified(), + ) + .await + .is_ok(); + if released_by_probe { + remove_runtime_inserted_stylesheet_href_mutation_gate(&host_key); + } else { + gate.stale_response_timed_out.store(true, Ordering::SeqCst); + } + css_response(BLOCKING_STYLESHEET_SLOW_CSS) +} + +pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_fresh_css() -> Response { + css_response(CHROME_STYLESHEETLIST_1_CSS) +} + +pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_probe_script( + headers: HeaderMap, +) -> Response { + let host_key = request_host_key(&headers).unwrap_or_default(); + let gate = runtime_inserted_stylesheet_href_mutation_gate(&host_key); + let parser_not_blocked = !gate.stale_response_timed_out.load(Ordering::SeqCst); + gate.release_stale_response.notify_one(); + if gate.stale_request_started.load(Ordering::SeqCst) { + remove_runtime_inserted_stylesheet_href_mutation_gate(&host_key); + } + javascript_string_response(format!( + "window.runtimeInsertedHrefMutationParserNotBlocked = {parser_not_blocked};" + )) +} + pub(super) async fn asset_runtime_connected_preload_very_slow_css() -> Response { sleep(Duration::from_millis(250)).await; css_response(BLOCKING_STYLESHEET_SLOW_CSS) diff --git a/moli-test-support/src/server.rs b/moli-test-support/src/server.rs index 2e19fc5efb..85d2cdfcc5 100644 --- a/moli-test-support/src/server.rs +++ b/moli-test-support/src/server.rs @@ -52,6 +52,9 @@ impl FixtureServer { } pub async fn shutdown(mut self) { + crate::routes_core::clear_runtime_inserted_stylesheet_href_mutation_gate( + &self.addr.to_string(), + ); if let Some(shutdown_tx) = self.shutdown_tx.take() { let _ = shutdown_tx.send(()); } @@ -64,6 +67,9 @@ impl FixtureServer { impl Drop for FixtureServer { fn drop(&mut self) { + crate::routes_core::clear_runtime_inserted_stylesheet_href_mutation_gate( + &self.addr.to_string(), + ); if let Some(shutdown_tx) = self.shutdown_tx.take() { let _ = shutdown_tx.send(()); } diff --git a/moli-test-support/src/server_routes.rs b/moli-test-support/src/server_routes.rs index c715e09733..10d90d5747 100644 --- a/moli-test-support/src/server_routes.rs +++ b/moli-test-support/src/server_routes.rs @@ -2393,6 +2393,18 @@ pub(super) fn build_router() -> Router { "/assets/blocking_stylesheet_slow.css", get(asset_blocking_stylesheet_slow_css), ) + .route( + "/assets/runtime_inserted_stylesheet_href_mutation_stale.css", + get(asset_runtime_inserted_stylesheet_href_mutation_stale_css), + ) + .route( + "/assets/runtime_inserted_stylesheet_href_mutation_fresh.css", + get(asset_runtime_inserted_stylesheet_href_mutation_fresh_css), + ) + .route( + "/assets/runtime_inserted_stylesheet_href_mutation_probe.js", + get(asset_runtime_inserted_stylesheet_href_mutation_probe_script), + ) .route( "/assets/runtime_connected_modulepreload_slow.mjs", get(asset_runtime_connected_modulepreload_slow_module),