diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs
index 722b191e5d..ab64a402e1 100644
--- a/moli-core/tests/scripts.rs
+++ b/moli-core/tests/scripts.rs
@@ -37,6 +37,9 @@ mod parser_cleanup;
#[path = "scripts/parser_preparation.rs"]
mod parser_preparation;
+#[path = "scripts/lifecycle_task_order.rs"]
+mod lifecycle_task_order;
+
fn diagnostic_global<'a>(
page: &'a moli_core::page::Page,
name: &str,
diff --git a/moli-core/tests/scripts/lifecycle_task_order.rs b/moli-core/tests/scripts/lifecycle_task_order.rs
new file mode 100644
index 0000000000..59e5c2d352
--- /dev/null
+++ b/moli-core/tests/scripts/lifecycle_task_order.rs
@@ -0,0 +1,230 @@
+use super::*;
+
+async fn parser_rejection_notification_precedes_lifecycle_events(child: bool) -> Result<()> {
+ let markup = r#"
parser tail
"#;
+ let result = observe_lifecycle_markup(child, markup).await?;
+ assert_eq!(
+ result["value"].as_str(),
+ Some(r#"["interactive","rejection","DOMContentLoaded","complete","load"]"#),
+ "child={child}"
+ );
+ Ok(())
+}
+
+async fn observe_lifecycle_markup(child: bool, markup: &str) -> Result {
+ let server = FixtureServer::spawn().await?;
+ let browser = Browser::new(AppConfig::default())?;
+ let mut target = url::Url::parse(&server.url("/compat/child-dynamic-markup-document"))?;
+ target.query_pairs_mut().append_pair("markup", markup);
+ if child {
+ let parent = format!(
+ "",
+ target.as_str().replace('&', "&")
+ );
+ target.set_query(None);
+ target.query_pairs_mut().append_pair("markup", &parent);
+ }
+ let mut page = browser.fetch(target.as_str()).await?;
+ let result = page
+ .evaluate_runtime_expression_with_await_async(
+ "JSON.stringify((document.getElementById('target')?.contentWindow || window).events)",
+ true,
+ )
+ .await?;
+ server.shutdown().await;
+ Ok(result)
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> {
+ parser_rejection_notification_precedes_lifecycle_events(false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> {
+ parser_rejection_notification_precedes_lifecycle_events(true).await
+}
+
+async fn deferred_rejection_keeps_dom_fifo_position(child: bool, module: bool) -> Result<()> {
+ let attribute = if module { "type=module" } else { "defer" };
+ let markup = format!(
+ r#"
+ parser tail
"#
+ );
+ let result = observe_lifecycle_markup(child, &markup).await?;
+ let events: Vec = serde_json::from_str(result["value"].as_str().unwrap())?;
+ let at = |name: &str| events.iter().position(|event| event == name).unwrap();
+ assert!(at("interactive") < at("rejection"), "{events:?}");
+ assert!(at("rejection") < at("DOMContentLoaded"), "{events:?}");
+ assert!(at("DOMContentLoaded") < at("later DOM task"), "{events:?}");
+ assert!(at("rejection") < at("complete"), "{events:?}");
+ assert!(at("complete") < at("load"), "{events:?}");
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
+ deferred_rejection_keeps_dom_fifo_position(false, false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
+ deferred_rejection_keeps_dom_fifo_position(true, false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
+ deferred_rejection_keeps_dom_fifo_position(false, true).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
+ deferred_rejection_keeps_dom_fifo_position(true, true).await
+}
+
+async fn earlier_dom_task_replaces_document_before_queued_dcl(child: bool) -> Result<()> {
+ let markup = r#"old parser tail
"#;
+ let result = observe_lifecycle_markup(child, markup).await?;
+ assert_eq!(result["value"].as_str(), Some(r#"["new DCL"]"#));
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> {
+ earlier_dom_task_replaces_document_before_queued_dcl(false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> {
+ earlier_dom_task_replaces_document_before_queued_dcl(true).await
+}
+
+async fn dom_callback_before_load_does_not_lose_load(child: bool) -> Result<()> {
+ let markup = r#"parser tail
"#;
+ let result = observe_lifecycle_markup(child, markup).await?;
+ let events: Vec = serde_json::from_str(result["value"].as_str().unwrap())?;
+ for expected in ["DOMContentLoaded", "rejection", "async", "load"] {
+ assert_eq!(
+ events
+ .iter()
+ .filter(|event| event.as_str() == expected)
+ .count(),
+ 1,
+ "child={child}, {events:?}"
+ );
+ }
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> {
+ dom_callback_before_load_does_not_lose_load(false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> {
+ dom_callback_before_load_does_not_lose_load(true).await
+}
+
+async fn parser_import_map_error_keeps_dom_fifo_position(
+ child: bool,
+ document_write: bool,
+) -> Result<()> {
+ let import_map = r#""#;
+ let parser_markup = if document_write {
+ format!(
+ "",
+ serde_json::to_string(import_map)?.replace("", "<\\/script>")
+ )
+ } else {
+ import_map.to_owned()
+ };
+ let markup = format!(
+ r#"{parser_markup}parser tail"#
+ );
+ let result = observe_lifecycle_markup(child, &markup).await?;
+ assert_eq!(
+ result["value"].as_str(),
+ Some(r#"["parser error","dynamic error","DOMContentLoaded"]"#),
+ "child={child}, document_write={document_write}"
+ );
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> {
+ parser_import_map_error_keeps_dom_fifo_position(false, false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> {
+ parser_import_map_error_keeps_dom_fifo_position(true, false).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn main_written_import_map_error_precedes_later_dom_tasks() -> Result<()> {
+ parser_import_map_error_keeps_dom_fifo_position(false, true).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn child_written_import_map_error_precedes_later_dom_tasks() -> Result<()> {
+ parser_import_map_error_keeps_dom_fifo_position(true, true).await
+}
diff --git a/moli-renderer-v8/src/document_runtime/document_write.rs b/moli-renderer-v8/src/document_runtime/document_write.rs
index 5c88788c7f..8429acdd54 100644
--- a/moli-renderer-v8/src/document_runtime/document_write.rs
+++ b/moli-renderer-v8/src/document_runtime/document_write.rs
@@ -2129,10 +2129,7 @@ impl DocumentRuntime {
}
}
PreparedImportMapSource::ExternalUnsupported => {
- let _ = self.enqueue_script_event_lifecycle_work(
- ScriptEventKind::Error,
- &host_script_handle,
- );
+ unsafe { &mut *host_ptr }.queue_script_preparation_error(scope, node);
}
}
}
diff --git a/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs b/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs
index 3de30e81ed..98e1ec7d76 100644
--- a/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs
+++ b/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs
@@ -28,8 +28,9 @@ impl DocumentRuntime {
.unwrap_or(work.len());
// This is an owner-queue boundary marker, not yet the HTML event-loop
// queue point for DOMContentLoaded. When it reaches the front after all
- // defer-like and parser-owned pre-DCL work has drained, the driver first
- // runs ready timer tasks recorded during classic defer execution.
+ // defer-like and parser-owned pre-DCL work has drained, the driver
+ // admits the event to the shared DOM task source before yielding to
+ // ready timer tasks recorded during classic defer execution.
work.insert(
domcontentloaded_index,
PostParsePageOwnedWork::main_document_domcontentloaded(owner),
diff --git a/moli-renderer-v8/src/module_runtime.rs b/moli-renderer-v8/src/module_runtime.rs
index 54e6452bc0..d064bb510e 100644
--- a/moli-renderer-v8/src/module_runtime.rs
+++ b/moli-renderer-v8/src/module_runtime.rs
@@ -151,11 +151,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
start_line: u64,
start_column: u64,
import_map: PreparedImportMap,
-) {
+) -> anyhow::Result<()> {
vm.document_runtime
.note_parser_script_start_position(node_id, start_line, start_column);
- let host_script_handle = vm
- .document_runtime
+ vm.document_runtime
.bind_parser_owned_script_handle_for_node(import_map.node_id);
let _ = vm
.document_runtime
@@ -169,7 +168,7 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
let Some(source) =
vm.inline_script_element_source_for_execution(node_id, &source, request)
else {
- return;
+ return Ok(());
};
if let Err(error) =
register_parser_owned_import_map_source(vm, &source, &import_map.base_url)
@@ -181,12 +180,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
}
}
PreparedImportMapSource::ExternalUnsupported => {
- let _ = vm.document_runtime.enqueue_script_event_lifecycle_work(
- crate::host::ScriptEventKind::Error,
- &host_script_handle,
- );
+ vm.queue_script_preparation_error(node_id)?;
}
}
+ Ok(())
}
pub(crate) fn resolve_module_specifier(
diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs
index 0d6ba5f7b4..135c5f4317 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs
@@ -63,8 +63,21 @@ impl JsContextHost {
action: FrameDocumentLifecycleAction,
realm_id: FrameRealmId,
) -> bool {
+ if !matches!(action, FrameDocumentLifecycleAction::Interactive(_)) {
+ return self
+ .page_task_capabilities
+ .get()
+ .expect("a child Document must retain its Page task capabilities")
+ .dom_manipulation()
+ .send_child_document_lifecycle(
+ crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new(
+ action, realm_id,
+ ),
+ )
+ .is_ok();
+ }
self.page_child_frame_task_sender()
- .send_document_lifecycle(
+ .send_interactive_lifecycle(
crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new(
action, realm_id,
),
@@ -196,6 +209,35 @@ impl JsContextHost {
self.route_child_document_lifecycle_action(action.into(), realm_request.realm_id())
}
+ /// Parser stop changes readiness synchronously. Only an unmaterialized
+ /// child realm needs an owner continuation before it can dispatch events.
+ pub(crate) fn finish_child_document_parser_stop(
+ &mut self,
+ scope: &mut v8::PinScope<'_, '_>,
+ action: FrameDocumentInteractiveLifecycleAction,
+ ) -> FrameDocumentLifecycleTaskEffect {
+ if !self.child_document_lifecycle_action_is_current(action.into()) {
+ return FrameDocumentLifecycleTaskEffect::NotApplied;
+ }
+ let dispatch_scope = super::super::OwnerDispatchScope::Child(action.child_handle());
+ let execution_owner = crate::native_bridge::WindowExecutionContextOwner::Frame(
+ action.owner().local_window_id,
+ );
+ let Some((_, context)) =
+ self.window_execution_context(scope, execution_owner, dispatch_scope)
+ else {
+ return if self.queue_child_document_interactive_lifecycle_action(action) {
+ FrameDocumentLifecycleTaskEffect::ConsumedWithoutEvent
+ } else {
+ FrameDocumentLifecycleTaskEffect::NotApplied
+ };
+ };
+ let context = v8::Global::new(scope, context);
+ let context = v8::Local::new(scope, &context);
+ let child_scope = &mut v8::ContextScope::new(scope, context);
+ self.run_child_document_lifecycle_action(child_scope, action.into())
+ }
+
pub(crate) fn queue_child_document_domcontentloaded_if_ready(
&mut self,
child_handle: DomHandle,
diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
index 8663b77289..0ed39dfe53 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
@@ -937,12 +937,11 @@ impl JsContextHost {
}
}
PreparedImportMapSource::ExternalUnsupported => {
- tracing::debug!(
- child_handle = ?child_handle,
- document_handle = ?document_handle,
- script_handle = ?node_id,
- "child parser external import map is unsupported"
- );
+ if !self.queue_script_preparation_error(scope, node_id) {
+ return ScriptDisposition::AdmissionFailed {
+ script_handle: node_id,
+ };
+ }
}
}
ScriptDisposition::Continue
@@ -1143,7 +1142,7 @@ impl JsContextHost {
document_handle,
parser,
) {
- self.queue_child_document_interactive_lifecycle_action(action);
+ self.finish_child_document_parser_stop(scope, action);
}
}
diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs
index e353720443..9176a7efee 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs
@@ -749,9 +749,10 @@ impl JsContextHost {
};
};
let initial_classic_ready_work = install.initial_classic_ready_work;
- let parser_stop_queued = install
- .parser_stop_action
- .is_some_and(|action| self.queue_child_document_interactive_lifecycle_action(action));
+ let parser_stop_queued = install.parser_stop_action.is_some_and(|action| {
+ self.finish_child_document_parser_stop(scope, action)
+ != crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::NotApplied
+ });
self.promote_pending_service_worker_child_client(handle);
self.register_or_update_service_worker_child_client(handle);
self.complete_pending_service_worker_child_client_navigation(handle);
diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs
index 382519c948..cba1103b88 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/core.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs
@@ -781,6 +781,16 @@ impl JsContextHost {
.promise_rejection()
}
+ pub(crate) fn page_main_document_lifecycle_sender(
+ &self,
+ ) -> crate::page_task_queue::RendererPageMainDocumentLifecycleSender {
+ self.page_task_capabilities
+ .get()
+ .expect("a live Page Window must install its lifecycle task capability")
+ .dom_manipulation()
+ .main_document_lifecycle()
+ }
+
pub(crate) fn page_file_reading_sender(
&self,
) -> crate::page_task_queue::RendererPageFileReadingSender {
diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs
index 0721dc2676..dfc3f8a57e 100644
--- a/moli-renderer-v8/src/page_task_queue.rs
+++ b/moli-renderer-v8/src/page_task_queue.rs
@@ -17,6 +17,7 @@ mod history_traversal;
mod image_load_event;
mod indexed_db_task;
mod internal_loading;
+mod main_document_lifecycle;
mod main_document_post_parse;
mod main_document_runtime;
mod main_document_task_owner;
@@ -218,12 +219,10 @@ pub(crate) use self::dedicated_worker_client_event::{
RendererPageDedicatedWorkerClientEventProducer, RendererPageDedicatedWorkerClientEventSender,
RendererPageDedicatedWorkerClientEventTask,
};
-#[cfg(test)]
-pub(crate) use self::dom_manipulation::RendererPageDomManipulationOwner;
pub(crate) use self::dom_manipulation::{
PageDomManipulationTurnAction, PageDomManipulationTurnOutcome,
- RendererPageDomManipulationRoute, RendererPageDomManipulationSender,
- RendererPageDomManipulationTask,
+ RendererPageDomManipulationOwner, RendererPageDomManipulationRoute,
+ RendererPageDomManipulationSender, RendererPageDomManipulationTask,
};
pub(crate) use self::dynamic_import_owner_action::{
PageDynamicImportOwnerActionDocumentEffect, PageDynamicImportOwnerActionTurnAction,
@@ -278,6 +277,11 @@ pub(crate) use self::internal_loading::{
RendererPageInternalLoadingOwner, RendererPageInternalLoadingSender,
RendererPageInternalLoadingTask,
};
+pub(crate) use self::main_document_lifecycle::{
+ PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome,
+ RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleOwner,
+ RendererPageMainDocumentLifecycleSender, RendererPageMainDocumentLifecycleTask,
+};
pub(crate) use self::main_document_post_parse::{
MainDocumentCompletionRecheckEffect, MainDocumentPostParseCallbackExecution,
MainDocumentPostParseCallbackSettlement, MainDocumentPostParseExecution,
diff --git a/moli-renderer-v8/src/page_task_queue/child_frame_task.rs b/moli-renderer-v8/src/page_task_queue/child_frame_task.rs
index fecb0960bf..95cbc6355f 100644
--- a/moli-renderer-v8/src/page_task_queue/child_frame_task.rs
+++ b/moli-renderer-v8/src/page_task_queue/child_frame_task.rs
@@ -390,7 +390,7 @@ pub(crate) type PageChildClassicScriptSourceLoadTurnOutcome =
PageOwnerTurnOutcome;
impl RendererPageChildFrameTask {
- fn new(owner: RendererPageChildFrameTaskOwner) -> Self {
+ pub(super) fn new(owner: RendererPageChildFrameTaskOwner) -> Self {
debug_assert!(!matches!(
owner.target(),
RendererPageChildFrameTaskTarget::ParserModuleRootStart(_)
@@ -566,10 +566,14 @@ impl RendererPageChildFrameTaskSender {
))
}
- pub(crate) fn send_document_lifecycle(
+ pub(crate) fn send_interactive_lifecycle(
&self,
target: RendererPageChildDocumentLifecycleTarget,
) -> Result<(), RendererPageChildFrameTaskRouteClosed> {
+ debug_assert!(matches!(
+ target.action(),
+ crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)
+ ));
self.send(RendererPageChildFrameTaskTarget::DocumentLifecycle(target))
}
diff --git a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs
index addb597836..b25df6df0d 100644
--- a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs
+++ b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs
@@ -78,6 +78,8 @@ pub(crate) enum RendererPageDomManipulationOwner {
ScriptPreparationError(RendererPageScriptPreparationErrorOwner),
PromiseRejection(RendererPagePromiseRejectionOwner),
ImageLoadEvent(RendererPageImageLoadEventOwner),
+ MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleOwner),
+ ChildDocumentLifecycle(super::RendererPageChildFrameTaskOwner),
PopupLoadEvent(RendererPagePopupLoadEventOwner),
PopupClose(RendererPagePopupCloseOwner),
ConnectedStyleEvent(RendererPageStylesheetTaskOwner),
@@ -97,6 +99,8 @@ pub(crate) enum RendererPageDomManipulationTask {
ScriptPreparationError(RendererPageScriptPreparationErrorTask),
PromiseRejection(RendererPagePromiseRejectionTask),
ImageLoadEvent(RendererPageImageLoadEventTask),
+ MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleTask),
+ ChildDocumentLifecycle(super::RendererPageChildFrameTask),
PopupLoadEvent(RendererPagePopupLoadEventTask),
PopupClose(RendererPagePopupCloseTask),
ConnectedStyleEvent(RendererPageConnectedStyleEventTask),
@@ -130,6 +134,12 @@ impl RendererPageDomManipulationTask {
Self::ImageLoadEvent(task) => {
RendererPageDomManipulationOwner::ImageLoadEvent(task.owner())
}
+ Self::MainDocumentLifecycle(task) => {
+ RendererPageDomManipulationOwner::MainDocumentLifecycle(task.owner)
+ }
+ Self::ChildDocumentLifecycle(task) => {
+ RendererPageDomManipulationOwner::ChildDocumentLifecycle(task.owner())
+ }
Self::PopupLoadEvent(task) => {
RendererPageDomManipulationOwner::PopupLoadEvent(task.owner())
}
@@ -169,6 +179,8 @@ pub(crate) enum PageDomManipulationTurnAction {
ScriptPreparationError(super::PageScriptPreparationErrorTurnAction),
PromiseRejection(super::PagePromiseRejectionTurnAction),
ImageLoadEvent(super::PageImageLoadEventTurnAction),
+ MainDocumentLifecycle(super::PageMainDocumentLifecycleTurnAction),
+ ChildDocumentLifecycle(super::PageChildDocumentLifecycleTurnAction),
PopupLoadEvent(super::PagePopupLoadEventTurnAction),
PopupClose(super::PagePopupCloseTurnAction),
ConnectedStyleEvent(PageConnectedStyleEventTurnAction),
@@ -234,6 +246,30 @@ impl RendererPageDomManipulationSender {
RendererPageImageLoadEventSender::new(self.route.clone(), self.root_document)
}
+ pub(crate) fn main_document_lifecycle(&self) -> super::RendererPageMainDocumentLifecycleSender {
+ super::RendererPageMainDocumentLifecycleSender::new(self.route.clone(), self.root_document)
+ }
+
+ pub(crate) fn send_child_document_lifecycle(
+ &self,
+ target: super::RendererPageChildDocumentLifecycleTarget,
+ ) -> Result<(), super::child_frame_task::RendererPageChildFrameTaskRouteClosed> {
+ debug_assert!(!matches!(
+ target.action(),
+ crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)
+ ));
+ self.route
+ .send(RendererPageDomManipulationTask::ChildDocumentLifecycle(
+ super::RendererPageChildFrameTask::new(
+ super::RendererPageChildFrameTaskOwner::new(
+ self.root_document,
+ super::RendererPageChildFrameTaskTarget::DocumentLifecycle(target),
+ ),
+ ),
+ ))
+ .map_err(|_| super::child_frame_task::RendererPageChildFrameTaskRouteClosed)
+ }
+
pub(crate) fn popup_load_event(&self) -> RendererPagePopupLoadEventSender {
RendererPagePopupLoadEventSender::new(self.route.clone(), self.root_document)
}
@@ -292,6 +328,21 @@ pub(crate) struct RendererPageDomManipulationSource {
}
impl RendererPageDomManipulationSource {
+ pub(crate) fn has_main_document_lifecycle_task(
+ &mut self,
+ root_document: RendererDocumentToken,
+ document: crate::frame_owner_model::FrameDocumentTaskOwner,
+ ) -> bool {
+ self.source.has_matching_task(|ready| {
+ matches!(
+ ready.value(),
+ RendererPageDomManipulationTask::MainDocumentLifecycle(task)
+ if task.owner.root_document == root_document
+ && task.owner.body.owner() == document
+ )
+ })
+ }
+
/// Remove cancelled task closures before they become scheduler-visible.
///
/// Element toggle coalescing cancels and reposts at the tail, matching the
diff --git a/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs b/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs
new file mode 100644
index 0000000000..dc0a3f2dec
--- /dev/null
+++ b/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs
@@ -0,0 +1,82 @@
+use tokio::sync::oneshot;
+
+use crate::{
+ frame_owner_model::FrameDocumentTaskOwner,
+ runtime::{PageOwnerTurnOutcome, RendererDocumentToken},
+ script_vm::{MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect},
+};
+
+use super::dom_manipulation::{RendererPageDomManipulationRoute, RendererPageDomManipulationTask};
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) struct RendererPageMainDocumentLifecycleOwner {
+ pub(crate) root_document: RendererDocumentToken,
+ pub(crate) body: MainDocumentLifecycleBody,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) enum RendererPageMainDocumentLifecycleCompletion {
+ Executed,
+ /// An earlier DOM task added a load delay after this task was admitted.
+ /// Return the exact load boundary to its driver without completing it.
+ LoadBlocked {
+ owner: FrameDocumentTaskOwner,
+ },
+}
+
+/// An admitted HTML lifecycle task, after parser/defer and load prerequisites.
+/// Its receipt releases the driver's completion token only after execution.
+#[derive(Debug)]
+pub(crate) struct RendererPageMainDocumentLifecycleTask {
+ pub(crate) owner: RendererPageMainDocumentLifecycleOwner,
+ pub(crate) completion: Option>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct RendererPageMainDocumentLifecycleSender {
+ route: RendererPageDomManipulationRoute,
+ root_document: RendererDocumentToken,
+}
+
+impl RendererPageMainDocumentLifecycleSender {
+ pub(super) fn new(
+ route: RendererPageDomManipulationRoute,
+ root_document: RendererDocumentToken,
+ ) -> Self {
+ Self {
+ route,
+ root_document,
+ }
+ }
+
+ pub(crate) fn send(
+ &self,
+ body: MainDocumentLifecycleBody,
+ completion: Option>,
+ ) -> anyhow::Result<()> {
+ anyhow::ensure!(
+ !matches!(body, MainDocumentLifecycleBody::Interactive(_)),
+ "interactive readiness belongs to parser completion"
+ );
+ self.route
+ .send(RendererPageDomManipulationTask::MainDocumentLifecycle(
+ RendererPageMainDocumentLifecycleTask {
+ owner: RendererPageMainDocumentLifecycleOwner {
+ root_document: self.root_document,
+ body,
+ },
+ completion,
+ },
+ ))
+ .map_err(|_| anyhow::anyhow!("main document lifecycle DOM task route closed"))
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) struct PageMainDocumentLifecycleTurnAction {
+ pub(crate) owner: RendererPageMainDocumentLifecycleOwner,
+ pub(crate) target: MainDocumentLifecycleTargetEffect,
+}
+
+pub(crate) type PageMainDocumentLifecycleTurnOutcome =
+ PageOwnerTurnOutcome;
diff --git a/moli-renderer-v8/src/page_task_queue/owner_sources.rs b/moli-renderer-v8/src/page_task_queue/owner_sources.rs
index 2b79f032c5..1d28325ae0 100644
--- a/moli-renderer-v8/src/page_task_queue/owner_sources.rs
+++ b/moli-renderer-v8/src/page_task_queue/owner_sources.rs
@@ -627,6 +627,15 @@ pub(crate) enum RendererPageSchedulerTask {
}
impl RendererPageOwnedTaskSources {
+ pub(crate) fn has_queued_main_document_lifecycle_dom_task(
+ &mut self,
+ root_document: crate::runtime::RendererDocumentToken,
+ document: crate::frame_owner_model::FrameDocumentTaskOwner,
+ ) -> bool {
+ self.dom_manipulation
+ .has_main_document_lifecycle_task(root_document, document)
+ }
+
pub(crate) fn new(
runtime_wake: PageRuntimeWakeSignal,
owner_wake: RendererOwnerWakeSender,
@@ -1958,6 +1967,43 @@ impl RendererPageOwnedTaskSourcesTestHarness {
})
}
+ /// Compare the two child semantic source heads without changing either FIFO.
+ pub(crate) fn next_child_semantic_task_target(
+ &self,
+ ) -> Option {
+ self.sources
+ .borrow_mut()
+ .ready_descriptors()
+ .into_iter()
+ .filter(|descriptor| {
+ matches!(
+ descriptor,
+ RendererPageReadyDescriptor::ChildFrameTask { .. }
+ | RendererPageReadyDescriptor::DomManipulation {
+ owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle(
+ _
+ ),
+ ..
+ }
+ )
+ })
+ .min_by_key(|descriptor| {
+ (
+ descriptor.runnable_since(),
+ descriptor.enqueue_order().unwrap_or(0),
+ descriptor.source_kind(),
+ )
+ })
+ .map(|descriptor| match descriptor {
+ RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
+ | RendererPageReadyDescriptor::DomManipulation {
+ owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner),
+ ..
+ } => owner.target(),
+ _ => unreachable!("child semantic selector must retain its exact target"),
+ })
+ }
+
/// Dequeue one production scheduler task selected by a narrow semantic
/// fixture. The selector sees the same descriptor as the owner scheduler;
/// this helper adds no lane-specific consumer or alternate queue model.
diff --git a/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs b/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs
index 1d9e99f8d1..ff8add88d3 100644
--- a/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs
+++ b/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs
@@ -141,12 +141,13 @@ pub(super) struct PendingDocumentLifecycleTurn {
/// can immediately take another owner turn without ordinary Page work.
///
/// This is stable residence state, not a wake hint. It keeps a runnable
- /// parser-finish chain (notably `interactive` -> DOMContentLoaded) from
- /// being displaced by an older resource terminal that became ready while
- /// the parser task was still executing.
+ /// parser preparation and lifecycle task admission contiguous. Once an
+ /// event is admitted to the DOM source, the resident waits for its receipt
+ /// and ordinary task arbitration owns event delivery.
pub(super) owner_turn_is_runnable: bool,
pub(super) driver: PostParseLifecycleDriver,
pub(super) completed_task: Option,
+ pub(super) awaiting_dom_task: Option,
pub(super) completion_action: Option,
/// Whether this exact resident owns a sealed main-parser defer/module
/// continuation.
@@ -158,3 +159,10 @@ pub(super) struct PendingDocumentLifecycleTurn {
pub(super) has_sealed_main_parser_script_queue: bool,
pub(super) started: Instant,
}
+
+pub(super) struct PendingDocumentLifecycleDomTask {
+ pub(super) task: PostParsePageOwnedTask,
+ pub(super) completion: tokio::sync::oneshot::Receiver<
+ crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion,
+ >,
+}
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..dead34c08c 100644
--- a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs
+++ b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs
@@ -763,11 +763,30 @@ pub(super) fn select_page_scheduler_turn(
!lifecycle_is_deferred && entry.document_lifecycle_owner_turn_is_runnable();
let has_ready_main_parser_script_continuation =
!lifecycle_is_deferred && entry.has_ready_main_parser_script_continuation();
- let document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state(
+ let mut document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state(
has_pending_document_lifecycle_turn,
document_lifecycle_owner_turn_is_runnable,
has_ready_main_parser_script_continuation,
);
+ let has_lifecycle_dom_task = entry
+ .page_vm()
+ .vm()
+ .current_main_document_task_owner()
+ .is_some_and(|owner| {
+ task_sources.has_queued_main_document_lifecycle_dom_task(
+ entry.page_vm().document_lifecycle.identity().document,
+ owner,
+ )
+ });
+ if has_lifecycle_dom_task
+ && !matches!(
+ document_lifecycle,
+ DocumentLifecycleClassReadiness::RunnableContinuation
+ | DocumentLifecycleClassReadiness::ReadyMainParserScriptContinuation
+ )
+ {
+ document_lifecycle = DocumentLifecycleClassReadiness::QueuedDomTask;
+ }
let gate_policy = lifecycle_gate
.as_mut()
.map(|gate| gate.turn_policy(entry, !snapshot.eligible.is_empty()))
@@ -794,8 +813,26 @@ pub(super) fn select_page_scheduler_turn(
),
},
Some(PageTurnClass::Ordinary) => {
+ // Preserve parser-finish priority across task sources while the
+ // lifecycle event occupies its real DOM FIFO position. Earlier DOM
+ // tasks run first; no descriptor can jump to the lifecycle payload.
+ let dom_head_is_eligible = snapshot.eligible.iter().any(|descriptor| {
+ matches!(
+ descriptor,
+ crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. }
+ )
+ });
+ // A child lifecycle head can be awaiting its exact realm. Allow
+ // that prerequisite source to run before restoring DOM priority.
+ let eligible = if has_lifecycle_dom_task && dom_head_is_eligible {
+ snapshot.eligible.into_iter().filter(|descriptor| {
+ matches!(descriptor, crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. })
+ }).collect()
+ } else {
+ snapshot.eligible
+ };
let selected = scheduler
- .select_ready_descriptor(snapshot.eligible)
+ .select_ready_descriptor(eligible)
.expect("selected ordinary Page-turn class must retain an eligible descriptor");
let task = task_sources.take_task(selected);
RendererPageScheduledTurn::Ordinary(Box::new(task))
@@ -815,6 +852,12 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task(
) -> (LivePageEntry, Result<()>) {
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
Box::pin(async move {
+ let main_lifecycle_owner = match &task {
+ RendererPageSchedulerTask::DomManipulation(
+ crate::page_task_queue::RendererPageDomManipulationTask::MainDocumentLifecycle(task),
+ ) => Some(task.owner),
+ _ => None,
+ };
let replacement_lifecycle_snapshot = entry
.page_vm()
.document_replacement_lifecycle_action_snapshot();
@@ -847,6 +890,21 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task(
.await
};
+ if application.is_ok()
+ && main_lifecycle_owner.is_some_and(|owner| {
+ owner.root_document == entry.page_vm().document_lifecycle.identity().document
+ && entry.page_vm().vm().current_main_document_task_owner() == Some(owner.body.owner())
+ })
+ {
+ let (_, pending) = entry.page_vm_and_document_lifecycle_turn_mut();
+ if let Some(pending) = pending {
+ // Resume the exact driver to consume its receipt. An
+ // executed event publishes the reached stage before other
+ // sources run; a blocked load returns to its prerequisites.
+ pending.owner_turn_is_runnable = true;
+ }
+ }
+
match (application, reconciliation) {
(Ok(()), Ok(_)) => Ok(()),
(Err(action_error), Ok(_)) => Err(action_error),
diff --git a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs
index 31c70fa86f..c07e454f59 100644
--- a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs
+++ b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs
@@ -81,6 +81,8 @@ pub(super) enum PageTurnClass {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum DocumentLifecycleClassReadiness {
Absent,
+ /// The DOM source owns the admitted event. Only its FIFO head may run.
+ QueuedDomTask,
Available,
RunnableContinuation,
ReadyMainParserScriptContinuation,
@@ -110,7 +112,7 @@ impl DocumentLifecycleClassReadiness {
}
const fn is_available(self) -> bool {
- !matches!(self, Self::Absent)
+ !matches!(self, Self::Absent | Self::QueuedDomTask)
}
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs b/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs
index bb97d14d0a..fcc7186d95 100644
--- a/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs
@@ -249,6 +249,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
+ awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue,
started,
@@ -372,6 +373,34 @@ impl PageVm {
return Ok(settled);
}
+ let pending = pending_document_lifecycle_turn
+ .as_mut()
+ .expect("post-parse lifecycle state should remain installed");
+ if let Some(waiting) = pending.awaiting_dom_task.as_mut() {
+ match waiting.completion.try_recv() {
+ Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => {
+ pending.completed_task = Some(pending.awaiting_dom_task.take().unwrap().task);
+ }
+ Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => {
+ // The attempt did not execute the load boundary. Its token
+ // must never tell the driver that this stage was reached.
+ pending.awaiting_dom_task = None;
+ self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![
+ PostParsePageOwnedWork::main_document_window_load(owner),
+ ]);
+ }
+ Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
+ return Ok(DocumentLifecycleTurnOutcome::blocked(
+ DocumentLifecycleTurnAction::None,
+ document,
+ ));
+ }
+ Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
+ anyhow::bail!("current lifecycle DOM task disappeared before completion");
+ }
+ }
+ }
+
// Feed the previous exact task result into the driver before other
// page work is admitted. `AwaitProgress` parks immediately; a producer
// wake returns to owner arbitration instead of waiting in this method.
@@ -412,17 +441,6 @@ impl PageVm {
)
.await
}
- PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
- self.run_classic_defer_timer_before_domcontentloaded(&request_client)
- .await?;
- self.outcome_after_exact_post_parse_action(
- pending_document_lifecycle_turn,
- document,
- stage,
- DocumentLifecycleTurnAction::Progressed,
- false,
- )
- }
PostParseLifecycleAdvance::NeedsContinuation => self
.outcome_after_exact_post_parse_action(
pending_document_lifecycle_turn,
@@ -570,6 +588,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
+ awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue: false,
started: Instant::now(),
@@ -598,6 +617,7 @@ impl PageVm {
owner_turn_is_runnable: _,
driver,
completed_task,
+ awaiting_dom_task: _,
completion_action,
has_sealed_main_parser_script_queue: _,
started: _,
@@ -620,6 +640,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task,
+ awaiting_dom_task: None,
completion_action,
has_sealed_main_parser_script_queue,
started: Instant::now(),
@@ -647,6 +668,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
+ awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue: self
.vm()
diff --git a/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs b/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs
index 2e66209eb1..dd38cb88c7 100644
--- a/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs
@@ -3,11 +3,9 @@
//! The exact lifecycle resident remains the sole ordinary DCL/load authority.
//! This component only reconciles an already-claimed body: typed checkpoint
//! continuations, milestone journal visibility, body settlement, and lifecycle
-//! priming stay in one auditable order. Parser completion may call this same
-//! component after claiming the exact DCL direct successor, but the parser
-//! continuation's own task-end checkpoint has already completed first. This
-//! preserves the observable task boundary without opening a scheduler round in
-//! which an ordinary Page task could overtake DCL.
+//! priming stay in one auditable order. DCL and load reach this coordinator
+//! through the shared DOM task source after parser completion has admitted
+//! them; interactive readiness remains part of parser completion.
use anyhow::Result;
use std::time::Instant;
@@ -42,17 +40,6 @@ pub(super) async fn execute_main_document_lifecycle_on_owner_local_task(
execute_main_document_lifecycle_body_on_owner_local_task(page_vm, body).await
}
-pub(super) async fn execute_parser_exact_domcontentloaded_on_owner_local_task(
- page_vm: &mut PageVm,
- owner: crate::frame_owner_model::FrameDocumentTaskOwner,
-) -> Result {
- execute_main_document_lifecycle_body_on_owner_local_task(
- page_vm,
- MainDocumentLifecycleBody::DomContentLoaded { owner },
- )
- .await
-}
-
async fn execute_main_document_lifecycle_body_on_owner_local_task(
page_vm: &mut PageVm,
body: MainDocumentLifecycleBody,
diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs
index b1d9e45482..aaf56e8c04 100644
--- a/moli-renderer-v8/src/runtime/page_vm/mod.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs
@@ -105,6 +105,7 @@ mod page_image_load_event;
mod page_indexed_db_task;
mod page_internal_loading;
mod page_internal_loading_task_completion;
+mod page_main_document_lifecycle;
mod page_main_document_post_parse;
mod page_main_document_runtime;
mod page_main_native_module_task;
@@ -2119,9 +2120,23 @@ impl PageVm {
) -> bool {
use crate::{
frame_owner_model::ChildFrameSemanticTurnKind,
- page_task_queue::RendererPageChildFrameTaskTarget,
+ page_task_queue::{
+ RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner,
+ RendererPageReadyDescriptor,
+ },
};
+ if expected == ChildFrameSemanticTurnKind::DocumentLifecycle {
+ return self.page_task_executor_sources_for_test().has_scheduler_task_for_executor_test(|descriptor| {
+ matches!(descriptor,
+ RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. }
+ ) || matches!(descriptor,
+ RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
+ if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
+ )
+ });
+ }
+
let Some(target) = self
.page_task_executor_sources_for_test()
.next_child_frame_task_target()
@@ -2961,7 +2976,11 @@ impl PageVm {
{
return Some(ChildFrameSemanticTurnKind::NavigationCommit);
}
- if self
+ if matches!(
+ self.page_task_executor_sources_for_test()
+ .next_child_semantic_task_target(),
+ Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
+ ) && self
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::ChildDocumentLifecycle,
&loader,
@@ -3170,11 +3189,61 @@ impl PageVm {
match advance {
PostParseLifecycleAdvance::PageOwnedTask(mut task) => {
let request_client = self.request_client.clone();
- self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane(
- &request_client,
- task.take_work_for_execution(),
- )
- .await?;
+ let work = task.take_work_for_execution();
+ if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work
+ && matches!(
+ **lifecycle,
+ PostParseLifecycleWork::DispatchDomContentLoaded { .. }
+ | PostParseLifecycleWork::DispatchWindowLoad { .. }
+ )
+ {
+ let body = crate::script_vm::MainDocumentLifecycleBody::from_post_parse_work(
+ lifecycle,
+ )
+ .unwrap();
+ let (sender, mut receiver) = tokio::sync::oneshot::channel();
+ self.vm()
+ .queue_main_document_lifecycle_dom_task(body, Some(sender))?;
+ if matches!(
+ body,
+ crate::script_vm::MainDocumentLifecycleBody::DomContentLoaded { .. }
+ ) {
+ self.run_ready_classic_defer_timers_before_domcontentloaded(
+ &request_client,
+ )
+ .await?;
+ }
+ loop {
+ match receiver.try_recv() {
+ Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => break,
+ Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => {
+ self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![PostParsePageOwnedWork::main_document_window_load(owner)]);
+ return Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)));
+ }
+ Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
+ anyhow::bail!(
+ "lifecycle DOM task disappeared before its fixture completion"
+ );
+ }
+ Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
+ anyhow::ensure!(
+ self.run_exact_selected_page_task_for_test(
+ PageSelectedTaskTestSelector::AnyDomManipulation,
+ &request_client,
+ )
+ .await?,
+ "queued lifecycle task must remain runnable"
+ );
+ }
+ }
+ }
+ } else {
+ self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane(
+ &request_client,
+ work,
+ )
+ .await?;
+ }
if self.vm().has_pending_location_navigation() {
return Ok(PostParseLifecycleLoopAdvance::Complete(
PostParseLifecycleCompletionAction::TriggeredNavigation,
@@ -3184,12 +3253,6 @@ impl PageVm {
*task,
))))
}
- PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
- let request_client = self.request_client.clone();
- self.run_classic_defer_timer_before_domcontentloaded(&request_client)
- .await?;
- Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
- }
PostParseLifecycleAdvance::NeedsContinuation => {
Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs
index 0f9e54154a..ba8398cf7c 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs
@@ -22,14 +22,22 @@ impl PageVm {
matches!(
descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
- if matches!(
+ | RendererPageReadyDescriptor::DomManipulation {
+ owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
+ } if matches!(
owner.target(),
RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
)
)
})?;
- let RendererPageSchedulerTask::ChildFrameTask(task) = task else {
- unreachable!("DocumentLifecycle descriptor must dequeue a child-frame task")
+ let task = match task {
+ RendererPageSchedulerTask::ChildFrameTask(task)
+ | RendererPageSchedulerTask::DomManipulation(
+ crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle(
+ task,
+ ),
+ ) => task,
+ _ => unreachable!("DocumentLifecycle descriptor must dequeue its admitted task"),
};
Some(self.apply_selected_page_child_document_lifecycle_turn(task))
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs
index ee5afe3f3e..5a24762e98 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs
@@ -1,15 +1,8 @@
-//! Task-end boundary for an exact child `Document` lifecycle action.
+//! Task-end boundary for an exact child Document lifecycle action.
//!
-//! Interactive, `DOMContentLoaded`, and complete transitions are ordinary
-//! selected child-frame tasks. Their bodies synchronously dispatch the
-//! corresponding document event when a wrapper still exists, but deliberately
-//! leave Promise reactions pending. This module maps that execution-produced
-//! fact to the sole Page task-completion authority.
-//!
-//! This does not govern the parser-owned direct-successor boundary where the
-//! final deferred script completes parsing and synchronously dispatches DCL in
-//! the same task. That separately typed parser completion path must remain
-//! contiguous and must not be turned into another scheduler task here.
+//! DCL and complete use the shared DOM source. Interactive readiness belongs
+//! to parser stop; an unmaterialized realm may need a child owner continuation.
+//! The selected dispatcher completes the event's checkpoint and follow-up.
use crate::page_task_queue::{
PageChildDocumentLifecycleTargetEffect, PageChildDocumentLifecycleTurnAction,
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs
index 15055e1bff..a9610941e9 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs
@@ -5,11 +5,20 @@ use crate::page_task_queue::{
use super::PageVm;
impl PageVm {
- pub(in crate::runtime) fn apply_selected_page_dom_manipulation_turn(
+ pub(in crate::runtime) async fn apply_selected_page_dom_manipulation_turn(
&mut self,
task: RendererPageDomManipulationTask,
) -> anyhow::Result {
match task {
+ RendererPageDomManipulationTask::ChildDocumentLifecycle(task) => Ok(self
+ .apply_selected_page_child_document_lifecycle_turn(task)
+ .map_action(PageDomManipulationTurnAction::ChildDocumentLifecycle)),
+ RendererPageDomManipulationTask::MainDocumentLifecycle(task) => self
+ .apply_selected_page_main_document_lifecycle_turn(task)
+ .await
+ .map(|outcome| {
+ outcome.map_action(PageDomManipulationTurnAction::MainDocumentLifecycle)
+ }),
RendererPageDomManipulationTask::BroadcastChannel(task) => self
.apply_selected_page_broadcast_channel_delivery_turn(task)
.map(|outcome| outcome.map_action(PageDomManipulationTurnAction::BroadcastChannel)),
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs
index 00f33de262..a39139e114 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs
@@ -18,6 +18,12 @@ impl PageVm {
loader: &crate::network::ResourceRequestClient,
) -> Result<()> {
let completion = match action {
+ PageDomManipulationTurnAction::ChildDocumentLifecycle(action) => {
+ action.into_page_task_completion()
+ }
+ PageDomManipulationTurnAction::MainDocumentLifecycle(action) => {
+ action.into_page_task_completion()
+ }
PageDomManipulationTurnAction::BroadcastChannel(action) => {
action.into_page_task_completion()
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs
index b1871de9d9..39d215278c 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs
@@ -21,6 +21,7 @@ pub(crate) enum PageDomManipulationTestFamily {
FileEntryFileCallback,
ScriptPreparationError,
PromiseRejection,
+ MainDocumentLifecycle,
ImageLoadEvent,
PopupLoadEvent,
ConnectedStyleEvent,
@@ -34,6 +35,9 @@ impl PageDomManipulationTestFamily {
matches!(
(self, owner),
(
+ Self::MainDocumentLifecycle,
+ RendererPageDomManipulationOwner::MainDocumentLifecycle(_)
+ ) | (
Self::BroadcastChannel,
RendererPageDomManipulationOwner::BroadcastChannel(_)
) | (
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs
new file mode 100644
index 0000000000..6629468123
--- /dev/null
+++ b/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs
@@ -0,0 +1,125 @@
+use crate::{
+ page_task_queue::{
+ PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome,
+ RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleTask,
+ },
+ runtime::PendingDocumentLifecycleTurn,
+ script_vm::{
+ MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect,
+ MainDocumentLifecycleTargetRejection, PostParsePageOwnedTask,
+ },
+};
+
+use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm};
+
+/// Event bodies finish their own checkpoints through the lifecycle coordinator.
+/// A blocked or stale task does not enter page code.
+impl IntoPageTaskCompletion for PageMainDocumentLifecycleTurnAction {
+ fn into_page_task_completion(self) -> PageTaskCompletion {
+ PageTaskCompletion::NoCompletion
+ }
+}
+
+impl PageVm {
+ pub(super) async fn run_ready_classic_defer_timers_before_domcontentloaded(
+ &mut self,
+ loader: &crate::network::ResourceRequestClient,
+ ) -> anyhow::Result<()> {
+ // Preserve the existing cross-source timer preference, after DCL has
+ // acquired its FIFO position in the DOM task source.
+ while self
+ .vm()
+ .document_runtime
+ .has_ready_timeout_queued_by_classic_defer_script()
+ {
+ self.run_classic_defer_timer_before_domcontentloaded(loader)
+ .await?;
+ }
+ Ok(())
+ }
+
+ pub(super) fn queue_post_parse_lifecycle_dom_task(
+ &self,
+ pending: &mut PendingDocumentLifecycleTurn,
+ body: MainDocumentLifecycleBody,
+ task: PostParsePageOwnedTask,
+ ) -> anyhow::Result<()> {
+ anyhow::ensure!(
+ pending.awaiting_dom_task.is_none() && pending.completed_task.is_none(),
+ "a lifecycle resident may await only one exact task"
+ );
+ let (sender, receiver) = tokio::sync::oneshot::channel();
+ self.vm()
+ .queue_main_document_lifecycle_dom_task(body, Some(sender))?;
+ pending.awaiting_dom_task = Some(
+ super::super::document_lifecycle_turn::PendingDocumentLifecycleDomTask {
+ task,
+ completion: receiver,
+ },
+ );
+ Ok(())
+ }
+
+ pub(super) async fn apply_selected_page_main_document_lifecycle_turn(
+ &mut self,
+ task: RendererPageMainDocumentLifecycleTask,
+ ) -> anyhow::Result {
+ let RendererPageMainDocumentLifecycleTask { owner, completion } = task;
+ if owner.root_document == self.document_lifecycle.identity().document
+ && let MainDocumentLifecycleBody::WindowLoad {
+ owner: document_owner,
+ } = owner.body
+ && self
+ .vm_mut()
+ .main_document_window_load_task_is_ready(document_owner)
+ == Some(false)
+ {
+ // The DOM queue may have run a producer since admission. Do not
+ // open the milestone journal or consume its boundary token while
+ // the exact Document has a new load delay.
+ let completion =
+ completion.expect("queued Window load must retain its lifecycle driver");
+ let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::LoadBlocked {
+ owner: document_owner,
+ });
+ return Ok(PageMainDocumentLifecycleTurnOutcome::new(
+ PageMainDocumentLifecycleTurnAction {
+ owner,
+ target: MainDocumentLifecycleTargetEffect::NotApplied {
+ reason: MainDocumentLifecycleTargetRejection::TransitionRejected,
+ current_owner: self.vm().current_main_document_task_owner(),
+ },
+ },
+ ));
+ }
+ let target = if owner.root_document == self.document_lifecycle.identity().document {
+ let run = super::main_document_lifecycle_completion::execute_main_document_lifecycle_on_owner_local_task(
+ self, owner.body,
+ ).await?;
+ let target = run.completion.target();
+ if let crate::script_vm::MainDocumentLifecycleFollowup::ScheduleInternalLoading {
+ task,
+ ready_at,
+ } = run.completion.into_followup()
+ {
+ self.vm()
+ .schedule_page_internal_loading_task(task, ready_at)?;
+ }
+ // The common selected Page owner reconciles document.open() after
+ // the body and its checkpoints, just as for other DOM tasks.
+ target
+ } else {
+ MainDocumentLifecycleTargetEffect::NotApplied {
+ reason: MainDocumentLifecycleTargetRejection::TransitionRejected,
+ current_owner: self.vm().current_main_document_task_owner(),
+ }
+ };
+ if let Some(completion) = completion {
+ // Replacement may have retired the receiver during this task.
+ let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::Executed);
+ }
+ Ok(PageMainDocumentLifecycleTurnOutcome::new(
+ PageMainDocumentLifecycleTurnAction { owner, target },
+ ))
+ }
+}
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs
index bb77c451b9..5549dd0ced 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs
@@ -52,6 +52,7 @@ pub(crate) enum PageSelectedTaskTestSelector {
ChildNavigationCommit,
ChildModulepreloadEventAction,
DomManipulation(PageDomManipulationTestFamily),
+ AnyDomManipulation,
DedicatedWorkerClientEvent,
DynamicImportOwnerAction,
FileReading,
@@ -89,6 +90,9 @@ impl PageSelectedTaskTestSelector {
Self::ChildDocumentLifecycle => matches!(
descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
+ | RendererPageReadyDescriptor::DomManipulation {
+ owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
+ }
if matches!(
owner.target(),
crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -153,6 +157,10 @@ impl PageSelectedTaskTestSelector {
descriptor,
RendererPageReadyDescriptor::ChildModulepreloadEventAction { .. }
),
+ Self::AnyDomManipulation => matches!(
+ descriptor,
+ RendererPageReadyDescriptor::DomManipulation { .. }
+ ),
Self::DomManipulation(family) => matches!(
descriptor,
RendererPageReadyDescriptor::DomManipulation { owner, .. }
@@ -308,7 +316,13 @@ impl PageSelectedTaskTestSelector {
fn matches_task(self, task: &RendererPageSchedulerTask) -> bool {
match (self, task) {
- (Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task)) => {
+ (Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task))
+ | (
+ Self::ChildDocumentLifecycle,
+ RendererPageSchedulerTask::DomManipulation(
+ RendererPageDomManipulationTask::ChildDocumentLifecycle(task),
+ ),
+ ) => {
matches!(
task.owner().target(),
crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -369,6 +383,7 @@ impl PageSelectedTaskTestSelector {
Self::ChildModulepreloadEventAction,
RendererPageSchedulerTask::ChildModulepreloadEventAction(_),
) => true,
+ (Self::AnyDomManipulation, RendererPageSchedulerTask::DomManipulation(_)) => true,
(Self::DomManipulation(family), RendererPageSchedulerTask::DomManipulation(task)) => {
family.matches_owner(task.owner())
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs
index bce2e08304..48f11b8e12 100644
--- a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs
@@ -74,6 +74,27 @@ impl PageVm {
descriptor: RendererPageReadyDescriptor,
) -> bool {
match descriptor {
+ RendererPageReadyDescriptor::DomManipulation {
+ owner:
+ crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(
+ owner,
+ ),
+ ..
+ } => {
+ if owner.root_document() != self.document_lifecycle.identity().document {
+ return true;
+ }
+ let crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(
+ target,
+ ) = owner.target()
+ else {
+ unreachable!("child lifecycle DOM carrier must retain its exact target");
+ };
+ // A current task retains its DOM FIFO position while its
+ // separately queued realm prerequisite runs. It is not stale
+ // merely because that realm has not materialized yet.
+ !self.vm().child_document_lifecycle_waits_for_realm(target)
+ }
RendererPageReadyDescriptor::ActionWindow { .. }
| RendererPageReadyDescriptor::DomManipulation { .. }
| RendererPageReadyDescriptor::UserInteraction { .. }
diff --git a/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs b/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs
index 0b9b1e8dac..631024ffad 100644
--- a/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs
@@ -4,8 +4,8 @@
//! owns the bounded handoff from that work to parser completion: it consumes a
//! one-shot drained-queue permit, asks the lifecycle authority to claim the
//! exact DOMContentLoaded successor, closes the selected parser task's
-//! checkpoint boundary, and only then hands that already-claimed successor to
-//! the lifecycle coordinator. The coordinator applies it to the surviving
+//! checkpoint boundary, and admits the claimed successor to the shared DOM
+//! task source. Its selected-task coordinator applies it to the surviving
//! exact Document or stale-rejects it after replacement.
//! It is not a scheduler lane and stores no durable lifecycle state.
@@ -16,9 +16,7 @@ use crate::frame_owner_model::FrameDocumentTaskOwner;
use crate::network::ResourceRequestClient;
use crate::page_task_queue::{PostParseLifecycleWork, PostParsePageOwnedWork};
use crate::runtime::{PendingDocumentLifecycleTurn, RendererDocumentLifecycleIdentity};
-use crate::script_vm::{
- MainDocumentLifecycleBodyKind, ParserFinishDomContentLoadedTask, PostParsePageOwnedTask,
-};
+use crate::script_vm::{MainDocumentLifecycleBody, PostParsePageOwnedTask};
use super::super::document_lifecycle_turn::{
DocumentLifecycleTurnAction, DocumentLifecycleTurnOutcome,
@@ -164,31 +162,20 @@ impl ParserCompletion {
};
}
};
- // The parser continuation and DOMContentLoaded are distinct HTML task
- // boundaries even though this Chromium-compatible direct successor
- // deliberately does not reopen ordinary scheduler arbitration. Drain
- // terminal reactions first; the lifecycle coordinator then owns DCL's
- // separate task-end checkpoint.
+ // Finish parser reactions before admitting the DCL global task. An
+ // earlier DOM task keeps its FIFO position; later timer callbacks may
+ // enqueue DOM work only behind this already admitted lifecycle task.
Self::finish_task(page_vm, task_effect)?;
+ page_vm.vm().queue_main_document_lifecycle_dom_task(
+ MainDocumentLifecycleBody::DomContentLoaded {
+ owner: successor_owner,
+ },
+ None,
+ )?;
let request_client = page_vm.request_client.clone();
- while page_vm
- .vm()
- .document_runtime
- .has_ready_timeout_queued_by_classic_defer_script()
- {
- page_vm
- .run_classic_defer_timer_before_domcontentloaded(&request_client)
- .await?;
- }
- let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task(
- page_vm,
- successor_owner,
- )
- .await?;
- anyhow::ensure!(
- run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded,
- "parse-time DOMContentLoaded successor lost its typed lifecycle execution"
- );
+ page_vm
+ .run_ready_classic_defer_timers_before_domcontentloaded(&request_client)
+ .await?;
Ok(())
}
@@ -228,32 +215,6 @@ impl PageVm {
ParserCompletion::finish_parse_time(self, completion).await
}
- async fn execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane(
- &mut self,
- mut task: ParserFinishDomContentLoadedTask,
- ) -> Result {
- let owner = task.owner();
- let work = task.take_work_for_execution();
- let PostParsePageOwnedWork::Lifecycle(work) = work else {
- anyhow::bail!("parser-finish successor is not lifecycle work");
- };
- let successor_owner = match ParserCompletion::exact_domcontentloaded_owner(*work, owner) {
- Ok(owner) => owner,
- Err(message) => anyhow::bail!(message),
- };
-
- let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task(
- self,
- successor_owner,
- )
- .await?;
- anyhow::ensure!(
- run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded,
- "DOMContentLoaded direct successor lost its typed lifecycle execution"
- );
- Ok(task.into_completed_task())
- }
-
pub(super) async fn execute_and_complete_selected_post_parse_page_owned_task(
&mut self,
loader: &ResourceRequestClient,
@@ -262,12 +223,38 @@ impl PageVm {
stage: PageVmInitStage,
mut task: Box,
) -> Result {
+ let work = task.take_work_for_execution();
+ if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work
+ && matches!(
+ **lifecycle,
+ PostParseLifecycleWork::DispatchDomContentLoaded { .. }
+ | PostParseLifecycleWork::DispatchWindowLoad { .. }
+ )
+ {
+ let body = MainDocumentLifecycleBody::from_post_parse_work(lifecycle)
+ .expect("selected DCL/load work must have a typed lifecycle body");
+ self.queue_post_parse_lifecycle_dom_task(
+ pending_document_lifecycle_turn
+ .as_mut()
+ .expect("lifecycle resident must remain installed"),
+ body,
+ *task,
+ )?;
+ if matches!(body, MainDocumentLifecycleBody::DomContentLoaded { .. }) {
+ self.run_ready_classic_defer_timers_before_domcontentloaded(loader)
+ .await?;
+ }
+ return self.outcome_after_exact_post_parse_action(
+ pending_document_lifecycle_turn,
+ document,
+ stage,
+ DocumentLifecycleTurnAction::Progressed,
+ true,
+ );
+ }
let replacement_lifecycle_snapshot = self.document_replacement_lifecycle_action_snapshot();
let execution = self
- .execute_post_parse_page_owned_task_on_named_owner_lane(
- loader,
- task.take_work_for_execution(),
- )
+ .execute_post_parse_page_owned_task_on_named_owner_lane(loader, work)
.await;
// Generic post-parse callbacks must finish their old-realm task before
// a synchronous `document.open()` replacement is admitted. MainParser
@@ -416,50 +403,32 @@ impl PageVm {
};
}
};
- if let Some(dcl_task) = claimed_dcl {
+ if let Some(mut dcl_task) = claimed_dcl {
ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?;
- while self
- .vm()
- .document_runtime
- .has_ready_timeout_queued_by_classic_defer_script()
- {
- self.run_classic_defer_timer_before_domcontentloaded(loader)
- .await?;
- }
- let replacement_lifecycle_snapshot =
- self.document_replacement_lifecycle_action_snapshot();
- let execution = self
- .execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane(
- dcl_task,
- )
- .await;
- let admission = self
- .take_document_replacement_lifecycle_admission_after_action(
- replacement_lifecycle_snapshot,
- );
- let completed_dcl_task = match (execution, admission) {
- (Ok(task), Ok(_)) => task,
- (Err(execution_error), Ok(_)) => return Err(execution_error),
- (Ok(_), Err(admission_error)) => return Err(admission_error),
- (Err(execution_error), Err(admission_error)) => {
- return Err(anyhow::anyhow!(
- "parser-finish DOMContentLoaded action failed ({execution_error:#}) and its Document replacement admission also failed ({admission_error:#})"
- ));
- }
+ let owner = dcl_task.owner();
+ let PostParsePageOwnedWork::Lifecycle(work) =
+ dcl_task.take_work_for_execution()
+ else {
+ anyhow::bail!("parser-finish successor is not lifecycle work");
};
- pending_document_lifecycle_turn
- .as_mut()
- .expect("post-parse lifecycle state should remain installed")
- .completed_task = Some(completed_dcl_task);
- if let Some(outcome) = self
- .transition_lifecycle_for_pending_top_level_navigation(
- pending_document_lifecycle_turn,
- document,
- stage,
- )
- {
- return Ok(outcome);
- }
+ let owner = ParserCompletion::exact_domcontentloaded_owner(*work, owner)
+ .map_err(anyhow::Error::msg)?;
+ self.queue_post_parse_lifecycle_dom_task(
+ pending_document_lifecycle_turn
+ .as_mut()
+ .expect("lifecycle resident must remain installed"),
+ MainDocumentLifecycleBody::DomContentLoaded { owner },
+ dcl_task.into_pending_task(),
+ )?;
+ self.run_ready_classic_defer_timers_before_domcontentloaded(loader)
+ .await?;
+ return self.outcome_after_exact_post_parse_action(
+ pending_document_lifecycle_turn,
+ document,
+ stage,
+ DocumentLifecycleTurnAction::Progressed,
+ true,
+ );
} else {
ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?;
}
diff --git a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs
index 35d63f8740..2d1d0136ec 100644
--- a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs
@@ -23,7 +23,7 @@ impl PageVm {
self.apply_selected_page_action_window_turn(deadline)
}
RendererPageSchedulerTask::DomManipulation(task) => {
- let outcome = self.apply_selected_page_dom_manipulation_turn(task)?;
+ let outcome = self.apply_selected_page_dom_manipulation_turn(task).await?;
self.finish_selected_page_dom_manipulation_task(outcome.action, loader)
.await?;
Ok(())
diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs
index d5fb298a4d..62556ec9ae 100644
--- a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs
@@ -206,6 +206,26 @@ async fn advance_unblocked_exact_lifecycle_to_stage(
},
..
} => assert_eq!(*runnable_document, document),
+ DocumentLifecycleTurnOutcome {
+ readiness: DocumentLifecycleTurnReadiness::Blocked { document: waiting },
+ ..
+ } if pending
+ .as_ref()
+ .is_some_and(|pending| pending.awaiting_dom_task.is_some()) =>
+ {
+ assert_eq!(*waiting, document);
+ let loader = page_vm.request_client.clone();
+ assert!(
+ page_vm
+ .run_exact_selected_page_task_for_test(
+ super::super::PageSelectedTaskTestSelector::DomManipulation(
+ super::super::PageDomManipulationTestFamily::MainDocumentLifecycle
+ ),
+ &loader
+ )
+ .await?
+ );
+ }
_ => panic!(
"unblocked exact lifecycle should remain runnable before {stage:?}: {outcome:?}"
),
@@ -267,6 +287,47 @@ async fn prepare_parse_time_exact_domcontentloaded(
Ok(owner)
}
+impl PageVm {
+ async fn advance_lifecycle_and_selected_dom_task_for_test(
+ &mut self,
+ pending: &mut Option,
+ document: RendererDocumentLifecycleIdentity,
+ ) -> anyhow::Result {
+ for _ in 0..64 {
+ let outcome = self
+ .advance_post_parse_lifecycle_one_owner_turn(pending, document)
+ .await?;
+ if !matches!(
+ outcome.readiness,
+ DocumentLifecycleTurnReadiness::Blocked { .. }
+ ) || pending
+ .as_ref()
+ .is_none_or(|pending| pending.awaiting_dom_task.is_none())
+ {
+ return Ok(outcome);
+ }
+ let snapshot = self.document_replacement_lifecycle_action_snapshot();
+ let loader = self.request_client.clone();
+ assert!(
+ self.run_exact_selected_page_task_for_test(
+ super::super::PageSelectedTaskTestSelector::DomManipulation(
+ super::super::PageDomManipulationTestFamily::MainDocumentLifecycle,
+ ),
+ &loader,
+ )
+ .await?
+ );
+ if let Some(outcome) = self
+ .reconcile_document_replacement_lifecycle_after_owner_action(snapshot, pending)
+ .await?
+ {
+ return Ok(outcome);
+ }
+ }
+ anyhow::bail!("lifecycle fixture exceeded its bounded DOM task turns")
+ }
+}
+
async fn exact_lifecycle_turn_reaches_handler_navigation(
event_target: &str,
event_name: &str,
@@ -303,7 +364,7 @@ async fn exact_lifecycle_turn_reaches_handler_navigation(
for _ in 0..128 {
match page_vm
- .advance_post_parse_lifecycle_one_owner_turn(&mut pending, document)
+ .advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document)
.await?
{
DocumentLifecycleTurnOutcome {
@@ -369,7 +430,7 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement(
for _ in 0..96 {
match page_vm
- .advance_post_parse_lifecycle_one_owner_turn(
+ .advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -401,7 +462,17 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement(
},
..
} => {
- assert_eq!(next_document, document);
+ if next_document != document {
+ assert_ne!(next_document, initial_document);
+ assert_eq!(page_vm.document_lifecycle.identity(), next_document);
+ assert_eq!(
+ pending_document_lifecycle_turn
+ .as_ref()
+ .map(|pending| pending.document),
+ Some(next_document)
+ );
+ return Ok(());
+ }
document = next_document;
}
DocumentLifecycleTurnOutcome {
@@ -576,7 +647,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() {
for _ in 0..128 {
let outcome = page_vm
- .advance_post_parse_lifecycle_one_owner_turn(&mut pending, document)
+ .advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document)
.await?;
if !matches!(
outcome.action,
@@ -622,7 +693,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() {
for _ in 0..128 {
let resumed = page_vm
- .advance_post_parse_lifecycle_one_owner_turn(
+ .advance_lifecycle_and_selected_dom_task_for_test(
&mut pending,
document,
)
@@ -1729,6 +1800,15 @@ Promise.resolve().then(() => __parseTimeExactDclOrder.push("module-terminal:micr
outcome,
ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained
);
+ assert_eq!(
+ page_vm.vm_mut().eval_without_microtask_checkpoint_for_test(
+ "__parseTimeExactDclOrder.join('|')"
+ )?,
+ "module-terminal:microtask",
+ "parser completion must only enqueue DCL"
+ );
+ let loader = page_vm.request_client.clone();
+ assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?);
assert_eq!(
page_vm.vm_mut().eval_without_microtask_checkpoint_for_test(
"__parseTimeExactDclOrder.join('|')"
@@ -1789,6 +1869,8 @@ Promise.resolve().then(() => {
ParseTimeMainParserBoundaryOutcome::DocumentReplaced,
"the parser task-end checkpoint must report replacement before attempting the old exact DCL"
);
+ let loader = page_vm.request_client.clone();
+ assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?);
assert_ne!(
page_vm.vm().current_main_document_task_owner(),
Some(owner)
@@ -1918,13 +2000,16 @@ document.addEventListener("DOMContentLoaded", () => {
.await?;
assert_eq!(
outcome,
- ParseTimeMainParserBoundaryOutcome::DocumentReplaced,
- "phase one must stop the retired Document after an exact-DCL reaction replaces it"
+ ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained,
+ "the parser returns before the queued DCL listener can replace its Document"
);
- assert_ne!(
- page_vm.vm().current_main_document_task_owner(),
- Some(owner)
+ let loader = page_vm.request_client.clone();
+ assert!(
+ page_vm
+ .run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader)
+ .await?
);
+ assert_ne!(page_vm.vm().current_main_document_task_owner(), Some(owner));
assert_eq!(
page_vm.vm_mut().eval(
"String(document.getElementById('parse-time-replacement') !== null)"
@@ -2377,7 +2462,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() {
let dcl_outcome = loop {
let outcome = page_vm
- .advance_post_parse_lifecycle_one_owner_turn(
+ .advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -2437,7 +2522,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() {
loop {
match page_vm
- .advance_post_parse_lifecycle_one_owner_turn(
+ .advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -3605,7 +3690,7 @@ fn top_level_http_location_navigation_reserves_service_worker_client_until_commi
} => {
loop {
match page_vm
- .advance_post_parse_lifecycle_one_owner_turn(
+ .advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
lifecycle_document,
)
diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs
index 6b95448ee8..c0303d9f99 100644
--- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs
@@ -2053,12 +2053,20 @@ async fn run_child_interactive_domcontentloaded_then_host_load_for_wait(
page_vm: &mut PageVm,
label: &str,
) -> ChildFrameSemanticTurnKind {
- run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
- page_vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- &format!("{label} interactive transition"),
- )
- .await;
+ if page_vm.has_ready_child_frame_semantic_turn_for_test(
+ ChildFrameSemanticTurnKind::RealmMaterialization,
+ ) {
+ run_expected_child_realm_materialization_for_wait(page_vm, label).await;
+ }
+ if matches!(page_vm.page_task_executor_sources_for_test().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)))
+ {
+ run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
+ page_vm,
+ ChildFrameSemanticTurnKind::DocumentLifecycle,
+ &format!("{label} queued interactive transition"),
+ )
+ .await;
+ }
run_child_domcontentloaded_then_host_load_for_wait(page_vm, label).await
}
@@ -3439,14 +3447,7 @@ globalThis.__childClassicWaitValue = 91;
"external:true|current:external-classic|inline:91",
"second DocumentScriptReady should run parser continuation without iframe load"
);
- followup_sources.push(
- run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
- &mut page_vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child classic parser EOF interactive transition",
- )
- .await,
- );
+ assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously");
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
@@ -3535,10 +3536,9 @@ globalThis.__childClassicWaitValue = 91;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad
],
- "child classic completion should progress through script, parser continuation, interactive, DOMContentLoaded, complete, and HostLoad turns"
+ "child classic completion should progress through script, parser continuation, DOMContentLoaded, complete, and HostLoad turns"
);
assert_eq!(
final_events,
@@ -3679,14 +3679,7 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet()
"script:stylesheet-ready:loading",
"stylesheet source must be installed before the parser-blocking script executes while the document is still loading"
);
- followup_sources.push(
- run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
- &mut page_vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "stylesheet-gated child interactive transition",
- )
- .await,
- );
+ assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously");
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
@@ -3750,7 +3743,6 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet()
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad,
]
);
@@ -4516,9 +4508,8 @@ globalThis.__childClassicDeferWaitValue = 73;
ChildFrameSemanticTurnKind::RealmMaterialization,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
- ChildFrameSemanticTurnKind::DocumentLifecycle
],
- "child defer classic bootstrap should end with the document-owned interactive turn"
+ "child defer classic bootstrap should apply interactive at parser EOF"
);
assert_eq!(
followup_sources,
@@ -5100,7 +5091,6 @@ globalThis.__childClassicAsyncWaitValue = 41;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
- ChildFrameSemanticTurnKind::DocumentLifecycle
],
"child async bootstrap should dispatch interactive and DCL before the later async completion"
);
@@ -5347,7 +5337,6 @@ parent.__childClassicDeferOrderEvents.push("current:" + document.currentScript.i
ChildFrameSemanticTurnKind::RealmMaterialization,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
- ChildFrameSemanticTurnKind::DocumentLifecycle
],
"child defer ordering bootstrap should reach interactive before either source completion"
);
@@ -5782,8 +5771,8 @@ async fn page_vm_child_parser_defer_preserves_cross_kind_document_order() {
"parser EOF must not execute any mixed parser-deferred script"
);
assert!(
- bootstrap_sources.contains(&ChildFrameSemanticTurnKind::DocumentLifecycle),
- "mixed parser-deferred document should reach interactive"
+ page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")? == "interactive",
+ "mixed parser-deferred document should become interactive at parser EOF"
);
assert_eq!(
bootstrap_sources
@@ -6493,7 +6482,6 @@ globalThis.__childParserModuleWaitValue = 188;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::ParserModuleRootStart,
ChildFrameSemanticTurnKind::DocumentScriptReady,
- ChildFrameSemanticTurnKind::DocumentLifecycle
],
"the typed root fetch-start must preserve parser discovery FIFO, then the parser should run the following inline script and reach interactive"
);
@@ -8865,8 +8853,8 @@ async fn page_vm_child_modulepreload_terminal_event_does_not_delay_complete() {
.iter()
.filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle)
.count()
- >= 3,
- "interactive, DOMContentLoaded, and complete must advance while the terminal link event remains queued: {lifecycle_turns:?}"
+ == 2,
+ "DOMContentLoaded and complete must each take a queued turn while the terminal link event remains queued: {lifecycle_turns:?}"
);
assert!(
lifecycle_turns.contains(&ChildFrameSemanticTurnKind::HostLoad),
@@ -9082,8 +9070,8 @@ async fn page_vm_child_modulepreload_fetch_does_not_delay_iframe_load() {
.iter()
.filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle)
.count()
- >= 3,
- "interactive, DOMContentLoaded and complete should run while modulepreload fetch is pending: {startup_sources:?}"
+ == 2,
+ "DOMContentLoaded and complete should each take a queued turn while modulepreload fetch is pending: {startup_sources:?}"
);
assert!(startup_sources.contains(&ChildFrameSemanticTurnKind::HostLoad));
assert_eq!(
@@ -9565,7 +9553,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
first_followup_source,
events_after_first_followup,
lifecycle_ready_after_first_followup,
- interactive_source,
+ interactive_state,
host_load_source,
final_events,
) = local_executor
@@ -9644,12 +9632,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
ChildFrameSemanticTurnKind::DocumentLifecycle,
);
- let interactive_source = run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
- &mut page_vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child document parser EOF interactive transition",
- )
- .await;
+ let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
"child document iframe load",
@@ -9672,7 +9655,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
first_followup_source,
events_after_first_followup,
lifecycle_ready_after_first_followup,
- interactive_source,
+ interactive_state,
host_load_source,
final_events,
))
@@ -9710,8 +9693,8 @@ globalThis.__childDocumentLoadWaitValue = 42;
"document-script ready should make the later lifecycle turn runnable"
);
assert_eq!(
- interactive_source,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
+ interactive_state,
+ "interactive",
"parser EOF should become interactive before HostLoad"
);
assert_eq!(
@@ -9885,8 +9868,6 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self)
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
- page_vm.run_next_child_frame_task_source_for_semantic_test().await,
- page_vm.run_next_child_frame_task_source_for_semantic_test().await,
];
let first_host_load_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let events_after_first_host_load = page_vm
@@ -9966,8 +9947,8 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self)
);
assert_eq!(
lifecycle_sources,
- vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 6],
- "interactive, DOMContentLoaded and complete must each consume one lifecycle turn per child"
+ vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 4],
+ "DOMContentLoaded and complete must each consume one lifecycle turn per child"
);
assert_eq!(
first_host_load_source,
@@ -10034,7 +10015,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
script_ready_source,
events_after_script_ready,
lifecycle_ready_after_script,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
) = local_executor
@@ -10092,7 +10073,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
ChildFrameSemanticTurnKind::DocumentLifecycle,
);
- let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
+ let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -10111,7 +10092,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
script_ready_source,
events_after_script_ready,
lifecycle_ready_after_script,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
))
@@ -10146,8 +10127,8 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
"DocumentScriptReady should make the later lifecycle turn runnable"
);
assert_eq!(
- interactive_source,
- Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
+ interactive_state,
+ "interactive",
"parser EOF should dispatch interactive before HostLoad"
);
assert_eq!(
@@ -10771,7 +10752,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script
.run_next_child_frame_task_source_for_semantic_test()
.await,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
- "outer parser EOF should become interactive before nested document work"
+ "outer DOMContentLoaded must precede nested parser work admitted after it"
);
assert_eq!(
page_vm
@@ -10792,7 +10773,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script
.run_next_child_frame_task_source_for_semantic_test()
.await,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
- "outer DOMContentLoaded should remain a later FIFO turn after nested work already admitted by realm materialization"
+ "nested DOMContentLoaded must remain a later FIFO turn after its parser work"
);
Ok::<_, anyhow::Error>(())
})
@@ -11344,12 +11325,12 @@ onload = () => {
host_load_pending_after_completion,
script_ready_source,
events_after_script_ready,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
nested_script_ready_source,
events_after_nested_script_ready,
- nested_interactive_source,
+ nested_interactive_state,
nested_host_load_source,
events_after_nested_host_load,
) = local_executor
@@ -11393,7 +11374,7 @@ onload = () => {
let events_after_script_ready = page_vm
.vm_mut()
.eval("__hostLoadNestedEvents.join('|')")?;
- let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
+ let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11419,8 +11400,7 @@ onload = () => {
let events_after_nested_script_ready = page_vm
.vm_mut()
.eval("__hostLoadNestedEvents.join('|')")?;
- let nested_interactive_source =
- page_vm.run_next_child_frame_task_source_for_semantic_test().await;
+ let nested_interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.querySelector('iframe').contentDocument.readyState")?;
let nested_host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11438,12 +11418,12 @@ onload = () => {
host_load_pending_after_completion,
script_ready_source,
events_after_script_ready,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
nested_script_ready_source,
events_after_nested_script_ready,
- nested_interactive_source,
+ nested_interactive_state,
nested_host_load_source,
events_after_nested_host_load,
))
@@ -11471,8 +11451,8 @@ onload = () => {
);
assert_eq!(events_after_script_ready, "child-script");
assert_eq!(
- interactive_source,
- Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
+ interactive_state,
+ "interactive",
"child parser EOF should become interactive before window load"
);
assert_eq!(
@@ -11494,8 +11474,8 @@ onload = () => {
"nested script should run on the later DocumentScriptReady turn"
);
assert_eq!(
- nested_interactive_source,
- Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
+ nested_interactive_state,
+ "interactive",
"nested parser EOF should become interactive before nested HostLoad"
);
assert_eq!(
@@ -11586,7 +11566,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
completion_source,
script_ready_source,
events_after_script_ready,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
pending_after_host_load,
@@ -11638,7 +11618,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
.vm_mut()
.eval("__childLoadNavigationEvents.join('|')")?;
- let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
+ let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11688,7 +11668,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
completion_source,
script_ready_source,
events_after_script_ready,
- interactive_source,
+ interactive_state,
host_load_source,
events_after_host_load,
pending_after_host_load,
@@ -11723,8 +11703,8 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
"DocumentScriptReady should not dispatch child window load inline"
);
assert_eq!(
- interactive_source,
- Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
+ interactive_state,
+ "interactive",
"child document should become interactive before its window load"
);
assert_eq!(
diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs b/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs
index 5e1a64abe2..029c393791 100644
--- a/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs
@@ -213,7 +213,7 @@ JSON.stringify({
PageDomManipulationTestFamily::ViewTransitionUpdate,
)
.expect("view-transition update callback should queue one platform task");
- let body = page_vm.apply_selected_page_dom_manipulation_turn(task)?;
+ let body = page_vm.apply_selected_page_dom_manipulation_turn(task).await?;
let PageDomManipulationTurnAction::ViewTransitionUpdate(action) = body.action else {
unreachable!("the selected view-transition task must retain its typed action")
};
diff --git a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs
index 8eeb9b6531..df135d8458 100644
--- a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs
+++ b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs
@@ -1246,7 +1246,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
start_line,
start_column,
import_map,
- );
+ )?;
Ok(ScriptHandoffOutcome::NoNavigation)
}
ParserScriptHandoff::NoExecution {
diff --git a/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs b/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs
index b64d1459be..aa52bc43c2 100644
--- a/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs
+++ b/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs
@@ -753,6 +753,16 @@ fn streaming_stylesheet_and_json_ld_reach_tail_and_post_parse_boundary() {
DocumentLifecycleTurnReadiness::Runnable { .. },
..
} => {}
+ DocumentLifecycleTurnOutcome {
+ readiness: DocumentLifecycleTurnReadiness::Blocked { .. },
+ ..
+ } if pending.as_ref().is_some_and(|pending| pending.awaiting_dom_task.is_some()) => {
+ let loader = page_vm.request_client.clone();
+ assert!(page_vm.run_exact_selected_page_task_for_test(
+ crate::runtime::page_vm::PageSelectedTaskTestSelector::AnyDomManipulation,
+ &loader,
+ ).await?, "admitted DCL must advance through the real DOM source head");
+ }
outcome => panic!(
"streaming fixture should reach DCL without parking: {outcome:?}"
),
diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs
index 52970e057d..6670eb9c44 100644
--- a/moli-renderer-v8/src/script_vm.rs
+++ b/moli-renderer-v8/src/script_vm.rs
@@ -5338,9 +5338,23 @@ impl ScriptVm {
) -> bool {
use crate::{
frame_owner_model::ChildFrameSemanticTurnKind,
- page_task_queue::RendererPageChildFrameTaskTarget,
+ page_task_queue::{
+ RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner,
+ RendererPageReadyDescriptor,
+ },
};
+ if expected == ChildFrameSemanticTurnKind::DocumentLifecycle {
+ return self._page_task_residence_for_executor_test.as_ref().expect("semantic fixture must retain its sources").task_sources().has_scheduler_task_for_executor_test(|descriptor| {
+ matches!(descriptor,
+ RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. }
+ ) || matches!(descriptor,
+ RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
+ if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
+ )
+ });
+ }
+
let Some(target) = self
._page_task_residence_for_executor_test
.as_ref()
@@ -5393,7 +5407,14 @@ impl ScriptVm {
{
return Some(ChildFrameSemanticTurnKind::NavigationCommit);
}
- if self
+ if matches!(
+ self._page_task_residence_for_executor_test
+ .as_ref()
+ .expect("child fixture must retain its sources")
+ .task_sources()
+ .next_child_semantic_task_target(),
+ Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
+ ) && self
.run_child_document_lifecycle_body_for_test()
.expect("typed child lifecycle executor turn should succeed")
.is_some()
diff --git a/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs b/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs
index 4fbac866e5..edec0178c1 100644
--- a/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs
+++ b/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs
@@ -27,15 +27,29 @@ impl<'vm> ChildDocumentLifecycleOwner<'vm> {
pub(super) fn notify_parser_stop_action(
&mut self,
action: FrameDocumentInteractiveLifecycleAction,
- ) -> bool {
- self.vm
- ._context_host
- .borrow_mut()
- .queue_child_document_interactive_lifecycle_action(action)
+ ) -> Result {
+ let context_host = self.vm._context_host.clone();
+ self.vm.with_default_context_scope(move |scope, _| {
+ Ok(context_host
+ .borrow_mut()
+ .finish_child_document_parser_stop(scope, action))
+ })
}
}
impl ScriptVm {
+ pub(crate) fn child_document_lifecycle_waits_for_realm(
+ &self,
+ target: RendererPageChildDocumentLifecycleTarget,
+ ) -> bool {
+ let host = self._context_host.borrow();
+ host.child_document_lifecycle_action_is_current(target.action())
+ && host.has_child_frame_realm_materialization_request(
+ target.child_handle(),
+ target.document_owner(),
+ )
+ }
+
pub(crate) fn current_child_document_lifecycle_target(
&self,
expected: RendererPageChildDocumentLifecycleTarget,
@@ -102,9 +116,9 @@ impl ScriptVm {
let Some(task) = source.take_scheduler_task_for_executor_test(|descriptor| {
matches!(
descriptor,
- crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask {
- owner,
- ..
+ crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
+ | crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation {
+ owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
} if matches!(
owner.target(),
RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -113,8 +127,14 @@ impl ScriptVm {
}) else {
return Ok(None);
};
- let crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task) = task else {
- unreachable!("child-frame descriptor must dequeue its own family source")
+ let task = match task {
+ crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task)
+ | crate::page_task_queue::RendererPageSchedulerTask::DomManipulation(
+ crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle(
+ task,
+ ),
+ ) => task,
+ _ => unreachable!("child lifecycle selector must dequeue its admitted task"),
};
let RendererPageChildFrameTaskTarget::DocumentLifecycle(target) = task.owner().target()
else {
diff --git a/moli-renderer-v8/src/script_vm/child_navigation_commit.rs b/moli-renderer-v8/src/script_vm/child_navigation_commit.rs
index 624e317709..df3dc06da4 100644
--- a/moli-renderer-v8/src/script_vm/child_navigation_commit.rs
+++ b/moli-renderer-v8/src/script_vm/child_navigation_commit.rs
@@ -60,7 +60,7 @@ impl ScriptVm {
self.apply_child_document_owner_transition(transition);
}
if let Some(action) = parser_stop_action {
- ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action);
+ ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action)?;
}
let mut ready_owner = ChildDocumentScriptSchedulerOwner::new(self);
for work in ready_work {
diff --git a/moli-renderer-v8/src/script_vm/native_module.rs b/moli-renderer-v8/src/script_vm/native_module.rs
index 741ef9fcb7..da48abc6b3 100644
--- a/moli-renderer-v8/src/script_vm/native_module.rs
+++ b/moli-renderer-v8/src/script_vm/native_module.rs
@@ -5317,7 +5317,7 @@ mod tests {
vm: &mut ScriptVm,
label: &str,
) {
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
assert!(
vm.run_child_frame_task_source_once_for_test(
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -5355,7 +5355,6 @@ mod tests {
label: &str,
) {
for expected in [
- ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad,
diff --git a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs
index 6a350a9527..6a7d3d125b 100644
--- a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs
+++ b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs
@@ -110,8 +110,8 @@ impl ScriptVm {
/// The main-document lifecycle coordinator owns the ordinary task-end
/// checkpoint. Keeping this primitive body-only prevents DCL and
/// `readystatechange` helpers from silently becoming a second completion
- /// authority. Parser-finish DCL temporarily uses the explicit compatibility
- /// wrapper below until P5-A3 moves that direct successor as one unit.
+ /// authority. Parser completion admits DCL to the DOM task source after
+ /// finishing its own task-end checkpoint.
pub(super) fn dispatch_document_lifecycle_event_body(
&mut self,
event_type: &str,
diff --git a/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs b/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs
index a2ce28cec3..f7fdd523cd 100644
--- a/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs
+++ b/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs
@@ -908,6 +908,32 @@ impl ScriptVm {
Some(ParserFinishDomContentLoadedWork::new(owner, *work))
}
+ pub(crate) fn main_document_window_load_task_is_ready(
+ &mut self,
+ owner: FrameDocumentTaskOwner,
+ ) -> Option {
+ let has_runtime_delay = self.has_post_domcontentloaded_load_delaying_runtime_work();
+ self._context_host
+ .borrow()
+ .current_main_document_complete_transition_is_ready(owner)
+ .map(|ready| ready && !has_runtime_delay)
+ }
+
+ pub(crate) fn queue_main_document_lifecycle_dom_task(
+ &self,
+ body: super::MainDocumentLifecycleBody,
+ completion: Option<
+ tokio::sync::oneshot::Sender<
+ crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion,
+ >,
+ >,
+ ) -> anyhow::Result<()> {
+ self._context_host
+ .borrow()
+ .page_main_document_lifecycle_sender()
+ .send(body, completion)
+ }
+
/// Consume the completed parser task and claim only its exact, already
/// queued DOMContentLoaded successor.
///
@@ -1840,21 +1866,6 @@ impl ScriptVm {
reached_boundary,
invalidation_policy,
} = PostParseProcessingAction::from_document_processing_action(action);
- if work.is_domcontentloaded_task()
- && self
- .document_runtime
- .has_ready_timeout_queued_by_classic_defer_script()
- {
- // The lifecycle plan pre-seeds this boundary marker so owner work
- // stays ordered. It has now reached the front after defer-like and
- // parser-owned pre-DCL work, which is the point at which HTML queues
- // the actual DOMContentLoaded global task. Give only ready timers
- // scheduled by classic defer scripts their intervening task turn.
- page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![work]);
- return PostParseDriverStep::Ready(Box::new(
- ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded,
- ));
- }
if !(work.is_domcontentloaded_task() || work.is_window_load_task()) {
return PostParseDriverStep::Ready(Box::new(ReadyPostParseAction::Processing(
Box::new(PostParseProcessingAction {
@@ -1977,11 +1988,6 @@ impl ScriptVm {
ReadyPostParseAction::Processing(action) => {
return Ok(PostParseProcessingStep::Action(action));
}
- ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
- return Ok(
- PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded,
- );
- }
},
PostParseDriverStep::NeedsContinuation => {
return Ok(PostParseProcessingStep::NeedsContinuation);
@@ -2020,11 +2026,6 @@ impl ScriptVm {
execution.into_page_owned_task(),
)));
}
- PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
- return Ok(
- PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded,
- );
- }
PostParseProcessingStep::NeedsContinuation => {
return Ok(PostParseLifecycleAdvance::NeedsContinuation);
}
diff --git a/moli-renderer-v8/src/script_vm/runtime_work.rs b/moli-renderer-v8/src/script_vm/runtime_work.rs
index 34acd51688..a165bfebab 100644
--- a/moli-renderer-v8/src/script_vm/runtime_work.rs
+++ b/moli-renderer-v8/src/script_vm/runtime_work.rs
@@ -28,7 +28,6 @@ pub(crate) enum PostParseDrainResult {
pub(crate) enum PostParseLifecycleAdvance {
PageOwnedTask(Box),
- TimerQueuedByClassicDeferBeforeDomContentLoaded,
NeedsContinuation,
AwaitProgress,
Complete(PostParseLifecycleCompletionAction),
@@ -193,20 +192,20 @@ pub(crate) struct PostParsePageOwnedTask {
pub(super) completion: PostParseTaskCompletion,
}
-/// The exact DOMContentLoaded action claimed as the direct successor of a
+/// The exact DOMContentLoaded action claimed for DOM task admission after a
/// drained main-parser queue.
///
/// This value is deliberately short-lived: the existing post-parse queue and
/// lifecycle driver remain the only durable authority. The wrapper merely
/// prevents an already-claimed DCL action from falling back to a generic
-/// page-owned task before the parser continuation commits it.
+/// page-owned task before the parser continuation admits it to the DOM source.
pub(crate) struct ParserFinishDomContentLoadedTask {
owner: FrameDocumentTaskOwner,
task: PostParsePageOwnedTask,
}
-/// The exact parse-time DOMContentLoaded work claimed as the direct successor
-/// of a drained main-parser queue.
+/// The exact parse-time DOMContentLoaded work claimed for DOM task admission
+/// after a drained main-parser queue.
///
/// Unlike [`ParserFinishDomContentLoadedTask`], phase one has no installed
/// post-parse driver task token to complete. The lifecycle authority still
@@ -244,7 +243,7 @@ impl ParserFinishDomContentLoadedTask {
self.task.take_work_for_execution()
}
- pub(crate) fn into_completed_task(self) -> PostParsePageOwnedTask {
+ pub(crate) fn into_pending_task(self) -> PostParsePageOwnedTask {
self.task
}
}
@@ -323,7 +322,6 @@ impl PostParseProcessingAction {
pub(crate) enum ReadyPostParseAction {
Processing(Box),
- TimerQueuedByClassicDeferBeforeDomContentLoaded,
}
pub(crate) enum PostParseRuntimeDriverStep {
@@ -340,7 +338,6 @@ pub(crate) enum PostParseDriverStep {
pub(crate) enum PostParseProcessingStep {
Action(Box),
- TimerQueuedByClassicDeferBeforeDomContentLoaded,
NeedsContinuation,
AwaitProgress,
Idle,
diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs
index 5ca2fb9a1e..99252e71ba 100644
--- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs
+++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs
@@ -6068,7 +6068,7 @@ impl ScriptVm {
}
};
self.apply_pending_child_document_owner_retirements();
- let (application, body_activity) = application;
+ let (application, mut body_activity) = application;
let Some(application) = application else {
return Ok(CurrentChildDocumentLoadApplication::Applied { body_activity });
};
@@ -6076,9 +6076,13 @@ impl ScriptVm {
if let Some(transition) = owner_transition {
self.apply_child_document_owner_transition(transition);
}
- if let Some(action) = parser_stop_action {
- super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self)
- .notify_parser_stop_action(action);
+ if let Some(action) = parser_stop_action
+ && super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self)
+ .notify_parser_stop_action(action)?
+ == crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::EventDispatched
+ {
+ body_activity =
+ crate::native_bridge::ChildDocumentLoadBodyActivity::PageCodeOrEventDispatch;
}
if let Some(work) = work {
super::child_document_script_scheduler::ChildDocumentScriptSchedulerOwner::new(self)
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs
index 5bd3a9612b..55d709ba15 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs
@@ -11654,7 +11654,7 @@ async fn iframe_javascript_url_string_completion_replaces_child_document() {
"javascript URL should execute on DocumentScriptReady",
)
.await;
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13644,7 +13644,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output()
"old Window load listener must retain a current callback relevant realm"
);
}
- for context in ["old interactive", "old DOMContentLoaded", "old complete"] {
+ for context in ["old DOMContentLoaded", "old complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13696,11 +13696,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output()
"replacement child load handlers should install",
)
.await;
- for context in [
- "replacement interactive",
- "replacement DOMContentLoaded",
- "replacement complete",
- ] {
+ for context in ["replacement DOMContentLoaded", "replacement complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13855,7 +13851,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output()
"old Window lifecycle listeners must retain current callback relevant realms"
);
}
- for context in ["old interactive", "old DOMContentLoaded", "old complete"] {
+ for context in ["old DOMContentLoaded", "old complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13906,11 +13902,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output()
"replacement pageshow handlers should install",
)
.await;
- for context in [
- "replacement interactive",
- "replacement DOMContentLoaded",
- "replacement complete",
- ] {
+ for context in ["replacement DOMContentLoaded", "replacement complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -14088,13 +14080,12 @@ async fn child_static_media_delays_complete_and_iframe_load_until_loadeddata() {
"child media parser script should run before lifecycle",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child media document should enter interactive and accept its media token",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -14212,13 +14203,12 @@ async fn child_media_network_failure_releases_lifecycle_before_later_host_load()
"child media parser script should install listeners",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child failed media document should become interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
let request = request_rx.await.expect("child media request should arrive");
assert!(
request
@@ -14339,13 +14329,12 @@ async fn child_image_network_failure_releases_lifecycle_before_later_host_load()
"child image parser script should install listeners",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child image document should become interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
let request = request_rx.await.expect("child image request should arrive");
assert!(
request
@@ -14518,13 +14507,12 @@ async fn child_dynamic_media_accepted_during_dcl_delays_later_load_turns() {
"dynamic media parser script should install its DCL producer",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "dynamic media document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -14617,13 +14605,12 @@ async fn child_static_text_track_starts_at_interactive_without_own_load_token()
"child track parser script should install its listener",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "interactive should start the static child track",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
assert!(
vm.run_one_dom_manipulation_task_executor_turn(
PageDomManipulationTestFamily::TextTrackDefaultMode,
@@ -14746,12 +14733,12 @@ async fn child_document_replacement_retires_media_sequence_and_delay() {
"first media document parser script should run",
)
.await;
- expect_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "first media document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -14796,7 +14783,6 @@ async fn child_document_replacement_retires_media_sequence_and_delay() {
)
.await;
for (source, transition) in [
- (ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"),
(
ChildFrameSemanticTurnKind::DocumentLifecycle,
"DOMContentLoaded",
@@ -14868,13 +14854,12 @@ async fn moving_pending_child_media_restarts_under_the_new_document_owner() {
"moving media parser script should run",
)
.await;
- expect_page_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "moving media document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -15014,12 +14999,12 @@ async fn child_image_event_delay_blocks_complete_and_host_load_until_terminal()
"child image parser script should run before lifecycle",
)
.await;
- expect_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child image document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15117,12 +15102,12 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() {
"first image document parser script should run",
)
.await;
- expect_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "first image document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15166,8 +15151,19 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() {
"replacement parser script should run",
)
.await;
+ assert!(
+ vm.apply_next_image_load_event_body_for_test()
+ .expect("stale image DOM task"),
+ "the earlier image task must retire at the shared DOM FIFO head"
+ );
+ assert_eq!(
+ vm.eval("__childImageReplacementEvents.join('|')")
+ .expect("retired image trace"),
+ "first-dcl",
+ "the stale image task must not dispatch into the replacement"
+ );
+
for (source, transition) in [
- (ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"),
(
ChildFrameSemanticTurnKind::DocumentLifecycle,
"DOMContentLoaded",
@@ -15233,12 +15229,12 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request(
"moving image parser script should run",
)
.await;
- expect_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "moving image document should enter interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15266,6 +15262,17 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request(
"#,
)
.expect("pending child image should move to the parent document");
+ assert!(
+ vm.apply_next_image_load_event_body_for_test()
+ .expect("old image DOM task"),
+ "the old image task must retire before the later complete task at the shared FIFO head"
+ );
+ assert_eq!(
+ vm.eval("__movedChildImageEvents.join('|')")
+ .expect("old image trace"),
+ "child-dcl",
+ "retiring the old image task must leave the rebound request pending"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -16035,12 +16042,12 @@ async fn child_document_close_without_defer_queues_replacement_domcontentloaded(
"child parser script should install the original DCL handler",
)
.await;
- expect_child_frame_task_source_after_realm_prerequisite(
- &mut vm,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "original child document should become interactive",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -16150,10 +16157,16 @@ async fn child_script_document_open_after_location_navigation_is_noop() {
.await,
"the blob URL assigned by the child script must commit on a later navigation turn"
);
+ expect_one_child_frame_task_source(
+ &mut vm,
+ ChildFrameSemanticTurnKind::RealmMaterialization,
+ "blob replacement must materialize its exact realm",
+ )
+ .await;
expect_one_child_frame_task_source(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
- "the queued srcdoc lifecycle task must stale-discard before blob lifecycle work",
+ "the older srcdoc DCL must stale-discard at the shared DOM head",
)
.await;
for transition in ["interactive", "DOMContentLoaded", "complete"] {
diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs
index 49b2ba5e19..bb4fe3d049 100644
--- a/moli-renderer-v8/src/script_vm/tests/mod.rs
+++ b/moli-renderer-v8/src/script_vm/tests/mod.rs
@@ -4752,11 +4752,16 @@ async fn run_child_document_lifecycle_and_host_load_for_test(vm: &mut ScriptVm,
// Only consecutive materialization tasks at the stable family head
// belong here. Never jump over an earlier DocumentScriptReady task.
}
- assert!(
- vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
+ if matches!(vm._page_task_residence_for_executor_test.as_ref().expect("child fixture sources").task_sources().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)))
+ {
+ assert!(
+ vm.run_child_frame_task_source_once_for_test(
+ ChildFrameSemanticTurnKind::DocumentLifecycle
+ )
.await,
- "{message}: DocumentLifecycle should make the installed document interactive"
- );
+ "{message}: a child without a parser realm must apply its queued interactive transition"
+ );
+ }
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
.await,
@@ -7147,7 +7152,7 @@ async fn child_srcdoc_inline_classic_script_runs_as_frame_script_job() {
"child:true|current:inline-classic",
"DocumentScriptReady should execute the inline classic script without firing iframe load"
);
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
assert!(
vm.run_child_frame_task_source_once_for_test(
ChildFrameSemanticTurnKind::DocumentLifecycle
@@ -7264,7 +7269,7 @@ async fn child_inline_classic_script_moved_from_original_document_is_skipped() {
"after:after-stale-inline",
"parser continuation should run the next inline classic script without firing iframe load"
);
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -7593,7 +7598,7 @@ async fn child_external_classic_script_load_executes_as_frame_script_job() {
"external:true|external-current:external-classic|external-write:true|script-load|inline-current:after-external-classic|inline:73",
"ignored document.open() must leave the parser owner alive for the following inline script"
);
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
&mut vm,
&loader,
@@ -8935,10 +8940,11 @@ async fn child_inline_parser_module_executes_from_registered_pending_script() {
"parser should continue past the deferred inline module after graph start",
)
.await;
- assert!(
- vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
- .await,
- "parser EOF should make the child document interactive before defer execution"
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
);
assert!(
vm.run_child_frame_task_source_once_for_test(
@@ -9048,13 +9054,12 @@ async fn child_external_parser_module_executes_from_document_ready_lane() {
"parser should continue past module-defer script from DocumentScriptReady after fetch start",
)
.await;
- run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
- &mut vm,
- &loader,
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- "child parser module should enter interactive before module-defer execution",
- )
- .await;
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
+ );
assert!(
!vm.run_one_child_frame_task_executor_turn(ChildFrameSemanticTurnKind::HostLoad, &loader)
.await
@@ -9194,7 +9199,7 @@ async fn child_dynamic_import_root_fetch_uses_child_import_map_and_initiator_url
.expect("child dynamic import owner ready flag should evaluate"),
"true"
);
- for transition in ["interactive", "DOMContentLoaded", "complete"] {
+ for transition in ["DOMContentLoaded", "complete"] {
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -9451,14 +9456,11 @@ async fn child_external_classic_source_error_dispatches_before_later_inline() {
"script-error|after-inline",
"parser continuation should execute the following inline script without firing iframe load"
);
- assert!(
- vm.run_one_child_frame_task_executor_turn(
- ChildFrameSemanticTurnKind::DocumentLifecycle,
- &loader,
- )
- .await
- .expect("child interactive task should use the selected-task dispatcher"),
- "parser EOF should dispatch interactive before HostLoad"
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
);
assert!(
vm.run_one_child_frame_task_executor_turn(
@@ -9596,10 +9598,11 @@ async fn child_inline_classic_throw_reports_to_child_window_and_continues() {
"child-error:Uncaught Error: child-boom:child-boom:true|after-inline:true",
"parser continuation should run the following inline script without firing iframe load"
);
- assert!(
- vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
- .await,
- "parser EOF should dispatch interactive before HostLoad"
+ assert_eq!(
+ vm.eval("document.querySelector('iframe').contentDocument.readyState")
+ .expect("child readiness"),
+ "interactive",
+ "parser EOF must apply interactive synchronously"
);
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
diff --git a/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs b/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs
index 6d5d7a3d37..564b8fa312 100644
--- a/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs
+++ b/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs
@@ -128,7 +128,8 @@ addEventListener("error", () => {
base_url: document_url.clone(),
initiator_url: document_url,
},
- );
+ )
+ .expect("parser import-map handoff should be admitted");
assert_eq!(
vm.eval_without_microtask_checkpoint_for_test("__importMapFailureOrder.join('|')")
@@ -182,7 +183,8 @@ fn parser_import_map_registration_obeys_inline_script_csp() {
base_url: document_url.clone(),
initiator_url: document_url,
},
- );
+ )
+ .expect("parser import-map handoff should be admitted");
}
let base_url = Url::parse("https://script-terminal.test/import-map.html").unwrap();