From 75fb70710c6a63fc696dcbf4c4670d150a9317a9 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 19 Sep 2026 05:03:26 +0800 Subject: [PATCH] fix(runtime): admit document lifecycle during awaited evaluation --- moli-core/tests/scripts/document_close.rs | 36 +++++++++++++++++++ .../src/runtime/owner_local_store/bound.rs | 29 +++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/moli-core/tests/scripts/document_close.rs b/moli-core/tests/scripts/document_close.rs index 1fa5cdac8a..41703ef023 100644 --- a/moli-core/tests/scripts/document_close.rs +++ b/moli-core/tests/scripts/document_close.rs @@ -122,3 +122,39 @@ async fn child_document_close_in_written_script_drains_tail_and_finishes_load() async fn child_document_close_in_external_writer_drains_tail_and_finishes_load() -> Result<()> { written_child_script_close_finishes_load(false, false, true).await } + +#[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 page = browser + .fetch(&markup_url(&server, "initial")) + .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 da26930ac8..ac2835c395 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs @@ -623,7 +623,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, @@ -632,7 +635,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