mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 00:06:19 +00:00
refactor(dom): add native ownership transfer for detached subtrees
This commit is contained in:
@@ -182,6 +182,33 @@ pub struct ElementControlState {
|
||||
}
|
||||
|
||||
impl ElementControlState {
|
||||
pub(super) fn native_node_references(&self) -> Vec<NativeNodeId> {
|
||||
let mut references = Vec::new();
|
||||
references.extend(self.dialog_previously_focused_element);
|
||||
if let Some(explicit) = &self.explicit_element_references {
|
||||
for elements in explicit.references_by_attribute.values() {
|
||||
references.extend(elements);
|
||||
}
|
||||
}
|
||||
references
|
||||
}
|
||||
|
||||
pub(super) fn remap_native_node_references(
|
||||
&mut self,
|
||||
handles: &HashMap<NativeNodeId, NativeNodeId>,
|
||||
) {
|
||||
if let Some(focused) = self.dialog_previously_focused_element.as_mut() {
|
||||
*focused = handles[focused];
|
||||
}
|
||||
if let Some(explicit) = self.explicit_element_references.as_mut() {
|
||||
for elements in explicit.references_by_attribute.values_mut() {
|
||||
for element in elements {
|
||||
*element = handles[element];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn web_mcp_form_active(&self) -> bool {
|
||||
self.web_mcp_form_active
|
||||
}
|
||||
|
||||
@@ -96,6 +96,17 @@ pub struct Element {
|
||||
}
|
||||
|
||||
impl Element {
|
||||
pub(crate) fn native_node_references(&self) -> Vec<NativeNodeId> {
|
||||
self.rare_data.native_node_references()
|
||||
}
|
||||
|
||||
pub(crate) fn remap_native_node_references(
|
||||
&mut self,
|
||||
handles: &std::collections::HashMap<NativeNodeId, NativeNodeId>,
|
||||
) {
|
||||
self.rare_data.remap_native_node_references(handles);
|
||||
}
|
||||
|
||||
pub fn svg_user_transform(&self) -> SvgUserTransform {
|
||||
self.rare_data.svg_user_transform()
|
||||
}
|
||||
|
||||
@@ -50,6 +50,37 @@ impl ElementRareDataPayload {
|
||||
}
|
||||
|
||||
impl ElementRareData {
|
||||
pub(super) fn native_node_references(&self) -> Vec<NativeNodeId> {
|
||||
let Some(payload) = self.payload.as_deref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut references = Vec::new();
|
||||
references.extend(payload.parser_associated_form_owner);
|
||||
references.extend(payload.template_contents);
|
||||
if let Some(control) = &payload.control_state {
|
||||
references.extend(control.native_node_references());
|
||||
}
|
||||
references
|
||||
}
|
||||
|
||||
pub(super) fn remap_native_node_references(
|
||||
&mut self,
|
||||
handles: &std::collections::HashMap<NativeNodeId, NativeNodeId>,
|
||||
) {
|
||||
let Some(payload) = self.payload.as_deref_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(owner) = payload.parser_associated_form_owner.as_mut() {
|
||||
*owner = handles[owner];
|
||||
}
|
||||
if let Some(contents) = payload.template_contents.as_mut() {
|
||||
*contents = handles[contents];
|
||||
}
|
||||
if let Some(control) = payload.control_state.as_mut() {
|
||||
control.remap_native_node_references(handles);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn svg_user_transform(&self) -> SvgUserTransform {
|
||||
self.payload
|
||||
.as_deref()
|
||||
|
||||
@@ -22,6 +22,7 @@ mod query_index;
|
||||
mod retarget;
|
||||
mod stylesheet_candidates;
|
||||
mod svg_user_transform;
|
||||
mod transfer;
|
||||
mod types;
|
||||
|
||||
pub use html_serialization::{ShadowRootInclusion, ShadowRootRegistryAttributePolicy};
|
||||
@@ -45,6 +46,7 @@ use self::types::{
|
||||
};
|
||||
pub use self::types::{ConnectedShadowRootSnapshot, DomHandle, DomHost};
|
||||
pub use self::types::{HostElementSnapshot, ShadowRootBindingSnapshot, ShadowRootInit};
|
||||
pub use transfer::{DomSubtreeTransfer, DomSubtreeTransferError};
|
||||
|
||||
fn is_html_frame_owner_candidate(local_name: &str, namespace: &str) -> bool {
|
||||
(namespace.is_empty() || namespace == "http://www.w3.org/1999/xhtml")
|
||||
|
||||
@@ -245,7 +245,7 @@ impl DomHost {
|
||||
}
|
||||
}
|
||||
|
||||
fn record_shadow_root_binding_mutation(&self) {
|
||||
pub(super) fn record_shadow_root_binding_mutation(&self) {
|
||||
self.shadow_root_binding_version
|
||||
.set(self.shadow_root_binding_version.get().saturating_add(1));
|
||||
self.connected_shadow_roots_cache.borrow_mut().take();
|
||||
|
||||
@@ -100,6 +100,29 @@ impl StylesheetCandidateTransition {
|
||||
}
|
||||
|
||||
impl NativeDom {
|
||||
pub(super) fn transfer_stylesheet_candidate_scopes_from(
|
||||
&mut self,
|
||||
source: &mut Self,
|
||||
handles: &HashMap<NativeNodeId, NativeNodeId>,
|
||||
) {
|
||||
let source_scopes = Arc::make_mut(
|
||||
&mut source
|
||||
.stylesheet_candidate_registries
|
||||
.candidates_by_tree_scope,
|
||||
);
|
||||
let target_scopes = Arc::make_mut(
|
||||
&mut self
|
||||
.stylesheet_candidate_registries
|
||||
.candidates_by_tree_scope,
|
||||
);
|
||||
for (old, new) in handles {
|
||||
if let Some(candidates) = source_scopes.remove(old) {
|
||||
let moved = candidates.iter().map(|handle| handles[handle]).collect();
|
||||
target_scopes.insert(*new, Arc::new(moved));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stylesheet_candidate_handles_for_tree_scope(
|
||||
&self,
|
||||
tree_scope: NativeNodeId,
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
use super::*;
|
||||
|
||||
/// Native identities to rebind after moving an ownership graph between hosts.
|
||||
/// V8 reflectors, callbacks, observers, resources and registry state belong to
|
||||
/// the renderer and must migrate separately before exposing the moved graph.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct DomSubtreeTransfer {
|
||||
root: DomHandle,
|
||||
handles: HashMap<DomHandle, DomHandle>,
|
||||
}
|
||||
|
||||
impl DomSubtreeTransfer {
|
||||
pub fn root(&self) -> DomHandle {
|
||||
self.root
|
||||
}
|
||||
|
||||
pub fn handles(&self) -> &HashMap<DomHandle, DomHandle> {
|
||||
&self.handles
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DomSubtreeTransferError {
|
||||
InvalidTargetDocument(DomHandle),
|
||||
InvalidSource(DomHandle),
|
||||
AttachedSource(DomHandle),
|
||||
InvalidTree(DomHandle),
|
||||
OpenReference {
|
||||
node: DomHandle,
|
||||
referenced: DomHandle,
|
||||
},
|
||||
HandleCapacity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum TransferDocument {
|
||||
Target,
|
||||
TemplateContents,
|
||||
}
|
||||
|
||||
struct TransferPlan {
|
||||
nodes: Vec<(DomHandle, TransferDocument)>,
|
||||
membership: HashSet<DomHandle>,
|
||||
}
|
||||
|
||||
impl DomHost {
|
||||
/// Move a detached, closed native ownership graph without DOM clone steps.
|
||||
///
|
||||
/// Includes all shadow roots (also closed/non-clonable), template contents,
|
||||
/// manual slot assignments and native element state. Old source slots are
|
||||
/// retired, never reused; snapshots keep their immutable source view.
|
||||
///
|
||||
/// This is the native stage of adoption, not a Web API entrypoint. The
|
||||
/// caller must first perform removal and its lifecycle/observer work, and
|
||||
/// then rebind renderer identities using the returned map. References that
|
||||
/// cross the graph boundary currently require a wider ownership service;
|
||||
/// they are rejected before either host changes instead of being dropped
|
||||
/// or accidentally rebound to a colliding host-local handle.
|
||||
pub fn transfer_detached_subtree_from(
|
||||
&mut self,
|
||||
document: DomHandle,
|
||||
source: &mut Self,
|
||||
root: DomHandle,
|
||||
) -> Result<DomSubtreeTransfer, DomSubtreeTransferError> {
|
||||
if !self.node(document).is_some_and(Node::is_document) {
|
||||
return Err(DomSubtreeTransferError::InvalidTargetDocument(document));
|
||||
}
|
||||
let plan = source.preflight_subtree_transfer(root)?;
|
||||
let needs_template_document = plan
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|(_, owner)| matches!(owner, TransferDocument::TemplateContents));
|
||||
let capacity = self
|
||||
.dom
|
||||
.len()
|
||||
.checked_add(plan.nodes.len())
|
||||
.and_then(|count| {
|
||||
count.checked_add(usize::from(
|
||||
needs_template_document
|
||||
&& !self.dom.inert_template_documents.contains_key(&document),
|
||||
))
|
||||
});
|
||||
if capacity.is_none_or(|count| count > u32::MAX as usize) {
|
||||
return Err(DomSubtreeTransferError::HandleCapacity);
|
||||
}
|
||||
// Every fallible check precedes destination allocation or source removal.
|
||||
let template_document = if needs_template_document {
|
||||
self.dom
|
||||
.appropriate_template_contents_owner_document(document)
|
||||
} else {
|
||||
document
|
||||
};
|
||||
let first_slot = self.dom.len();
|
||||
let handles: HashMap<_, _> = plan
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(offset, (old, _))| (*old, DomHandle::new(first_slot + offset)))
|
||||
.collect();
|
||||
for (old, owner) in &plan.nodes {
|
||||
let mut node = source
|
||||
.dom
|
||||
.nodes
|
||||
.take(old.index())
|
||||
.expect("preflight retained every source node");
|
||||
let document = match owner {
|
||||
TransferDocument::Target => document,
|
||||
TransferDocument::TemplateContents => template_document,
|
||||
};
|
||||
node.remap_for_transfer(handles[old], document, &handles);
|
||||
self.dom.nodes.push(node);
|
||||
}
|
||||
self.transfer_shadow_bindings_from(source, &handles);
|
||||
self.dom
|
||||
.transfer_stylesheet_candidate_scopes_from(&mut source.dom, &handles);
|
||||
for old in &plan.membership {
|
||||
if let Some(assigned) = source.manual_slot_assignments.borrow_mut().remove(old) {
|
||||
let assigned = assigned.into_iter().map(|node| handles[&node]).collect();
|
||||
self.manual_slot_assignments
|
||||
.borrow_mut()
|
||||
.insert(handles[old], assigned);
|
||||
}
|
||||
}
|
||||
source
|
||||
.child_browsing_context_host_candidates
|
||||
.borrow_mut()
|
||||
.retain(|handle| !plan.membership.contains(handle));
|
||||
for (old, _) in &plan.nodes {
|
||||
let new = handles[old];
|
||||
if self
|
||||
.node(new)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| {
|
||||
is_html_frame_owner_candidate(element.local_name(), element.namespace())
|
||||
})
|
||||
{
|
||||
self.child_browsing_context_host_candidates
|
||||
.borrow_mut()
|
||||
.push(new);
|
||||
}
|
||||
}
|
||||
if source
|
||||
.active_element
|
||||
.get()
|
||||
.is_some_and(|node| plan.membership.contains(&node))
|
||||
{
|
||||
source.active_element.set(None);
|
||||
}
|
||||
if source
|
||||
.focus_transition_common_ancestor
|
||||
.get()
|
||||
.is_some_and(|node| plan.membership.contains(&node))
|
||||
{
|
||||
source.focus_transition_common_ancestor.set(None);
|
||||
}
|
||||
source
|
||||
.hovered_elements
|
||||
.borrow_mut()
|
||||
.retain(|node| !plan.membership.contains(node));
|
||||
source.invalidate_indexes_after_transfer();
|
||||
self.invalidate_indexes_after_transfer();
|
||||
Ok(DomSubtreeTransfer {
|
||||
root: handles[&root],
|
||||
handles,
|
||||
})
|
||||
}
|
||||
|
||||
fn preflight_subtree_transfer(
|
||||
&self,
|
||||
root: DomHandle,
|
||||
) -> Result<TransferPlan, DomSubtreeTransferError> {
|
||||
let node = self
|
||||
.node(root)
|
||||
.ok_or(DomSubtreeTransferError::InvalidSource(root))?;
|
||||
if node.is_document() || self.is_shadow_root(root) {
|
||||
return Err(DomSubtreeTransferError::InvalidSource(root));
|
||||
}
|
||||
if node.parent_node().is_some() || node.is_connected() {
|
||||
return Err(DomSubtreeTransferError::AttachedSource(root));
|
||||
}
|
||||
let mut plan = TransferPlan {
|
||||
nodes: Vec::new(),
|
||||
membership: HashSet::new(),
|
||||
};
|
||||
let mut stack = vec![(root, TransferDocument::Target)];
|
||||
while let Some((handle, owner)) = stack.pop() {
|
||||
let node = self
|
||||
.node(handle)
|
||||
.ok_or(DomSubtreeTransferError::InvalidTree(handle))?;
|
||||
if !plan.membership.insert(handle) || node.is_document() || node.is_connected() {
|
||||
return Err(DomSubtreeTransferError::InvalidTree(handle));
|
||||
}
|
||||
plan.nodes.push((handle, owner));
|
||||
let mut child = node.first_child();
|
||||
let mut siblings = HashSet::new();
|
||||
while let Some(handle) = child {
|
||||
if !siblings.insert(handle) || self.parent_node(handle) != Some(node.id()) {
|
||||
return Err(DomSubtreeTransferError::InvalidTree(handle));
|
||||
}
|
||||
stack.push((handle, owner));
|
||||
child = self
|
||||
.node(handle)
|
||||
.ok_or(DomSubtreeTransferError::InvalidTree(handle))?
|
||||
.next_sibling();
|
||||
}
|
||||
if let Some(shadow) = self.shadow_root_handle(handle) {
|
||||
stack.push((shadow, owner));
|
||||
}
|
||||
if let Some(contents) = node.as_element().and_then(Element::template_contents) {
|
||||
stack.push((contents, TransferDocument::TemplateContents));
|
||||
}
|
||||
}
|
||||
// Both outgoing and incoming host-local references must be closed.
|
||||
for node in self.dom.nodes() {
|
||||
for referenced in node.native_node_references() {
|
||||
if plan.membership.contains(&node.id()) != plan.membership.contains(&referenced) {
|
||||
return Err(DomSubtreeTransferError::OpenReference {
|
||||
node: node.id(),
|
||||
referenced,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (slot, assigned) in self.manual_slot_assignments.borrow().iter() {
|
||||
for &referenced in assigned {
|
||||
if plan.membership.contains(slot) != plan.membership.contains(&referenced) {
|
||||
return Err(DomSubtreeTransferError::OpenReference {
|
||||
node: *slot,
|
||||
referenced,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for &(node, _) in &plan.nodes {
|
||||
for referenced in self
|
||||
.dom
|
||||
.stylesheet_candidate_handles_for_tree_scope(node)
|
||||
.iter()
|
||||
{
|
||||
if !plan.membership.contains(referenced) {
|
||||
return Err(DomSubtreeTransferError::OpenReference {
|
||||
node,
|
||||
referenced: *referenced,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn transfer_shadow_bindings_from(
|
||||
&mut self,
|
||||
source: &mut Self,
|
||||
handles: &HashMap<DomHandle, DomHandle>,
|
||||
) {
|
||||
let mut changed = false;
|
||||
for (old, new) in handles {
|
||||
if let Some(mut state) = source.shadow_roots_by_host.borrow_mut().remove(old) {
|
||||
let old_root = state.handle;
|
||||
state.handle = handles[&old_root];
|
||||
source.shadow_hosts_by_root.borrow_mut().remove(&old_root);
|
||||
self.shadow_hosts_by_root
|
||||
.borrow_mut()
|
||||
.insert(state.handle, *new);
|
||||
self.shadow_roots_by_host.borrow_mut().insert(*new, state);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
source.record_shadow_root_binding_mutation();
|
||||
self.record_shadow_root_binding_mutation();
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate_indexes_after_transfer(&self) {
|
||||
self.id_index.borrow_mut().take();
|
||||
self.name_index.borrow_mut().take();
|
||||
*self.element_query_index.borrow_mut() = ElementQueryIndex::default();
|
||||
self.shadow_slot_name_indexes.borrow_mut().clear();
|
||||
self.record_mutation(MutationScope::QueryState);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,439 @@
|
||||
use super::*;
|
||||
use crate::native::{CustomElementState, DomStringValue, SelectedFile};
|
||||
|
||||
fn host(name: &str) -> DomHost {
|
||||
DomHost::from_dom(NativeDom::new_html(
|
||||
Url::parse(&format!("https://{name}.test/")).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
fn element(host: &mut DomHost, handle: DomHandle) -> &mut Element {
|
||||
host.node_mut(handle)
|
||||
.unwrap()
|
||||
.data_mut()
|
||||
.as_element_mut()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_moves_complete_native_state_and_retires_source_handles() {
|
||||
let mut source = host("source");
|
||||
let mut target = host("target");
|
||||
for _ in 0..31 {
|
||||
target.create_element("aside");
|
||||
}
|
||||
let document = target.document_handle();
|
||||
let root = source.create_element("section");
|
||||
let input = source.create_element("input");
|
||||
element(&mut source, input)
|
||||
.set_input_value_from_user_edit(DomStringValue::from_utf16(&[0xd800, 65, 0xdfff]));
|
||||
element(&mut source, input).set_selection_range_with_direction(1, 2, "backward");
|
||||
element(&mut source, input).set_checked_with_dirty(true, true);
|
||||
let files = source.create_element("input");
|
||||
source.set_attribute(files, "type", "file");
|
||||
element(&mut source, files).set_selected_files(vec![SelectedFile {
|
||||
bytes: vec![1, 3, 9],
|
||||
mime_type: "text/plain".into(),
|
||||
name: "native.txt".into(),
|
||||
last_modified: 7.0,
|
||||
}]);
|
||||
let script = source.create_element("script");
|
||||
element(&mut source, script).set_script_already_started(true);
|
||||
element(&mut source, script).set_script_force_async(false);
|
||||
element(&mut source, script).set_script_text_internal_slot("retained source");
|
||||
let media = source.create_element("video");
|
||||
element(&mut source, media).set_media_current_time(41.0);
|
||||
element(&mut source, media).set_media_volume(0.25);
|
||||
element(&mut source, media).set_media_paused(false);
|
||||
let svg = source
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), "p:svg")
|
||||
.unwrap();
|
||||
source.set_svg_root_scale(svg, 8.0);
|
||||
source.set_svg_root_translation(svg, [31.0, 7.0, 0.0, 1.0]);
|
||||
let custom = source.create_element("x-moved");
|
||||
element(&mut source, custom).set_custom_element_state(CustomElementState::Custom);
|
||||
element(&mut source, custom).set_custom_element_is_name(Some("x-moved".into()));
|
||||
element(&mut source, custom).set_cryptographic_nonce(Some("native nonce".into()));
|
||||
element(&mut source, custom).set_scroll_top(17.0);
|
||||
element(&mut source, custom).set_custom_validation_message("retained validation");
|
||||
element(&mut source, custom).set_attribute_ns_utf16_units(
|
||||
"value".into(),
|
||||
"urn:one".into(),
|
||||
Some("p".into()),
|
||||
"\u{fffd}".into(),
|
||||
vec![0xd800],
|
||||
);
|
||||
element(&mut source, custom).set_attribute_ns_utf16_units(
|
||||
"value".into(),
|
||||
"urn:two".into(),
|
||||
Some("p".into()),
|
||||
"\u{fffd}".into(),
|
||||
vec![0xdfff],
|
||||
);
|
||||
let text = source.create_text_node(DomStringValue::from_utf16(&[0xdfff, 13, 0xd800]));
|
||||
let children = [input, files, script, media, svg, custom, text];
|
||||
for child in children {
|
||||
assert!(source.append_child(root, child));
|
||||
}
|
||||
source
|
||||
.node_mut(script)
|
||||
.unwrap()
|
||||
.flags_mut()
|
||||
.set_parser_created(true);
|
||||
let payloads: HashMap<_, _> = [root]
|
||||
.into_iter()
|
||||
.chain(children)
|
||||
.map(|id| (id, format!("{:?}", source.node(id).unwrap().data())))
|
||||
.collect();
|
||||
let source_snapshot = source.clone();
|
||||
let allocated = source.dom.len();
|
||||
let source_version = source.query_version();
|
||||
let target_version = target.query_version();
|
||||
let moved = target
|
||||
.transfer_detached_subtree_from(document, &mut source, root)
|
||||
.unwrap();
|
||||
assert_eq!(moved.handles().len(), children.len() + 1);
|
||||
assert_eq!(source.dom.len(), allocated);
|
||||
assert_eq!(source.dom.nodes().len(), 1);
|
||||
assert!(source.query_version() > source_version);
|
||||
assert!(target.query_version() > target_version);
|
||||
for (&old, &new) in moved.handles() {
|
||||
assert!(source.node(old).is_none());
|
||||
let node = target.node(new).unwrap();
|
||||
assert_eq!(node.id(), new);
|
||||
assert_eq!(node.owner_document(), Some(document));
|
||||
assert!(!node.is_connected());
|
||||
assert_eq!(format!("{:?}", node.data()), payloads[&old]);
|
||||
assert!(source_snapshot.node(old).is_some());
|
||||
}
|
||||
assert_eq!(
|
||||
target.child_handles(moved.root()).collect::<Vec<_>>(),
|
||||
children.map(|id| moved.handles()[&id])
|
||||
);
|
||||
assert!(
|
||||
target
|
||||
.node(moved.handles()[&script])
|
||||
.unwrap()
|
||||
.flags()
|
||||
.parser_created()
|
||||
);
|
||||
let fresh = source.create_element("p");
|
||||
assert_eq!(fresh.index(), allocated);
|
||||
assert!(moved.handles().keys().all(|old| *old != fresh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_preserves_all_shadow_roots_manual_slots_and_stylesheet_membership() {
|
||||
let mut source = host("source");
|
||||
let mut target = host("target");
|
||||
let root = source.create_element("div");
|
||||
let light = source.create_element("span");
|
||||
source.append_child(root, light);
|
||||
let mut init = ShadowRootInit::new("closed");
|
||||
init.set_slot_assignment("manual");
|
||||
init.set_serializable(true);
|
||||
init.set_null_custom_element_registry(true);
|
||||
let shadow = source
|
||||
.attach_declarative_shadow_root_with_init(root, init.clone())
|
||||
.unwrap();
|
||||
source.set_shadow_root_available_to_element_internals(shadow, true);
|
||||
let slot = source.create_element("slot");
|
||||
let style = source.create_element("style");
|
||||
source.append_child(shadow, slot);
|
||||
source.append_child(shadow, style);
|
||||
source.assign_nodes_to_slot(slot, vec![light]);
|
||||
let nested_host = source.create_element("div");
|
||||
source.append_child(shadow, nested_host);
|
||||
let nested = source.attach_shadow_root(nested_host, "open").unwrap();
|
||||
let content = source.create_text_node("nested");
|
||||
source.append_child(nested, content);
|
||||
assert_eq!(
|
||||
source
|
||||
.stylesheet_candidate_handles_for_tree_scope(shadow)
|
||||
.as_ref(),
|
||||
&[style]
|
||||
);
|
||||
let snapshot = source.clone();
|
||||
let moved = target
|
||||
.transfer_detached_subtree_from(target.document_handle(), &mut source, root)
|
||||
.unwrap();
|
||||
let new_root = moved.root();
|
||||
let new_shadow = moved.handles()[&shadow];
|
||||
assert_eq!(source.shadow_root_handle(root), None);
|
||||
assert_eq!(source.shadow_root_host(shadow), None);
|
||||
assert_eq!(
|
||||
source
|
||||
.stylesheet_candidate_handles_for_tree_scope(shadow)
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(target.shadow_root_handle(new_root), Some(new_shadow));
|
||||
assert_eq!(target.shadow_root_host(new_shadow), Some(new_root));
|
||||
assert_eq!(
|
||||
target.shadow_root_mode(new_shadow).as_deref(),
|
||||
Some("closed")
|
||||
);
|
||||
assert_eq!(target.shadow_root_clonable(new_shadow), Some(false));
|
||||
assert_eq!(target.shadow_root_serializable(new_shadow), Some(true));
|
||||
assert_eq!(target.shadow_root_is_declarative(new_shadow), Some(true));
|
||||
assert_eq!(
|
||||
target.shadow_root_available_to_element_internals(new_shadow),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
target.shadow_root_uses_null_custom_element_registry(new_shadow),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
target.assigned_nodes_for_slot_with_options(moved.handles()[&slot], false),
|
||||
vec![moved.handles()[&light]]
|
||||
);
|
||||
assert_eq!(
|
||||
target
|
||||
.stylesheet_candidate_handles_for_tree_scope(new_shadow)
|
||||
.as_ref(),
|
||||
&[moved.handles()[&style]]
|
||||
);
|
||||
assert_eq!(
|
||||
target.shadow_root_host(moved.handles()[&nested]),
|
||||
Some(moved.handles()[&nested_host])
|
||||
);
|
||||
assert_eq!(snapshot.shadow_root_handle(root), Some(shadow));
|
||||
assert_eq!(
|
||||
snapshot.assigned_nodes_for_slot_with_options(slot, false),
|
||||
vec![light]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_retargets_nested_template_contents_to_target_inert_document() {
|
||||
for xml in [false, true] {
|
||||
let mut source = host("source");
|
||||
let mut target = if xml {
|
||||
DomHost::from_dom(NativeDom::new_xml(
|
||||
Url::parse("https://target.test/").unwrap(),
|
||||
))
|
||||
} else {
|
||||
host("target")
|
||||
};
|
||||
let root = source.create_element("template");
|
||||
let contents = source
|
||||
.node(root)
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.template_contents()
|
||||
.unwrap();
|
||||
let nested = source.create_element("template");
|
||||
let nested_contents = source
|
||||
.node(nested)
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.template_contents()
|
||||
.unwrap();
|
||||
source.append_child(contents, nested);
|
||||
let text = source.create_text_node(DomStringValue::from_utf16(&[0xd800, 0xdfff]));
|
||||
source.append_child(nested_contents, text);
|
||||
let moved = target
|
||||
.transfer_detached_subtree_from(target.document_handle(), &mut source, root)
|
||||
.unwrap();
|
||||
let target_document = target.document_handle();
|
||||
let target_inert = target
|
||||
.dom
|
||||
.appropriate_template_contents_owner_document(target_document);
|
||||
assert_ne!(target_document, target_inert);
|
||||
assert_eq!(
|
||||
target.node(moved.root()).unwrap().owner_document(),
|
||||
Some(target_document)
|
||||
);
|
||||
for old in [contents, nested, nested_contents, text] {
|
||||
assert_eq!(
|
||||
target.node(moved.handles()[&old]).unwrap().owner_document(),
|
||||
Some(target_inert)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
target
|
||||
.node(moved.handles()[&contents])
|
||||
.unwrap()
|
||||
.data()
|
||||
.as_document_fragment()
|
||||
.unwrap()
|
||||
.host(),
|
||||
Some(moved.root())
|
||||
);
|
||||
assert_eq!(
|
||||
target
|
||||
.node(moved.handles()[&nested])
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.template_contents(),
|
||||
Some(moved.handles()[&nested_contents])
|
||||
);
|
||||
assert_eq!(
|
||||
target
|
||||
.node(moved.handles()[&text])
|
||||
.unwrap()
|
||||
.character_data_value()
|
||||
.unwrap()
|
||||
.utf16_units()
|
||||
.as_ref(),
|
||||
&[0xd800, 0xdfff]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_remaps_element_references_without_confusing_host_local_ids() {
|
||||
let mut source = host("source");
|
||||
let mut target = host("target");
|
||||
for _ in 0..40 {
|
||||
target.create_element("p");
|
||||
}
|
||||
let root = source.create_element("form");
|
||||
let input = source.create_element("input");
|
||||
let dialog = source.create_element("dialog");
|
||||
source.append_child(root, input);
|
||||
source.append_child(root, dialog);
|
||||
element(&mut source, input).set_parser_associated_form_owner(Some(root));
|
||||
element(&mut source, dialog).set_dialog_previously_focused_element(Some(input));
|
||||
element(&mut source, root)
|
||||
.set_explicit_element_references("aria-controls", vec![input, dialog, input]);
|
||||
let moved = target
|
||||
.transfer_detached_subtree_from(target.document_handle(), &mut source, root)
|
||||
.unwrap();
|
||||
let new_input = moved.handles()[&input];
|
||||
let new_dialog = moved.handles()[&dialog];
|
||||
assert_eq!(
|
||||
target
|
||||
.node(new_input)
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.parser_associated_form_owner(),
|
||||
Some(moved.root())
|
||||
);
|
||||
assert_eq!(
|
||||
target
|
||||
.node(new_dialog)
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.dialog_previously_focused_element(),
|
||||
Some(new_input)
|
||||
);
|
||||
assert_eq!(
|
||||
target
|
||||
.node(moved.root())
|
||||
.unwrap()
|
||||
.as_element()
|
||||
.unwrap()
|
||||
.explicit_element_references("aria-controls"),
|
||||
Some([new_input, new_dialog, new_input].as_slice())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_rejects_open_references_before_mutating_either_host() {
|
||||
for incoming in [false, true] {
|
||||
let mut source = host("source");
|
||||
let mut target = host("target");
|
||||
let root = source.create_element("template");
|
||||
let other = source.create_element("p");
|
||||
let (node, referenced) = if incoming {
|
||||
(other, root)
|
||||
} else {
|
||||
(root, other)
|
||||
};
|
||||
element(&mut source, node)
|
||||
.set_explicit_element_references("aria-controls", vec![referenced]);
|
||||
let before_source = format!("{source:?}");
|
||||
let before_target = format!("{target:?}");
|
||||
assert_eq!(
|
||||
target.transfer_detached_subtree_from(target.document_handle(), &mut source, root),
|
||||
Err(DomSubtreeTransferError::OpenReference { node, referenced })
|
||||
);
|
||||
assert_eq!(format!("{source:?}"), before_source);
|
||||
assert_eq!(format!("{target:?}"), before_target);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_rejects_connected_or_unsupported_sources_atomically() {
|
||||
let mut source = host("source");
|
||||
let mut target = host("target");
|
||||
let root = source.create_element("div");
|
||||
source.append_child(source.document_handle(), root);
|
||||
let before_source = format!("{source:?}");
|
||||
let before_target = format!("{target:?}");
|
||||
assert_eq!(
|
||||
target.transfer_detached_subtree_from(target.document_handle(), &mut source, root),
|
||||
Err(DomSubtreeTransferError::AttachedSource(root))
|
||||
);
|
||||
assert_eq!(format!("{source:?}"), before_source);
|
||||
assert_eq!(format!("{target:?}"), before_target);
|
||||
source.remove_child(source.document_handle(), root);
|
||||
let source_document = source.document_handle();
|
||||
assert_eq!(
|
||||
target.transfer_detached_subtree_from(
|
||||
target.document_handle(),
|
||||
&mut source,
|
||||
source_document
|
||||
),
|
||||
Err(DomSubtreeTransferError::InvalidSource(source_document))
|
||||
);
|
||||
let shadow = source.attach_shadow_root(root, "closed").unwrap();
|
||||
assert_eq!(
|
||||
target.transfer_detached_subtree_from(target.document_handle(), &mut source, shadow),
|
||||
Err(DomSubtreeTransferError::InvalidSource(shadow))
|
||||
);
|
||||
let bad_document = target.create_element("p");
|
||||
assert_eq!(
|
||||
target.transfer_detached_subtree_from(bad_document, &mut source, root),
|
||||
Err(DomSubtreeTransferError::InvalidTargetDocument(bad_document))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_keeps_retained_indexes_complete_across_repeated_moves() {
|
||||
let mut first = host("first");
|
||||
let mut second = host("second");
|
||||
let root = first.create_element("div");
|
||||
let child = first.create_element("p");
|
||||
first.set_attribute(child, "id", "moved");
|
||||
first.append_child(root, child);
|
||||
first.append_child(first.document_handle(), root);
|
||||
assert_eq!(first.element_handle_by_id("moved"), Some(child));
|
||||
first.remove_child(first.document_handle(), root);
|
||||
let frozen = first.clone();
|
||||
let mut current_root = root;
|
||||
let mut current_child = child;
|
||||
for _ in 0..3 {
|
||||
let moved = second
|
||||
.transfer_detached_subtree_from(second.document_handle(), &mut first, current_root)
|
||||
.unwrap();
|
||||
second.append_child(second.document_handle(), moved.root());
|
||||
assert_eq!(
|
||||
second.element_handle_by_id("moved"),
|
||||
Some(moved.handles()[¤t_child])
|
||||
);
|
||||
assert_eq!(first.element_handle_by_id("moved"), None);
|
||||
second.remove_child(second.document_handle(), moved.root());
|
||||
let returned = first
|
||||
.transfer_detached_subtree_from(first.document_handle(), &mut second, moved.root())
|
||||
.unwrap();
|
||||
first.append_child(first.document_handle(), returned.root());
|
||||
let returned_child = returned.handles()[&moved.handles()[¤t_child]];
|
||||
assert_eq!(first.element_handle_by_id("moved"), Some(returned_child));
|
||||
first.remove_child(first.document_handle(), returned.root());
|
||||
// Next iteration deliberately uses new identities, never vacant slots.
|
||||
assert!(first.node(current_root).is_none());
|
||||
assert!(second.node(moved.root()).is_none());
|
||||
current_root = returned.root();
|
||||
current_child = returned_child;
|
||||
}
|
||||
assert!(frozen.node(root).is_some());
|
||||
assert!(frozen.node(child).is_some());
|
||||
}
|
||||
+14
-103
@@ -7,13 +7,16 @@ mod host;
|
||||
mod html_serialization;
|
||||
mod markdown;
|
||||
mod node;
|
||||
mod node_storage;
|
||||
mod parser_construction;
|
||||
mod queries;
|
||||
mod scripts;
|
||||
mod string_value;
|
||||
mod svg_user_transform;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
#[cfg(test)]
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use document::{
|
||||
Document, DocumentBaseUrlPolicyCheck, DocumentFragment, DocumentMetaReferrerPolicySource,
|
||||
@@ -30,8 +33,9 @@ pub use host::{
|
||||
DomMutationRecordKind, DomScriptMutationEffects, DomSlotAssignmentChange,
|
||||
DomSlotMutationEffects, DomStyleInvalidationInputs, DomStylesheetOwnerChange,
|
||||
DomStylesheetOwnerChangeKind, DomStylesheetOwnerTransitions, DomStylesheetOwnerTreeScopes,
|
||||
DomTextNormalizationStep, DomTextareaValueChange, DomTreeMutationEffects, ScriptPrepareTrigger,
|
||||
ScriptPrepareTriggerKind, StylesheetCandidateTreeScopeSnapshots,
|
||||
DomSubtreeTransfer, DomSubtreeTransferError, DomTextNormalizationStep, DomTextareaValueChange,
|
||||
DomTreeMutationEffects, ScriptPrepareTrigger, ScriptPrepareTriggerKind,
|
||||
StylesheetCandidateTreeScopeSnapshots,
|
||||
};
|
||||
pub use host::{
|
||||
HostElementSnapshot, ShadowRootBindingSnapshot, ShadowRootInclusion, ShadowRootInit,
|
||||
@@ -42,111 +46,16 @@ pub use node::{
|
||||
CDataSection, Comment, LiveDomNodeMetadata, NativeNodeId, Node, NodeData, NodeFlags, NodeType,
|
||||
ProcessingInstruction, Text,
|
||||
};
|
||||
pub use node_storage::NativeDomNodes;
|
||||
use node_storage::NativeNodeStorage;
|
||||
pub use parser_construction::ParserConstruction;
|
||||
pub use string_value::DomStringValue;
|
||||
|
||||
// Node IDs remain dense indexes, while immutable page snapshots share complete
|
||||
// chunks. A mutation detaches only its 256-node chunk, bounding copy-on-write
|
||||
// work without introducing a pointer per node or changing tree identity.
|
||||
// Node IDs are monotonically allocated slot indexes. Immutable snapshots share
|
||||
// chunks; a mutation detaches only its 256-node chunk. Transferred slots are
|
||||
// vacant and are never reused for a different node.
|
||||
const NATIVE_DOM_NODE_CHUNK_CAPACITY: usize = 256;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NativeNodeStorage {
|
||||
chunks: Arc<Vec<Arc<Vec<Node>>>>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl NativeNodeStorage {
|
||||
fn from_node(node: Node) -> Self {
|
||||
Self {
|
||||
chunks: Arc::new(vec![Arc::new(vec![node])]),
|
||||
len: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Option<&Node> {
|
||||
let chunk = self.chunks.get(index / NATIVE_DOM_NODE_CHUNK_CAPACITY)?;
|
||||
chunk.get(index % NATIVE_DOM_NODE_CHUNK_CAPACITY)
|
||||
}
|
||||
|
||||
fn get_mut(&mut self, index: usize) -> Option<&mut Node> {
|
||||
let chunks = Arc::make_mut(&mut self.chunks);
|
||||
let chunk = chunks.get_mut(index / NATIVE_DOM_NODE_CHUNK_CAPACITY)?;
|
||||
Arc::make_mut(chunk).get_mut(index % NATIVE_DOM_NODE_CHUNK_CAPACITY)
|
||||
}
|
||||
|
||||
fn push(&mut self, node: Node) {
|
||||
let chunks = Arc::make_mut(&mut self.chunks);
|
||||
match chunks.last_mut() {
|
||||
Some(chunk) if chunk.len() < NATIVE_DOM_NODE_CHUNK_CAPACITY => {
|
||||
Arc::make_mut(chunk).push(node);
|
||||
}
|
||||
_ => chunks.push(Arc::new(vec![node])),
|
||||
}
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
fn iter(&self) -> NativeDomNodes<'_> {
|
||||
NativeDomNodes {
|
||||
storage: self,
|
||||
front: 0,
|
||||
back: self.len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeDomNodes<'a> {
|
||||
storage: &'a NativeNodeStorage,
|
||||
front: usize,
|
||||
back: usize,
|
||||
}
|
||||
|
||||
impl<'a> NativeDomNodes<'a> {
|
||||
pub fn iter(&self) -> Self {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NativeDomNodes<'a> {
|
||||
type Item = &'a Node;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.front == self.back {
|
||||
return None;
|
||||
}
|
||||
let index = self.front;
|
||||
self.front += 1;
|
||||
self.storage.get(index)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let remaining = self.back - self.front;
|
||||
(remaining, Some(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for NativeDomNodes<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.front == self.back {
|
||||
return None;
|
||||
}
|
||||
self.back -= 1;
|
||||
self.storage.get(self.back)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for NativeDomNodes<'_> {}
|
||||
impl std::iter::FusedIterator for NativeDomNodes<'_> {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NativeDom {
|
||||
nodes: NativeNodeStorage,
|
||||
@@ -213,6 +122,8 @@ impl NativeDom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of allocated handle slots, including transferred vacancies.
|
||||
/// Use `nodes().len()` for the number of nodes currently owned by this DOM.
|
||||
pub fn len(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
@@ -130,6 +130,46 @@ pub struct Node {
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub(crate) fn native_node_references(&self) -> Vec<NativeNodeId> {
|
||||
let mut references = match &self.data {
|
||||
NodeData::Element(element) => element.native_node_references(),
|
||||
NodeData::DocumentFragment(fragment) => fragment.host().into_iter().collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
references.extend(self.parent_node);
|
||||
references.extend(self.first_child);
|
||||
references.extend(self.last_child);
|
||||
references.extend(self.prev_sibling);
|
||||
references.extend(self.next_sibling);
|
||||
references
|
||||
}
|
||||
|
||||
/// Rebase a preflighted, closed ownership graph without applying clone steps.
|
||||
pub(crate) fn remap_for_transfer(
|
||||
&mut self,
|
||||
id: NativeNodeId,
|
||||
owner_document: NativeNodeId,
|
||||
handles: &std::collections::HashMap<NativeNodeId, NativeNodeId>,
|
||||
) {
|
||||
self.id = id;
|
||||
self.owner_document = Some(owner_document);
|
||||
self.parent_node = self.parent_node.map(|handle| handles[&handle]);
|
||||
self.first_child = self.first_child.map(|handle| handles[&handle]);
|
||||
self.last_child = self.last_child.map(|handle| handles[&handle]);
|
||||
self.prev_sibling = self.prev_sibling.map(|handle| handles[&handle]);
|
||||
self.next_sibling = self.next_sibling.map(|handle| handles[&handle]);
|
||||
self.flags.set_tree_state(false, false);
|
||||
match &mut self.data {
|
||||
NodeData::Element(element) => element.remap_native_node_references(handles),
|
||||
NodeData::DocumentFragment(fragment) => {
|
||||
if let Some(host) = fragment.host() {
|
||||
fragment.set_host(handles[&host]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DOCUMENT_POSITION_DISCONNECTED: u16 = 0x01;
|
||||
pub const DOCUMENT_POSITION_PRECEDING: u16 = 0x02;
|
||||
pub const DOCUMENT_POSITION_FOLLOWING: u16 = 0x04;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{NATIVE_DOM_NODE_CHUNK_CAPACITY, Node};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct NativeNodeStorage {
|
||||
pub(super) chunks: Arc<Vec<Arc<Vec<Option<Node>>>>>,
|
||||
allocated: usize,
|
||||
owned: usize,
|
||||
}
|
||||
|
||||
impl NativeNodeStorage {
|
||||
pub(super) fn from_node(node: Node) -> Self {
|
||||
Self {
|
||||
chunks: Arc::new(vec![Arc::new(vec![Some(node)])]),
|
||||
allocated: 1,
|
||||
owned: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.allocated
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.owned == 0
|
||||
}
|
||||
|
||||
pub(super) fn get(&self, index: usize) -> Option<&Node> {
|
||||
let chunk = self.chunks.get(index / NATIVE_DOM_NODE_CHUNK_CAPACITY)?;
|
||||
chunk.get(index % NATIVE_DOM_NODE_CHUNK_CAPACITY)?.as_ref()
|
||||
}
|
||||
|
||||
pub(super) fn get_mut(&mut self, index: usize) -> Option<&mut Node> {
|
||||
let chunks = Arc::make_mut(&mut self.chunks);
|
||||
let chunk = chunks.get_mut(index / NATIVE_DOM_NODE_CHUNK_CAPACITY)?;
|
||||
Arc::make_mut(chunk)
|
||||
.get_mut(index % NATIVE_DOM_NODE_CHUNK_CAPACITY)?
|
||||
.as_mut()
|
||||
}
|
||||
|
||||
/// Move the complete native payload out without reusing its old handle.
|
||||
/// Snapshots keep their own payload through the existing chunk COW boundary.
|
||||
pub(super) fn take(&mut self, index: usize) -> Option<Node> {
|
||||
self.get(index)?;
|
||||
let chunks = Arc::make_mut(&mut self.chunks);
|
||||
let chunk = Arc::make_mut(&mut chunks[index / NATIVE_DOM_NODE_CHUNK_CAPACITY]);
|
||||
let node = chunk[index % NATIVE_DOM_NODE_CHUNK_CAPACITY].take()?;
|
||||
self.owned -= 1;
|
||||
Some(node)
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, node: Node) {
|
||||
let chunks = Arc::make_mut(&mut self.chunks);
|
||||
match chunks.last_mut() {
|
||||
Some(chunk) if chunk.len() < NATIVE_DOM_NODE_CHUNK_CAPACITY => {
|
||||
Arc::make_mut(chunk).push(Some(node));
|
||||
}
|
||||
_ => chunks.push(Arc::new(vec![Some(node)])),
|
||||
}
|
||||
self.allocated += 1;
|
||||
self.owned += 1;
|
||||
}
|
||||
|
||||
pub(super) fn iter(&self) -> NativeDomNodes<'_> {
|
||||
NativeDomNodes {
|
||||
storage: self,
|
||||
front: 0,
|
||||
back: self.allocated,
|
||||
remaining: self.owned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeDomNodes<'a> {
|
||||
storage: &'a NativeNodeStorage,
|
||||
front: usize,
|
||||
back: usize,
|
||||
remaining: usize,
|
||||
}
|
||||
|
||||
impl NativeDomNodes<'_> {
|
||||
pub fn iter(&self) -> Self {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NativeDomNodes<'a> {
|
||||
type Item = &'a Node;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.front < self.back {
|
||||
let index = self.front;
|
||||
self.front += 1;
|
||||
if let Some(node) = self.storage.get(index) {
|
||||
self.remaining -= 1;
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(self.remaining, 0);
|
||||
None
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(self.remaining, Some(self.remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for NativeDomNodes<'_> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
while self.front < self.back {
|
||||
self.back -= 1;
|
||||
if let Some(node) = self.storage.get(self.back) {
|
||||
self.remaining -= 1;
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(self.remaining, 0);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for NativeDomNodes<'_> {}
|
||||
impl std::iter::FusedIterator for NativeDomNodes<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::native::{NativeNodeId, NodeData, NodeFlags, Text};
|
||||
|
||||
fn node(index: usize) -> Node {
|
||||
Node::new(
|
||||
NativeNodeId::new(index),
|
||||
None,
|
||||
None,
|
||||
NodeFlags::new(false),
|
||||
NodeData::Text(Text::new("native")),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vacancies_preserve_exact_size_double_ended_iteration_and_handle_allocation() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Option<Node>>(),
|
||||
std::mem::size_of::<Node>()
|
||||
);
|
||||
let mut storage = NativeNodeStorage::from_node(node(0));
|
||||
for index in 1..520 {
|
||||
storage.push(node(index));
|
||||
}
|
||||
for index in [0, 1, 3, 256, 257, 518, 519] {
|
||||
assert!(storage.take(index).is_some());
|
||||
}
|
||||
assert!(storage.take(256).is_none());
|
||||
assert!(storage.get_mut(256).is_none());
|
||||
assert_eq!(storage.len(), 520);
|
||||
let mut iter = storage.iter();
|
||||
assert_eq!(iter.len(), 513);
|
||||
assert_eq!(iter.next().unwrap().id().index(), 2);
|
||||
assert_eq!(iter.next_back().unwrap().id().index(), 517);
|
||||
assert_eq!(iter.size_hint(), (511, Some(511)));
|
||||
let copied = iter.iter();
|
||||
assert_eq!(copied.len(), 511);
|
||||
let rest = iter
|
||||
.by_ref()
|
||||
.map(|node| node.id().index())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(rest.len(), 511);
|
||||
assert!(!rest.contains(&256));
|
||||
assert!(iter.next().is_none());
|
||||
assert!(iter.next_back().is_none());
|
||||
storage.push(node(520));
|
||||
assert_eq!(storage.get(520).unwrap().id().index(), 520);
|
||||
assert_eq!(storage.iter().len(), 514);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_retires_only_the_source_chunk_and_preserves_immutable_snapshots() {
|
||||
let mut storage = NativeNodeStorage::from_node(node(0));
|
||||
for index in 1..600 {
|
||||
storage.push(node(index));
|
||||
}
|
||||
let snapshot = storage.clone();
|
||||
let payload = storage.take(300).unwrap();
|
||||
assert_eq!(payload.id().index(), 300);
|
||||
assert_eq!(snapshot.get(300).unwrap().id().index(), 300);
|
||||
assert!(storage.get(300).is_none());
|
||||
assert_eq!(snapshot.iter().len(), 600);
|
||||
assert_eq!(storage.iter().len(), 599);
|
||||
for (index, (live, frozen)) in storage
|
||||
.chunks
|
||||
.iter()
|
||||
.zip(snapshot.chunks.iter())
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(Arc::ptr_eq(live, frozen), index != 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -564,7 +564,7 @@ impl DocumentRuntime {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
) {
|
||||
let handles = (0..self.dom_host.dom().nodes().len())
|
||||
let handles = (0..self.dom_host.dom().len())
|
||||
.map(crate::document_runtime::DomHandle::new)
|
||||
.collect::<Vec<_>>();
|
||||
for handle in handles {
|
||||
@@ -596,7 +596,7 @@ impl DocumentRuntime {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
) {
|
||||
let handles = (0..self.dom_host.dom().nodes().len())
|
||||
let handles = (0..self.dom_host.dom().len())
|
||||
.map(DomHandle::new)
|
||||
.collect::<Vec<_>>();
|
||||
for handle in handles {
|
||||
@@ -1197,7 +1197,7 @@ mod tests {
|
||||
);
|
||||
let mut runtime = DocumentRuntime::new(&document);
|
||||
|
||||
let image_handles = (0..runtime.dom_host().dom().nodes().len())
|
||||
let image_handles = (0..runtime.dom_host().dom().len())
|
||||
.map(crate::document_runtime::DomHandle::new)
|
||||
.filter(|handle| {
|
||||
runtime
|
||||
|
||||
@@ -125,7 +125,7 @@ impl JsContextHost {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let image_handles = (0..self.dom_host().dom().nodes().len())
|
||||
let image_handles = (0..self.dom_host().dom().len())
|
||||
.map(DomHandle::new)
|
||||
.filter(|handle| {
|
||||
self.dom_host().owner_document_handle(*handle) == Some(snapshot.document_handle)
|
||||
@@ -163,7 +163,7 @@ impl JsContextHost {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let handles = (0..self.dom_host().dom().nodes().len())
|
||||
let handles = (0..self.dom_host().dom().len())
|
||||
.map(DomHandle::new)
|
||||
.filter(|handle| {
|
||||
self.dom_host().is_connected(*handle)
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(super) fn revealed_lazy_image_handles(
|
||||
document: DomHandle,
|
||||
output: &FrozenLayoutTree<DomHandle>,
|
||||
) -> Vec<DomHandle> {
|
||||
(0..runtime.dom_host().dom().nodes().len())
|
||||
(0..runtime.dom_host().dom().len())
|
||||
.map(DomHandle::new)
|
||||
.filter(|handle| is_unadmitted_lazy_image(runtime, document, *handle))
|
||||
.filter(|handle| image_is_near_live_viewport(runtime, document, output, *handle))
|
||||
|
||||
+2
-2
@@ -548,12 +548,12 @@ track.remove();
|
||||
.get_element_by_id("js-replace-inserted-track")
|
||||
.or_else(|| {
|
||||
let dom_host = page_vm.vm().document_runtime.dom_host();
|
||||
dom_host.dom().nodes().iter().enumerate().find_map(|(index, node)| {
|
||||
dom_host.dom().nodes().iter().find_map(|node| {
|
||||
node.as_element()
|
||||
.is_some_and(|element| {
|
||||
element.attribute("id") == Some("js-replace-inserted-track")
|
||||
})
|
||||
.then_some(DomHandle::new(index))
|
||||
.then_some(node.id())
|
||||
})
|
||||
})
|
||||
.expect("detached replacement track handle should exist");
|
||||
|
||||
+1
-1
@@ -1020,7 +1020,7 @@ async fn child_image_network_failure_releases_lifecycle_before_later_host_load()
|
||||
.expect("child image pre-terminal trace"),
|
||||
"dcl"
|
||||
);
|
||||
let child_image = (0..vm.document_runtime.dom_host().dom().nodes().len())
|
||||
let child_image = (0..vm.document_runtime.dom_host().dom().len())
|
||||
.map(crate::document_runtime::DomHandle::new)
|
||||
.find(|handle| {
|
||||
vm.document_runtime
|
||||
|
||||
Reference in New Issue
Block a user