fix(range): preserve live ranges when creating Window realms

This commit is contained in:
ldm0
2026-09-26 09:08:11 +08:00
parent c3c279cf36
commit 817f43006b
8 changed files with 60 additions and 29 deletions
@@ -2,13 +2,6 @@ use super::selection::selection_update_composed_boundaries_for_child_removal;
use super::*;
use crate::document_runtime::DomHandle;
pub(super) fn clear_live_range_registry(scope: &mut v8::PinScope<'_, '_>) {
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
unsafe { &mut *host_ptr }.clear_live_range_registry();
}
fn range_container_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
host_ptr: *mut JsContextHost,
@@ -14,7 +14,6 @@ use super::range_algorithms::{
range_geometry_client_rects, range_geometry_dom_rect, range_insert_node_at_boundary,
range_intersects_node_native, range_string_contents, range_surround_contents, root_handle,
};
use super::range_live::clear_live_range_registry;
use super::*;
mod accessors;
@@ -26,7 +25,7 @@ mod geometry;
mod install;
mod template;
pub(super) use install::{install_range_template_bindings, reset_range_runtime_state};
pub(super) use install::install_range_template_bindings;
pub(super) use template::{
build_abstract_range_template, build_range_constructor_template,
build_static_range_constructor_template,
@@ -15,7 +15,6 @@ use super::content::{
range_surround_contents_callback, range_to_string_callback,
};
use super::geometry::{range_get_bounding_client_rect_callback, range_get_client_rects_callback};
use super::*;
use crate::web_api_interfaces;
use moli_webapi_declare::WebApiFunctionTemplate;
@@ -126,10 +125,6 @@ struct AbstractRangePrototypeDeclaration {
common_ancestor_container: (),
}
pub(in crate::context_bootstrap) fn reset_range_runtime_state(scope: &mut v8::PinScope<'_, '_>) {
clear_live_range_registry(scope);
}
pub(in crate::context_bootstrap) fn install_range_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
@@ -12,7 +12,6 @@ use super::{
navigation_bootstrap::install_window_location_history_navigation_runtime_state,
navigator_runtime::{bind_window_navigator_identity_seed, install_navigator_runtime_state},
performance_runtime::install_default_window_performance_seed,
range_surface::reset_range_runtime_state,
trusted_types::install_trusted_types_runtime_state,
web_storage::{
install_storage_runtime_state, window_local_storage_getter, window_session_storage_getter,
@@ -1956,7 +1955,6 @@ pub(crate) fn finish_context_bootstrap(
install_window_global_accessors(scope, global);
native_bridge::install_detached_bridge_methods(scope);
reset_range_runtime_state(scope);
initialize_file_api_runtime_queues(scope, global)?;
super::exposed_interfaces::capture_eager_intrinsic_interfaces(
scope,
@@ -1,10 +1,6 @@
use super::*;
impl JsContextHost {
pub(crate) fn clear_live_range_registry(&mut self) {
self.range_record_registry.clear();
}
pub(crate) fn live_ranges_is_empty(&mut self) -> bool {
self.range_record_registry.active_is_empty()
}
@@ -70,6 +70,8 @@ pub(super) struct RangeRecord {
end: RangeBoundaryPoint,
}
// Shared by all realms in this JsContextHost. Creating another Window must not
// invalidate existing wrappers; their weak finalizers retire individual records.
pub(super) struct RangeRecordRegistry {
lifetime_token: u64,
active_live_records: HashSet<RangeRecordId>,
@@ -93,15 +95,6 @@ impl RangeRecordRegistry {
}
}
pub(super) fn clear(&mut self) {
self.drain_finalized_live_records();
self.active_live_records.clear();
self.live_record_wrappers.clear();
self.records.clear();
self.by_boundary_container.clear();
self.by_boundary_child_before.clear();
}
pub(super) fn active_is_empty(&mut self) -> bool {
self.drain_finalized_live_records();
self.active_live_records.is_empty()
@@ -2650,6 +2650,18 @@ fn pointer_event_sequences_preserve_identity_and_secure_context_exposure() {
assert_eq!(insecure, "false|function|false|true");
}
#[test]
fn live_ranges_survive_initialization_of_child_and_sibling_realms() {
let mut vm = new_storage_test_vm("https://range-realm-lifetime.test/");
assert_eq!(
vm.eval(include_str!(
"../../../../tests/fixtures/range-realm-lifetime.js"
))
.expect("new realms must preserve live ranges in existing documents"),
""
);
}
#[test]
fn character_data_setters_apply_replace_all_live_range_offsets() {
let mut vm = new_storage_test_vm("https://character-data-range.test/");
+45
View File
@@ -0,0 +1,45 @@
(() => {
const root = document.documentElement || document.appendChild(document.createElement('html'));
const body = document.body || root.appendChild(document.createElement('body'));
const fixture = body.appendChild(document.createElement('div'));
const text = fixture.appendChild(document.createTextNode('abcd'));
const range = document.createRange();
range.setStart(text, 1);
range.setEnd(text, 3);
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
const failures = [];
const check = (condition, label) => { if (!condition) failures.push(label); };
const frame = fixture.appendChild(document.createElement('iframe'));
const other = frame.contentWindow;
const doc = other.document;
const childBody = doc.body || doc.documentElement.appendChild(doc.createElement('body'));
const childText = childBody.appendChild(doc.createTextNode('wxyz'));
const childRange = new other.Range();
childRange.setStart(childText, 1);
childRange.setEnd(childText, 3);
const childSelection = other.getSelection();
childSelection.addRange(childRange);
const sibling = fixture.appendChild(document.createElement('iframe'));
sibling.contentDocument.body.appendChild(sibling.contentDocument.createElement('span'));
check(range.startContainer === text && range.startOffset === 1 &&
range.endContainer === text && range.endOffset === 3, 'parent range survives new realms');
check(childRange.startContainer === childText && childRange.startOffset === 1 &&
childRange.endContainer === childText && childRange.endOffset === 3, 'child range survives sibling realm');
check(selection.getRangeAt(0) === range && childSelection.getRangeAt(0) === childRange,
'selections retain associated range identity');
text.insertData(0, '!');
childText.deleteData(0, 1);
check(range.startOffset === 2 && range.endOffset === 4 && range.toString() === 'bc',
'parent range still tracks mutations');
check(childRange.startOffset === 0 && childRange.endOffset === 2 && childRange.toString() === 'xy',
'child range still tracks mutations');
const clone = childRange.cloneRange();
check(clone instanceof other.Range && clone.toString() === 'xy', 'child range remains usable in its realm');
fixture.remove();
return failures.join('\n');
})()