diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index 5e4edf5e1c..f173d1596f 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -1819,6 +1819,24 @@ impl DocumentRuntime { options: RuntimeMutationOptions, prepublished_removals: Vec, ) -> bool { + let result = self.apply_runtime_mutation_effects_before_runtime_followups( + scope, + host_ptr, + effects, + options, + prepublished_removals, + ); + finish_runtime_mutation_effects(self, scope, host_ptr, result) + } + + pub(super) fn apply_runtime_mutation_effects_before_runtime_followups( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + effects: DomMutationEffects, + options: RuntimeMutationOptions, + prepublished_removals: Vec, + ) -> RuntimeMutationApplyResult { let mut result = apply_runtime_mutation_effects_to_dom_host( &mut self.mutations, &self.document, @@ -1834,7 +1852,7 @@ impl DocumentRuntime { &mut result.devtools_dom_mutations, prepublished_removals, ); - finish_runtime_mutation_effects(self, scope, host_ptr, result) + result } } @@ -1851,6 +1869,12 @@ pub(super) struct RuntimeMutationApplyResult { font_face_use_roots: Vec, } +impl RuntimeMutationApplyResult { + pub(super) fn did_change(&self) -> bool { + self.changed + } +} + #[derive(Clone, Debug)] pub(super) struct InlineStyleAttributeCspMutation { target: DomHandle, @@ -1913,22 +1937,7 @@ pub(super) fn finish_runtime_mutation_effects( } for candidate in runtime_script_start_candidates { - let (node, host_script_handle) = candidate.into_parts(); - let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else { - continue; - }; - match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) { - Ok(Some(committed)) => { - execute_committed_inline_classic_script(runtime, scope, host_ptr, committed); - } - Ok(None) => {} - Err(message) => { - if let Some(message) = v8::String::new(scope, &message) { - let exception = v8::Exception::error(scope, message); - scope.throw_exception(exception); - } - } - } + finish_runtime_script_start_candidate(runtime, scope, host_ptr, candidate); } for popover in removed_open_popovers { @@ -2011,6 +2020,30 @@ pub(super) fn finish_runtime_mutation_effects( changed } +pub(super) fn finish_runtime_script_start_candidate( + runtime: &mut DocumentRuntime, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + candidate: crate::mutation_coordinator::RuntimeScriptStartCandidate, +) { + let (node, host_script_handle) = candidate.into_parts(); + let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else { + return; + }; + match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) { + Ok(Some(committed)) => { + execute_committed_inline_classic_script(runtime, scope, host_ptr, committed); + } + Ok(None) => {} + Err(message) => { + if let Some(message) = v8::String::new(scope, &message) { + let exception = v8::Exception::error(scope, message); + scope.throw_exception(exception); + } + } + } +} + fn execute_committed_inline_classic_script( runtime: &mut DocumentRuntime, scope: &mut v8::PinScope<'_, '_>, diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/selectedcontent.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/selectedcontent.rs index b07237ddde..7df83555eb 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/selectedcontent.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/selectedcontent.rs @@ -1,11 +1,234 @@ +use super::{ + finish_runtime_mutation_effects, finish_runtime_script_start_candidate, + tree::{TreeInsertionPlan, TreeInsertionPostConnectionStep, TreeReactionDispatchPolicy}, +}; use crate::{ custom_elements, document_runtime::{DocumentRuntime, DomHandle}, - dom::native::Node, + dom::native::{DomMutationEffects, Node}, + mutation_coordinator::{ + ConnectedScriptMutationPolicy, RuntimeMutationOptions, ScriptStartRequest, + }, native_bridge::JsContextHost, + util::context_host_ptr_from_global_bridge, }; +fn selectedcontent_update_microtask_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments<'_>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let Ok(value) = v8::Local::::try_from(args.data()) else { + return; + }; + let (index, lossless) = value.u64_value(); + if !lossless { + return; + } + let select = DomHandle::new(index as usize); + if !unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select) { + return; + } + let runtime: &mut DocumentRuntime = unsafe { &mut *host_ptr }; + let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, host_ptr, select); +} + impl DocumentRuntime { + pub(super) fn apply_tree_insertion_mutation_effects_with_post_connection_steps( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + insertion_plan: &TreeInsertionPlan<'_>, + effects: DomMutationEffects, + options: RuntimeMutationOptions, + reaction_policy: TreeReactionDispatchPolicy, + run_post_connection_steps: bool, + prepublished_removals: Vec, + ) -> bool { + if !run_post_connection_steps + || !insertion_plan.requires_interleaved_post_connection_steps() + { + let changed = self.apply_runtime_mutation_effects_with_prepublished_removals( + scope, + host_ptr, + effects, + options, + prepublished_removals, + ); + if changed && run_post_connection_steps { + self.queue_selectedcontent_updates_after_selected_option_owner_change( + scope, + host_ptr, + &insertion_plan.selected_option_owners_before_insert, + ); + } + return changed; + } + + let run_script_steps = matches!( + options.connected_script_policy, + ConnectedScriptMutationPolicy::PrepareAndStart + ); + let script_start_requests = if run_script_steps { + self.mutations + .plan_connected_script_start_requests(&mut self.dom_host, &effects) + } else { + Vec::new() + }; + let result = self.apply_runtime_mutation_effects_before_runtime_followups( + scope, + host_ptr, + effects, + options.with_connected_script_policy(ConnectedScriptMutationPolicy::DeferToOwner), + prepublished_removals, + ); + if result.did_change() { + self.queue_selectedcontent_updates_after_selected_option_owner_change( + scope, + host_ptr, + &insertion_plan.selected_option_owners_before_insert, + ); + self.run_tree_insertion_post_connection_steps( + scope, + host_ptr, + insertion_plan, + script_start_requests, + reaction_policy, + ); + } + finish_runtime_mutation_effects(self, scope, host_ptr, result) + } + + pub(super) fn run_parser_tree_insertion_post_connection_steps( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + insertion_plan: &TreeInsertionPlan<'_>, + ) { + if insertion_plan.requires_interleaved_post_connection_steps() { + self.run_tree_insertion_post_connection_steps( + scope, + host_ptr, + insertion_plan, + Vec::new(), + TreeReactionDispatchPolicy::AppendToCurrentQueue, + ); + } + } + + fn run_tree_insertion_post_connection_steps( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + insertion_plan: &TreeInsertionPlan<'_>, + script_start_requests: Vec, + reaction_policy: TreeReactionDispatchPolicy, + ) { + match reaction_policy { + TreeReactionDispatchPolicy::DispatchNow => { + custom_elements::with_custom_element_reaction_scope(scope, host_ptr, |scope| { + self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue( + scope, + host_ptr, + insertion_plan, + script_start_requests, + ); + }); + } + TreeReactionDispatchPolicy::AppendToCurrentQueue => { + self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue( + scope, + host_ptr, + insertion_plan, + script_start_requests, + ); + } + } + } + + fn run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + insertion_plan: &TreeInsertionPlan<'_>, + script_start_requests: Vec, + ) { + let script_step_handles = insertion_plan + .post_connection_steps + .iter() + .filter_map(|step| match step { + TreeInsertionPostConnectionStep::Script(handle) => Some(*handle), + _ => None, + }) + .collect::>(); + let (extra_script_requests, mut subtree_script_requests): (Vec<_>, Vec<_>) = + script_start_requests + .into_iter() + .partition(|request| !script_step_handles.contains(&request.handle())); + + // A connected script parent can be prepared by a child insertion even + // though it is outside the inserted subtree. It precedes every + // descendant post-connection step in tree order. + for request in extra_script_requests { + self.run_script_start_request(scope, host_ptr, request); + } + + for step in insertion_plan.post_connection_steps.iter().copied() { + let handle = step.handle(); + if !self.dom_host.is_connected(handle) { + continue; + } + match step { + TreeInsertionPostConnectionStep::Script(script) => { + let Some(index) = subtree_script_requests + .iter() + .position(|request| request.handle() == script) + else { + continue; + }; + let request = subtree_script_requests.remove(index); + self.run_script_start_request(scope, host_ptr, request); + } + TreeInsertionPostConnectionStep::SelectedContent(selectedcontent) => { + let _ = self.sync_selectedcontent_after_post_connection( + scope, + host_ptr, + selectedcontent, + ); + } + TreeInsertionPostConnectionStep::SelectedOption(option) => { + let _ = self.sync_selectedcontents_for_selected_option(scope, host_ptr, option); + } + } + } + } + + fn run_script_start_request( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + request: ScriptStartRequest, + ) { + if request.clears_force_async() { + let _ = self + .dom_host + .set_script_force_async(request.handle(), false); + } + let candidate = self.mutations.collect_connected_script_start_candidate( + scope, + host_ptr, + &mut self.dom_host, + request.handle(), + &self.document, + ); + if let Some(candidate) = candidate { + finish_runtime_script_start_candidate(self, scope, host_ptr, candidate); + } + } + pub(crate) fn sync_selectedcontents_for_select_in_reaction_scope( &mut self, scope: &mut v8::PinScope<'_, '_>, @@ -24,6 +247,15 @@ impl DocumentRuntime { scope: &mut v8::PinScope<'_, '_>, host_ptr: *mut JsContextHost, option: DomHandle, + ) -> bool { + self.sync_selectedcontents_for_selected_option(scope, host_ptr, option) + } + + fn sync_selectedcontents_for_selected_option( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + option: DomHandle, ) -> bool { let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else { return false; @@ -42,6 +274,125 @@ impl DocumentRuntime { ) } + fn sync_selectedcontent_after_post_connection( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + selectedcontent: DomHandle, + ) -> bool { + let Some(select) = self + .dom_host + .selectedcontent_nearest_ancestor_select(selectedcontent) + else { + return false; + }; + if self + .dom_host + .node(select) + .and_then(Node::as_element) + .is_some_and(|element| element.has_attribute("multiple")) + { + return false; + } + let selected_option = self + .dom_host + .select_selected_option_elements(select) + .first() + .copied(); + self.clone_selected_option_contents_into_selectedcontent( + scope, + host_ptr, + selectedcontent, + selected_option, + ) + } + + pub(super) fn queue_selectedcontent_update_microtask( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + select: DomHandle, + ) { + if !unsafe { &mut *host_ptr }.mark_pending_selectedcontent_update(select) { + return; + } + let data = v8::BigInt::new_from_u64(scope, select.index() as u64).into(); + let Some(callback) = v8::Function::builder(selectedcontent_update_microtask_callback) + .data(data) + .build(scope) + else { + let _ = unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select); + return; + }; + scope.enqueue_microtask(callback); + } + + pub(super) fn queue_selectedcontent_updates_after_tree_removal( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + selected_option_owners_before_remove: &[(DomHandle, DomHandle)], + ) { + self.queue_selectedcontent_updates_after_selected_option_owner_change( + scope, + host_ptr, + selected_option_owners_before_remove, + ); + } + + fn queue_selectedcontent_updates_after_selected_option_owner_change( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + selected_option_owners_before_change: &[(DomHandle, DomHandle)], + ) { + for &(option, previous_select) in selected_option_owners_before_change { + if self.dom_host.option_nearest_ancestor_select(option) != Some(previous_select) { + self.queue_selectedcontent_update_microtask(scope, host_ptr, previous_select); + } + } + } + + pub(super) fn selected_option_owners_in_subtrees( + &self, + roots: &[DomHandle], + ) -> Vec<(DomHandle, DomHandle)> { + let options = roots + .iter() + .flat_map(|root| { + self.dom_host + .collect_matching_elements(*root, true, |handle| { + self.dom_host.is_html_element_named(handle, "option") + }) + }) + .collect::>(); + let mut selected_option_by_select = Vec::new(); + let mut owners = Vec::new(); + for option in options { + let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else { + continue; + }; + let selected_option = if let Some((_, selected_option)) = selected_option_by_select + .iter() + .find(|(cached_select, _)| *cached_select == select) + { + *selected_option + } else { + let selected_option = self + .dom_host + .select_selected_option_elements(select) + .first() + .copied(); + selected_option_by_select.push((select, selected_option)); + selected_option + }; + if selected_option == Some(option) { + owners.push((option, select)); + } + } + owners + } + pub(crate) fn sync_selectedcontents_for_select_appending_to_current_reaction_queue( &mut self, scope: &mut v8::PinScope<'_, '_>, diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion.rs index b3ba74d646..f8d862f02d 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion.rs @@ -158,11 +158,14 @@ impl DocumentRuntime { .map(|started| started.elapsed().as_micros()) .unwrap_or_default(); let mutation_started = cpu_profile_enabled.then(std::time::Instant::now); - let changed = self.apply_runtime_mutation_effects_with_prepublished_removals( + let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps( scope, host_ptr, + &insertion_plan, effects, RuntimeMutationOptions::js_dom_api(), + source_profile.reaction_policy, + true, prepublished_removals, ); let mutation_us = mutation_started @@ -410,11 +413,14 @@ impl DocumentRuntime { if effects.did_change() { self.apply_node_iterator_pre_remove_plans(host_ptr, &insertion_plan.node_iterator_plan); } - let changed = self.apply_runtime_mutation_effects_with_prepublished_removals( + let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps( scope, host_ptr, + &insertion_plan, effects, mutation_options, + source_profile.reaction_policy, + !dispatch_atomic_move_callbacks, prepublished_removals, ); if changed { diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs index 2389c4dc2f..dc001630db 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/insertion_plan.rs @@ -9,7 +9,24 @@ use crate::{ native_bridge::JsContextHost, }; -pub(super) struct TreeInsertionPlan<'a> { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(in crate::document_runtime::mutation_commands) enum TreeInsertionPostConnectionStep { + Script(DomHandle), + SelectedContent(DomHandle), + SelectedOption(DomHandle), +} + +impl TreeInsertionPostConnectionStep { + pub(in crate::document_runtime::mutation_commands) fn handle(self) -> DomHandle { + match self { + Self::Script(handle) | Self::SelectedContent(handle) | Self::SelectedOption(handle) => { + handle + } + } + } +} + +pub(in crate::document_runtime::mutation_commands) struct TreeInsertionPlan<'a> { pub(super) parent: DomHandle, pub(super) insertion_roots: &'a [DomHandle], pub(super) inserting_fragment_children: bool, @@ -23,6 +40,10 @@ pub(super) struct TreeInsertionPlan<'a> { pub(super) image_relevant_mutation_plan: ImageRelevantMutationPlan, pub(super) media_relevant_mutation_plan: MediaRelevantMutationPlan, pub(super) option_selectedness_before_insert: Option>, + pub(in crate::document_runtime::mutation_commands) selected_option_owners_before_insert: + Vec<(DomHandle, DomHandle)>, + pub(in crate::document_runtime::mutation_commands) post_connection_steps: + Vec, } #[derive(Clone, Copy)] @@ -48,6 +69,13 @@ impl TreeInsertionPlan<'_> { pub(super) fn was_lifecycle_connected_before_insert(&self) -> bool { !self.lifecycle_connected_roots_before_insert.is_empty() } + pub(in crate::document_runtime::mutation_commands) fn requires_interleaved_post_connection_steps( + &self, + ) -> bool { + self.post_connection_steps + .iter() + .any(|step| !matches!(step, TreeInsertionPostConnectionStep::Script(_))) + } } impl TreeInsertionLiveRangeMode { @@ -130,6 +158,12 @@ impl DocumentRuntime { let option_selectedness_before_insert = (options.selectedness_policy.captures() && subtree_plan.may_have_options) .then(|| self.option_selectedness_before_insert(insertion_roots)); + let selected_option_owners_before_insert = if subtree_plan.may_have_options { + self.selected_option_owners_in_subtrees(insertion_roots) + } else { + Vec::new() + }; + let post_connection_steps = self.tree_insertion_post_connection_steps(insertion_roots); let adoption = self.tree_adoption_plan_before_insert(host_ptr, insertion_roots, parent); let lifecycle_connected_roots_before_insert: Vec = insertion_roots .iter() @@ -154,6 +188,36 @@ impl DocumentRuntime { image_relevant_mutation_plan, media_relevant_mutation_plan, option_selectedness_before_insert, + selected_option_owners_before_insert, + post_connection_steps, } } + + fn tree_insertion_post_connection_steps( + &self, + roots: &[DomHandle], + ) -> Vec { + let mut stack = roots.iter().rev().copied().collect::>(); + let mut steps = Vec::new(); + while let Some(handle) = stack.pop() { + let Some(node) = self.dom_host.node(handle) else { + continue; + }; + if node.is_script_element() { + steps.push(TreeInsertionPostConnectionStep::Script(handle)); + } else if let Some(element) = node.as_element() { + if element.is_html_element("selectedcontent") { + steps.push(TreeInsertionPostConnectionStep::SelectedContent(handle)); + } else if element.is_html_option() { + steps.push(TreeInsertionPostConnectionStep::SelectedOption(handle)); + } + } + + stack.extend(self.dom_host.child_handles_reversed(handle)); + if let Some(shadow_root) = self.dom_host.shadow_root_handle(handle) { + stack.push(shadow_root); + } + } + steps + } } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/mod.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/mod.rs index e817824716..b0567528df 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/mod.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/mod.rs @@ -19,6 +19,8 @@ mod resources; mod tree_order; pub(in crate::document_runtime) use adoption::TreeAdoptionPlan; +pub(super) use insertion_plan::{TreeInsertionPlan, TreeInsertionPostConnectionStep}; pub(in crate::document_runtime) use parser_post_step::ParserPostStepRuntimeWork; #[cfg(test)] pub(crate) use parser_post_step::ParserPostStepRuntimeWorkForTest; +pub(super) use policy::TreeReactionDispatchPolicy; diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs index 00105ae72f..57d5d15215 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/parser.rs @@ -155,6 +155,7 @@ impl DocumentRuntime { if !changed { return; } + self.run_parser_tree_insertion_post_connection_steps(scope, host_ptr, &insertion_plan); self.dispatch_tree_insertion_side_effects_after_change( scope, host_ptr, diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/policy.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/policy.rs index bb1460a9d5..c64c9a46e1 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/policy.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/policy.rs @@ -5,7 +5,7 @@ pub(super) enum TreeMutationSideEffectSource { } #[derive(Clone, Copy)] -pub(super) enum TreeReactionDispatchPolicy { +pub(in crate::document_runtime::mutation_commands) enum TreeReactionDispatchPolicy { DispatchNow, AppendToCurrentQueue, } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal.rs index a61135bcff..713674df01 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal.rs @@ -19,6 +19,7 @@ pub(super) struct TreeRemovalPlan { pub(super) live_range_previous_sibling: Option, pub(super) node_iterator_plan: Option, pub(super) registry_retargets: Vec, + pub(super) selected_option_owners_before_remove: Vec<(DomHandle, DomHandle)>, } impl DocumentRuntime { @@ -58,6 +59,8 @@ impl DocumentRuntime { }; let registry_retargets = custom_elements::registry_association_retargets_before_removal(host_ptr, root); + let selected_option_owners_before_remove = + self.selected_option_owners_in_subtrees(std::slice::from_ref(&root)); TreeRemovalPlan { parent, root, @@ -69,6 +72,7 @@ impl DocumentRuntime { live_range_previous_sibling, node_iterator_plan, registry_retargets, + selected_option_owners_before_remove, } } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal_followups.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal_followups.rs index b2017f6570..a731a1971c 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal_followups.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/removal_followups.rs @@ -21,6 +21,11 @@ impl DocumentRuntime { removal_plan: &TreeRemovalPlan, profile: TreeMutationSourceProfile, ) { + self.queue_selectedcontent_updates_after_tree_removal( + scope, + host_ptr, + &removal_plan.selected_option_owners_before_remove, + ); self.dispatch_tree_removal_pre_reaction_followups_after_change( scope, host_ptr, diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/replacement.rs b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/replacement.rs index 28231c11a5..2c36ea47e0 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands/tree/replacement.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands/tree/replacement.rs @@ -39,9 +39,11 @@ impl DocumentRuntime { let _ = self .remove_child_appending_to_current_reaction_queue(scope, host_ptr, parent, *child); } - let changed = self - .append_child_appending_to_current_reaction_queue(scope, host_ptr, parent, fragment) - || removes_existing_children; + let inserted_new_children = !added_children.is_empty() + && self.append_child_appending_to_current_reaction_queue( + scope, host_ptr, parent, fragment, + ); + let changed = inserted_new_children || removes_existing_children; if changed && records_enabled { crate::observer_runtime::coalesce_child_list_replacement_records( host_ptr, @@ -249,11 +251,14 @@ impl DocumentRuntime { next_sibling, ); } - let changed = self.apply_runtime_mutation_effects_with_prepublished_removals( + let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps( scope, host_ptr, + &insertion_plan, effects, RuntimeMutationOptions::js_dom_api(), + reaction_policy, + true, prepublished_removals, ); if !changed { @@ -279,6 +284,13 @@ impl DocumentRuntime { replacement_plan: &TreeReplacementPlan<'_>, reaction_policy: TreeReactionDispatchPolicy, ) { + self.queue_selectedcontent_updates_after_tree_removal( + scope, + host_ptr, + &replacement_plan + .removal + .selected_option_owners_before_remove, + ); let insertion_plan = &replacement_plan.insertion; let mut dispatch_reactions = |scope: &mut v8::PinScope<'_, '_>| { self.dispatch_tree_insertion_immediate_side_effects_after_change( diff --git a/moli-renderer-v8/src/mutation_coordinator.rs b/moli-renderer-v8/src/mutation_coordinator.rs index 7e8e3dacd2..9212bb176d 100644 --- a/moli-renderer-v8/src/mutation_coordinator.rs +++ b/moli-renderer-v8/src/mutation_coordinator.rs @@ -113,11 +113,21 @@ impl RuntimeMutationOptions { } #[derive(Debug, Clone, Copy)] -struct ScriptStartRequest { +pub(super) struct ScriptStartRequest { handle: NativeNodeId, clears_force_async: bool, } +impl ScriptStartRequest { + pub(super) fn handle(&self) -> NativeNodeId { + self.handle + } + + pub(super) fn clears_force_async(&self) -> bool { + self.clears_force_async + } +} + #[derive(Debug, Default)] struct ScriptStartRequests { requests: Vec, @@ -244,20 +254,11 @@ impl MutationCoordinator { } let mutation_record_count = effects.observer_records().records().len(); let script_planning_started = cpu_profile_enabled.then(Instant::now); - let mut script_start_requests = ScriptStartRequests::default(); - if prepare_connected_scripts { - for &root in effects.scripts().connected_roots() { - self.collect_connected_scripts_in_subtree( - dom_host, - root, - &mut script_start_requests, - ); - } - for trigger in effects.scripts().prepare_triggers() { - script_start_requests.queue(trigger.handle(), trigger.clears_script_force_async()); - } - } - let script_start_requests = script_start_requests.into_tree_order(dom_host); + let script_start_requests = if prepare_connected_scripts { + self.plan_connected_script_start_requests(dom_host, &effects) + } else { + Vec::new() + }; let script_start_request_count = script_start_requests.len(); let script_planning_us = script_planning_started .map(|started| started.elapsed().as_micros()) @@ -376,7 +377,22 @@ impl MutationCoordinator { } } - fn collect_connected_script_start_candidate( + pub(super) fn plan_connected_script_start_requests( + &self, + dom_host: &mut DomHost, + effects: &DomMutationEffects, + ) -> Vec { + let mut requests = ScriptStartRequests::default(); + for &root in effects.scripts().connected_roots() { + self.collect_connected_scripts_in_subtree(dom_host, root, &mut requests); + } + for trigger in effects.scripts().prepare_triggers() { + requests.queue(trigger.handle(), trigger.clears_script_force_async()); + } + requests.into_tree_order(dom_host) + } + + pub(super) fn collect_connected_script_start_candidate( &mut self, scope: &mut v8::PinScope<'_, '_>, host_ptr: *mut JsContextHost, 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 e64247c190..f92a1e7ff8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -302,6 +302,7 @@ impl JsContextHost { super::misc_platform_api_tasks::MiscPlatformApiTaskState::default(), file_entry_file_callbacks: super::file_entry_file_callbacks::FileEntryFileCallbackState::default(), + pending_selectedcontent_updates: HashSet::new(), user_interaction_tasks: super::user_interaction_tasks::UserInteractionTaskState::default(), pending_image_load_events: HashMap::new(), @@ -1512,6 +1513,14 @@ impl JsContextHost { } } + pub(crate) fn mark_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool { + self.pending_selectedcontent_updates.insert(select) + } + + pub(crate) fn take_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool { + self.pending_selectedcontent_updates.remove(&select) + } + pub(crate) fn pending_image_load_event( &self, handle: DomHandle, diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 8465a8a9bb..f5faf452a3 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -909,6 +909,7 @@ pub(crate) struct JsContextHost { directory_reader_callbacks: directory_reader_callbacks::DirectoryReaderCallbackState, misc_platform_api_tasks: misc_platform_api_tasks::MiscPlatformApiTaskState, file_entry_file_callbacks: file_entry_file_callbacks::FileEntryFileCallbackState, + pending_selectedcontent_updates: HashSet, user_interaction_tasks: user_interaction_tasks::UserInteractionTaskState, pending_image_load_events: HashMap, next_image_load_event_id: u64, diff --git a/moli-renderer-v8/src/native_bridge/element/forms/select/select_element.rs b/moli-renderer-v8/src/native_bridge/element/forms/select/select_element.rs index 2f148b0940..b4f00a1d7f 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/select/select_element.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/select/select_element.rs @@ -636,6 +636,7 @@ pub(in crate::native_bridge) fn select_selected_index_setter_function<'s>( let _ = runtime.set_selected_state(scope, runtime_ptr, option, should_select); } let _ = runtime.set_select_explicit_none(scope, runtime_ptr, handle, !matched); + let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, runtime_ptr, handle); rv.set_undefined(); } diff --git a/moli-renderer-v8/src/native_bridge/element/forms/submission.rs b/moli-renderer-v8/src/native_bridge/element/forms/submission.rs index f2c18e3593..16102f6e19 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/submission.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/submission.rs @@ -400,6 +400,11 @@ pub(in crate::native_bridge) fn reset_form_default_action( did_change |= runtime .dom_host_mut() .set_select_explicit_none_state(handle, false); + did_change |= runtime.sync_selectedcontents_for_select_in_reaction_scope( + scope, + runtime_ptr, + handle, + ); } FormResetPlan::Output { handle, value } => { did_change |= runtime.dom_host_mut().set_text_content(handle, &value); diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index 1d6711314d..3d095240f9 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -12400,7 +12400,7 @@ JSON.stringify({ } #[test] - fn parser_option_finish_and_select_value_sync_selectedcontent_clones() { + fn parser_option_finish_and_select_setters_sync_selectedcontent_clones() { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -12409,11 +12409,11 @@ JSON.stringify({ runtime.block_on(tokio::task::LocalSet::new().run_until(async move { let mut page_vm = parse_phase_one_html_into_page_vm_for_test( r#" -
- + "#, ) @@ -12437,8 +12442,49 @@ window.selectedcontentState.push( .expect("selectedcontent parser state should evaluate"); assert_eq!( result.get("value").and_then(serde_json::Value::as_str), - Some(r#"["one",true,"two","STRONG"]"#), - "parser option completion and select.value must synchronously clone the selected option children" + Some(r#"["one",true,"two","STRONG","updated","one"]"#), + "parser option completion and select setters must synchronously clone the selected option children" + ); + })); + } + + #[test] + fn moving_selected_option_updates_previous_select_selectedcontent_in_a_microtask() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread runtime should build"); + + runtime.block_on(tokio::task::LocalSet::new().run_until(async move { + let mut page_vm = parse_phase_one_html_into_page_vm_for_test( + r#" + +
+ +"#, + ) + .await; + + page_vm + .evaluate_expression("0") + .expect("selectedcontent removal microtask checkpoint should run"); + let result = page_vm + .evaluate_expression( + "JSON.stringify([...window.selectedcontentMoveState, document.getElementById('selectedcontent').textContent.trim()])", + ) + .expect("selectedcontent move state should evaluate"); + assert_eq!( + result.get("value").and_then(serde_json::Value::as_str), + Some(r#"["one","two"]"#), + "implicit option removal must keep the old clone synchronously and update it at the next microtask checkpoint" ); })); } diff --git a/moli-renderer-v8/src/script_vm/autofill.rs b/moli-renderer-v8/src/script_vm/autofill.rs index 2251ab63fc..a51b3a7ea6 100644 --- a/moli-renderer-v8/src/script_vm/autofill.rs +++ b/moli-renderer-v8/src/script_vm/autofill.rs @@ -203,7 +203,7 @@ fn fill_credit_card_control( }; if is_select { - let _ = unsafe { &mut *runtime_ptr }.set_select_value(handle, &value); + let _ = unsafe { &mut *runtime_ptr }.set_select_value(scope, runtime_ptr, handle, &value); } else { let _ = unsafe { &mut *runtime_ptr }.set_input_value(handle, &value); }