Skip to content

Commit c356c5a

Browse files
Merge pull request #869 from SceneTech/fix/range-detached-owner-863
Preserve Range ownership for detached nodes
2 parents e9e53c4 + 6f8e830 commit c356c5a

6 files changed

Lines changed: 120 additions & 2 deletions

File tree

‎docs/validation/range-document-owner-863.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,17 @@ existing targeted highlight repaint behavior.
4444
Exact packaged Code OSS verification remains required after this focused PR is
4545
merged and the WebScene pin is advanced. That run owns the final zero-error
4646
Monaco trace and editor geometry comparison.
47+
48+
## Exact-package follow-up
49+
50+
The first exact package after #866 reached the extension-host Ready and
51+
Initialized states, but Monaco still raised `WrongDocumentError` when
52+
`_detachRange()` selected its `_textRangeRestingSpot`. That node is created by
53+
`document.createElement('div')` and deliberately never connected. WebScene had
54+
been treating connectivity to the document root as document ownership.
55+
56+
Document factory calls now record the receiver document independently of the
57+
parent tree. Range validation follows that identity, so a never-connected node
58+
and its descendants remain valid endpoints while a node created by another
59+
Document still fails atomically. Detached-subtree collection and navigation
60+
clear the ownership records with the corresponding native nodes.

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,6 +2007,7 @@
20072007
std::erase_if(detached_document_roots, [&](const auto id) {
20082008
return node_ids.contains(static_cast<uint32_t>(id));
20092009
});
2010+
for (const auto id : node_ids) node_document_roots.erase(id);
20102011
std::erase_if(hidden_frame_document_roots, [&](const auto id) {
20112012
return node_ids.contains(id);
20122013
});
@@ -4944,6 +4945,7 @@
49444945
"html-element");
49454946
auto& node = self->document.create_element(std::move(tag));
49464947
node.xml_mode = xml_mode;
4948+
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
49474949
self->apply_css_rules(node);
49484950
info.GetReturnValue().Set(self->wrap_node(node));
49494951
}
@@ -4959,6 +4961,12 @@
49594961
{},
49604962
"html-element");
49614963
auto& node = self->document.create_element(std::move(tag));
4964+
auto* document_root = info.This()->InternalFieldCount() > 0
4965+
? static_cast<dom_node*>(info.This()->GetAlignedPointerFromInternalField(
4966+
0,
4967+
v8::kEmbedderDataTypeTagDefault))
4968+
: nullptr;
4969+
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
49624970
node.set_namespace(info.Length() > 0
49634971
? to_utf8(info.GetIsolate(), info[0])
49644972
: "http://www.w3.org/2000/svg");
@@ -4976,6 +4984,7 @@
49764984
v8::kEmbedderDataTypeTagDefault))
49774985
: nullptr;
49784986
node.xml_mode = document_root != nullptr && document_root->xml_mode;
4987+
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
49794988
node.text_content = info.Length() > 0 ? to_wtf8(info.GetIsolate(), info[0]) : std::string{};
49804989
info.GetReturnValue().Set(self->wrap_node(node));
49814990
}
@@ -5007,6 +5016,7 @@
50075016
v8::kEmbedderDataTypeTagDefault))
50085017
: nullptr;
50095018
node.xml_mode = document_root != nullptr && document_root->xml_mode;
5019+
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
50105020
node.visible = false;
50115021
node.text_content = info.Length() > 0 ? to_wtf8(info.GetIsolate(), info[0]) : std::string{};
50125022
info.GetReturnValue().Set(self->wrap_node(node));
@@ -5410,6 +5420,7 @@
54105420
v8::kEmbedderDataTypeTagDefault))
54115421
: nullptr;
54125422
node.xml_mode = document_root != nullptr && document_root->xml_mode;
5423+
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
54135424
node.visible = false;
54145425
info.GetReturnValue().Set(self->wrap_node(node));
54155426
}
@@ -5491,6 +5502,12 @@
54915502
const auto deep = info.Length() > 1
54925503
&& info[1]->BooleanValue(info.GetIsolate());
54935504
auto& clone = clone_dom_subtree(*self, *source, deep);
5505+
auto* document_root = info.This()->InternalFieldCount() > 0
5506+
? static_cast<dom_node*>(info.This()->GetAlignedPointerFromInternalField(
5507+
0,
5508+
v8::kEmbedderDataTypeTagDefault))
5509+
: nullptr;
5510+
if (document_root != nullptr) self->bind_node_to_document(clone, *document_root);
54945511
info.GetReturnValue().Set(self->wrap_node(clone));
54955512
}
54965513

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,29 @@
11001100
binding_callback_timer binding_timer(self->profile(binding_category::dom_property));
11011101
auto isolate = info.GetIsolate();
11021102
auto* node = unwrap_node(info.Holder());
1103+
if (node != nullptr) {
1104+
const auto owner = self->node_document_roots.find(node->id);
1105+
auto* root = owner == self->node_document_roots.end()
1106+
? nullptr : self->document.find_by_native_id(owner->second);
1107+
if (root != nullptr && self->detached_document_roots.contains(root->id)) {
1108+
info.GetReturnValue().Set(wrap_detached_document(
1109+
*self,
1110+
*root,
1111+
isolate->GetCurrentContext(),
1112+
"about:blank"));
1113+
return;
1114+
}
1115+
if (root != nullptr && root->parent != nullptr
1116+
&& root->parent->tag == "iframe") {
1117+
auto* frame = root->parent;
1118+
const auto actual = self->actual_frame_documents.find(frame->id);
1119+
info.GetReturnValue().Set(
1120+
actual != self->actual_frame_documents.end()
1121+
? actual->second.Get(isolate)
1122+
: self->frame_document(*frame));
1123+
return;
1124+
}
1125+
}
11031126
for (auto* ancestor = node; ancestor != nullptr; ancestor = ancestor->parent) {
11041127
if (self->detached_document_roots.contains(ancestor->id)) {
11051128
info.GetReturnValue().Set(wrap_detached_document(
@@ -1976,6 +1999,33 @@
19761999
return root == nullptr ? &active_root() : root;
19772000
}
19782001

2002+
uint32_t document_root_id_for_node(const dom_node& node)
2003+
{
2004+
for (auto* current_node = &node; current_node != nullptr;
2005+
current_node = document.dom_parent(*current_node)) {
2006+
if (const auto known = node_document_roots.find(current_node->id);
2007+
known != node_document_roots.end()) {
2008+
return known->second;
2009+
}
2010+
if (detached_document_roots.contains(current_node->id)) {
2011+
return current_node->id;
2012+
}
2013+
if (current_node == &active_root()) return current_node->id;
2014+
if (const auto* parent = document.dom_parent(*current_node);
2015+
parent != nullptr && parent->tag == "iframe") {
2016+
return current_node->id;
2017+
}
2018+
}
2019+
return 0U;
2020+
}
2021+
2022+
void bind_node_to_document(dom_node& node, const dom_node& root)
2023+
{
2024+
visit_subtree(node, [&](dom_node& descendant) {
2025+
node_document_roots[descendant.id] = root.id;
2026+
});
2027+
}
2028+
19792029
bool range_binding_is_live(const range_binding& binding) const
19802030
{
19812031
return !binding.released
@@ -1985,8 +2035,9 @@
19852035
&& document.find_by_native_id(binding.root_id) != nullptr;
19862036
}
19872037

1988-
bool range_node_is_in_root(const dom_node& root, const dom_node& node) const
2038+
bool range_node_is_in_root(const dom_node& root, const dom_node& node)
19892039
{
2040+
if (document_root_id_for_node(node) == root.id) return true;
19902041
for (auto* current_node = &node; current_node != nullptr;
19912042
current_node = document.dom_parent(*current_node)) {
19922043
if (current_node == &root) return true;
@@ -2354,9 +2405,15 @@
23542405
static void range_create_contextual_fragment(const v8::FunctionCallbackInfo<v8::Value>& info)
23552406
{
23562407
auto* self = current(info.GetIsolate());
2408+
auto* binding = self == nullptr ? nullptr : self->require_live_range(info);
2409+
if (binding == nullptr) return;
23572410
const auto html = info.Length() > 0 ? to_utf8(info.GetIsolate(), info[0]) : std::string{};
23582411
if (!html.empty() && !self->require_runtime_html()) return;
23592412
auto& fragment_root = self->document.create_element("#fragment");
2413+
if (const auto* root = self->document.find_by_native_id(binding->root_id);
2414+
root != nullptr) {
2415+
self->bind_node_to_document(fragment_root, *root);
2416+
}
23602417
self->parse_inner_html(fragment_root, html);
23612418
// A contextual fragment is a real queryable DOM node. Reuse the native
23622419
// node wrapper so querySelector(All), children and removeChild all have

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@
356356
next_adopted_stylesheet_owner_id = 0xf0000000U;
357357
detached_documents.clear();
358358
detached_document_roots.clear();
359+
node_document_roots.clear();
359360
node_wrappers.clear();
360361
attr_wrappers.clear();
361362
attached_attributes.clear();

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@
187187
bool service_worker_realm{false};
188188
std::unordered_set<uint64_t> detached_document_roots;
189189
std::unordered_map<uint64_t, v8::Global<v8::Object>> detached_documents;
190+
// DOM ownership survives removal from the tree. Keep the creating/adopting
191+
// document separate from parent links so detached nodes remain valid Range
192+
// endpoints, as required by Monaco's reusable measurement Range.
193+
std::unordered_map<uint32_t, uint32_t> node_document_roots;
190194
std::unordered_map<uint32_t, window_selection_state> window_selections;
191195
static constexpr size_t maximum_live_ranges = 4096U;
192196
std::vector<std::unique_ptr<range_binding>> range_bindings;

‎experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2203,6 +2203,30 @@ void test_live_range_boundary_contract(webscene_engine* engine)
22032203
parsedRange.endContainer === parsedTarget,
22042204
parsedRange.endOffset
22052205
];
2206+
// Monaco creates this resting node with document.createElement(),
2207+
// never connects it, and selects it from _detachRange() in finally.
2208+
const restingSpot = document.createElement('div');
2209+
const monacoRange = document.createRange();
2210+
monacoRange.selectNodeContents(restingSpot);
2211+
const detachedMainSelected = [
2212+
restingSpot.ownerDocument === document,
2213+
monacoRange.startContainer === restingSpot,
2214+
monacoRange.startOffset,
2215+
monacoRange.endContainer === restingSpot,
2216+
monacoRange.endOffset
2217+
];
2218+
const parsedRestingSpot = parsed.createElement('div');
2219+
parsedRestingSpot.textContent = 'rest';
2220+
parsedRange.selectNodeContents(parsedRestingSpot);
2221+
const detachedParsedSelected = [
2222+
parsedRestingSpot.ownerDocument === parsed,
2223+
parsedRange.startContainer === parsedRestingSpot,
2224+
parsedRange.endContainer === parsedRestingSpot,
2225+
parsedRange.endOffset
2226+
];
2227+
let detachedWrongDocument = '';
2228+
try { monacoRange.selectNodeContents(parsedRestingSpot); }
2229+
catch (error) { detachedWrongDocument = error.name; }
22062230
const reuseStarted = performance.now();
22072231
for (let cycle = 0; cycle < 10000; ++cycle) {
22082232
parsedRange.setStart(parsedText, cycle % 4);
@@ -2214,13 +2238,14 @@ void test_live_range_boundary_contract(webscene_engine* engine)
22142238
return {
22152239
cross, marker, selected, collapsedForward, collapsedBackward,
22162240
invalid, parsedBefore, wrongDocument, parsedAtomic, parsedSelected,
2241+
detachedMainSelected, detachedParsedSelected, detachedWrongDocument,
22172242
reuseCycles: 10000,
22182243
reuseWithinBudget: reuseDuration <= 2000
22192244
};
22202245
})()
22212246
)JS", "native-live-range-boundaries.js");
22222247
require(
2223-
result == R"({"cross":[true,true,1,true,3,true,false],"marker":[true,2,true,3],"selected":[0,5,false],"collapsedForward":true,"collapsedBackward":true,"invalid":"IndexSizeError","parsedBefore":[true,1,true,7],"wrongDocument":"WrongDocumentError","parsedAtomic":true,"parsedSelected":[true,0,true,1],"reuseCycles":10000,"reuseWithinBudget":true})",
2248+
result == R"({"cross":[true,true,1,true,3,true,false],"marker":[true,2,true,3],"selected":[0,5,false],"collapsedForward":true,"collapsedBackward":true,"invalid":"IndexSizeError","parsedBefore":[true,1,true,7],"wrongDocument":"WrongDocumentError","parsedAtomic":true,"parsedSelected":[true,0,true,1],"detachedMainSelected":[true,true,0,true,0],"detachedParsedSelected":[true,true,true,1],"detachedWrongDocument":"WrongDocumentError","reuseCycles":10000,"reuseWithinBudget":true})",
22242249
"live Range boundary contract regressed: " + result);
22252250
std::cout << "Range document-owner reuse gate: cycles=10000 durationMs="
22262251
<< evaluate(engine, "__rangeDocumentOwnerReuseDuration",

0 commit comments

Comments
 (0)