From 2cbc7db2ef2de525ecab3ed8407ad40a63bf716f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 22 Sep 2026 16:50:28 +0800 Subject: [PATCH] fix(runtime): admit document lifecycle during awaited evaluation --- moli-core/tests/scripts.rs | 37 +++++++++++++++++++ .../src/runtime/owner_local_store/bound.rs | 29 ++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs index ae0f638343..48dd9be026 100644 --- a/moli-core/tests/scripts.rs +++ b/moli-core/tests/scripts.rs @@ -6711,3 +6711,40 @@ async fn parse_time_lifecycle_queue_can_stop_cleanly_at_load_stage_after_load_mi #[path = "scripts/document_write_errors.rs"] mod document_write_errors; + +#[tokio::test(flavor = "multi_thread")] +async fn main_document_close_can_resolve_an_awaited_lifecycle_promise() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document"))?; + url.query_pairs_mut() + .append_pair("markup", "initial"); + let mut page = browser.fetch(url.as_str()).await?; + let result = tokio::time::timeout( + Duration::from_secs(10), + page.evaluate_runtime_expression_with_await_async( + r#"new Promise(resolve => { + document.open(); + document.addEventListener('DOMContentLoaded', () => resolve(JSON.stringify({ + readyState: document.readyState, + text: document.getElementById('replacement').textContent + })), {once: true}); + document.write('

replacement

'); + document.close(); + })"#, + true, + ), + ) + .await??; + let observed: serde_json::Value = serde_json::from_str( + result["value"] + .as_str() + .expect("replacement lifecycle observation"), + )?; + assert_eq!( + observed, + serde_json::json!({"readyState": "interactive", "text": "replacement"}) + ); + server.shutdown().await; + Ok(()) +} diff --git a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs index 8db6c47677..0ade1057b1 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs @@ -592,7 +592,10 @@ pub(in crate::runtime) async fn advance_runtime_expression_await_turn_on_entry_v ) -> (LivePageEntry, Result) { run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| { Box::pin(async move { - entry + let replacement_lifecycle_snapshot = entry + .page_vm() + .document_replacement_lifecycle_action_snapshot(); + let evaluation = entry .page_vm_mut() .advance_runtime_expression_await_turn( execution_context_id, @@ -601,7 +604,29 @@ pub(in crate::runtime) async fn advance_runtime_expression_await_turn_on_entry_v remaining, result_mode, ) - .await + .await; + // An awaited expression can synchronously open and close a new + // Document, then return a promise resolved by its DCL/load tasks. + // Admit those tasks at this action boundary, before awaiting the + // result, including when evaluation reports an error after mutation. + let reconciliation = { + let (page_vm, pending_document_lifecycle_turn) = + entry.page_vm_and_document_lifecycle_turn_mut(); + page_vm + .reconcile_document_replacement_lifecycle_after_owner_action( + replacement_lifecycle_snapshot, + pending_document_lifecycle_turn, + ) + .await + }; + match (evaluation, reconciliation) { + (Ok(outcome), Ok(_)) => Ok(outcome), + (Err(evaluation_error), Ok(_)) => Err(evaluation_error), + (Ok(_), Err(reconciliation_error)) => Err(reconciliation_error), + (Err(evaluation_error), Err(reconciliation_error)) => Err(anyhow!( + "runtime evaluation failed ({evaluation_error:#}) and its Document replacement lifecycle reconciliation also failed ({reconciliation_error:#})" + )), + } }) }) .await