Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/validation/range-document-owner-863.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,17 @@ existing targeted highlight repaint behavior.
Exact packaged Code OSS verification remains required after this focused PR is
merged and the WebScene pin is advanced. That run owns the final zero-error
Monaco trace and editor geometry comparison.

## Exact-package follow-up

The first exact package after #866 reached the extension-host Ready and
Initialized states, but Monaco still raised `WrongDocumentError` when
`_detachRange()` selected its `_textRangeRestingSpot`. That node is created by
`document.createElement('div')` and deliberately never connected. WebScene had
been treating connectivity to the document root as document ownership.

Document factory calls now record the receiver document independently of the
parent tree. Range validation follows that identity, so a never-connected node
and its descendants remain valid endpoints while a node created by another
Document still fails atomically. Detached-subtree collection and navigation
clear the ownership records with the corresponding native nodes.
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,7 @@
std::erase_if(detached_document_roots, [&](const auto id) {
return node_ids.contains(static_cast<uint32_t>(id));
});
for (const auto id : node_ids) node_document_roots.erase(id);
std::erase_if(hidden_frame_document_roots, [&](const auto id) {
return node_ids.contains(id);
});
Expand Down Expand Up @@ -4944,6 +4945,7 @@
"html-element");
auto& node = self->document.create_element(std::move(tag));
node.xml_mode = xml_mode;
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
self->apply_css_rules(node);
info.GetReturnValue().Set(self->wrap_node(node));
}
Expand All @@ -4959,6 +4961,12 @@
{},
"html-element");
auto& node = self->document.create_element(std::move(tag));
auto* document_root = info.This()->InternalFieldCount() > 0
? static_cast<dom_node*>(info.This()->GetAlignedPointerFromInternalField(
0,
v8::kEmbedderDataTypeTagDefault))
: nullptr;
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
node.set_namespace(info.Length() > 0
? to_utf8(info.GetIsolate(), info[0])
: "http://www.w3.org/2000/svg");
Expand All @@ -4976,6 +4984,7 @@
v8::kEmbedderDataTypeTagDefault))
: nullptr;
node.xml_mode = document_root != nullptr && document_root->xml_mode;
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
node.text_content = info.Length() > 0 ? to_wtf8(info.GetIsolate(), info[0]) : std::string{};
info.GetReturnValue().Set(self->wrap_node(node));
}
Expand Down Expand Up @@ -5007,6 +5016,7 @@
v8::kEmbedderDataTypeTagDefault))
: nullptr;
node.xml_mode = document_root != nullptr && document_root->xml_mode;
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
node.visible = false;
node.text_content = info.Length() > 0 ? to_wtf8(info.GetIsolate(), info[0]) : std::string{};
info.GetReturnValue().Set(self->wrap_node(node));
Expand Down Expand Up @@ -5410,6 +5420,7 @@
v8::kEmbedderDataTypeTagDefault))
: nullptr;
node.xml_mode = document_root != nullptr && document_root->xml_mode;
if (document_root != nullptr) self->bind_node_to_document(node, *document_root);
node.visible = false;
info.GetReturnValue().Set(self->wrap_node(node));
}
Expand Down Expand Up @@ -5491,6 +5502,12 @@
const auto deep = info.Length() > 1
&& info[1]->BooleanValue(info.GetIsolate());
auto& clone = clone_dom_subtree(*self, *source, deep);
auto* document_root = info.This()->InternalFieldCount() > 0
? static_cast<dom_node*>(info.This()->GetAlignedPointerFromInternalField(
0,
v8::kEmbedderDataTypeTagDefault))
: nullptr;
if (document_root != nullptr) self->bind_node_to_document(clone, *document_root);
info.GetReturnValue().Set(self->wrap_node(clone));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,29 @@
binding_callback_timer binding_timer(self->profile(binding_category::dom_property));
auto isolate = info.GetIsolate();
auto* node = unwrap_node(info.Holder());
if (node != nullptr) {
const auto owner = self->node_document_roots.find(node->id);
auto* root = owner == self->node_document_roots.end()
? nullptr : self->document.find_by_native_id(owner->second);
if (root != nullptr && self->detached_document_roots.contains(root->id)) {
info.GetReturnValue().Set(wrap_detached_document(
*self,
*root,
isolate->GetCurrentContext(),
"about:blank"));
return;
}
if (root != nullptr && root->parent != nullptr
&& root->parent->tag == "iframe") {
auto* frame = root->parent;
const auto actual = self->actual_frame_documents.find(frame->id);
info.GetReturnValue().Set(
actual != self->actual_frame_documents.end()
? actual->second.Get(isolate)
: self->frame_document(*frame));
return;
}
}
for (auto* ancestor = node; ancestor != nullptr; ancestor = ancestor->parent) {
if (self->detached_document_roots.contains(ancestor->id)) {
info.GetReturnValue().Set(wrap_detached_document(
Expand Down Expand Up @@ -1976,6 +1999,33 @@
return root == nullptr ? &active_root() : root;
}

uint32_t document_root_id_for_node(const dom_node& node)
{
for (auto* current_node = &node; current_node != nullptr;
current_node = document.dom_parent(*current_node)) {
if (const auto known = node_document_roots.find(current_node->id);
known != node_document_roots.end()) {
return known->second;
}
if (detached_document_roots.contains(current_node->id)) {
return current_node->id;
}
if (current_node == &active_root()) return current_node->id;
if (const auto* parent = document.dom_parent(*current_node);
parent != nullptr && parent->tag == "iframe") {
return current_node->id;
}
}
return 0U;
}

void bind_node_to_document(dom_node& node, const dom_node& root)
{
visit_subtree(node, [&](dom_node& descendant) {
node_document_roots[descendant.id] = root.id;
});
}

bool range_binding_is_live(const range_binding& binding) const
{
return !binding.released
Expand All @@ -1985,8 +2035,9 @@
&& document.find_by_native_id(binding.root_id) != nullptr;
}

bool range_node_is_in_root(const dom_node& root, const dom_node& node) const
bool range_node_is_in_root(const dom_node& root, const dom_node& node)
{
if (document_root_id_for_node(node) == root.id) return true;
for (auto* current_node = &node; current_node != nullptr;
current_node = document.dom_parent(*current_node)) {
if (current_node == &root) return true;
Expand Down Expand Up @@ -2354,9 +2405,15 @@
static void range_create_contextual_fragment(const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* self = current(info.GetIsolate());
auto* binding = self == nullptr ? nullptr : self->require_live_range(info);
if (binding == nullptr) return;
const auto html = info.Length() > 0 ? to_utf8(info.GetIsolate(), info[0]) : std::string{};
if (!html.empty() && !self->require_runtime_html()) return;
auto& fragment_root = self->document.create_element("#fragment");
if (const auto* root = self->document.find_by_native_id(binding->root_id);
root != nullptr) {
self->bind_node_to_document(fragment_root, *root);
}
self->parse_inner_html(fragment_root, html);
// A contextual fragment is a real queryable DOM node. Reuse the native
// node wrapper so querySelector(All), children and removeChild all have
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@
next_adopted_stylesheet_owner_id = 0xf0000000U;
detached_documents.clear();
detached_document_roots.clear();
node_document_roots.clear();
node_wrappers.clear();
attr_wrappers.clear();
attached_attributes.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@
bool service_worker_realm{false};
std::unordered_set<uint64_t> detached_document_roots;
std::unordered_map<uint64_t, v8::Global<v8::Object>> detached_documents;
// DOM ownership survives removal from the tree. Keep the creating/adopting
// document separate from parent links so detached nodes remain valid Range
// endpoints, as required by Monaco's reusable measurement Range.
std::unordered_map<uint32_t, uint32_t> node_document_roots;
std::unordered_map<uint32_t, window_selection_state> window_selections;
static constexpr size_t maximum_live_ranges = 4096U;
std::vector<std::unique_ptr<range_binding>> range_bindings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2203,6 +2203,30 @@ void test_live_range_boundary_contract(webscene_engine* engine)
parsedRange.endContainer === parsedTarget,
parsedRange.endOffset
];
// Monaco creates this resting node with document.createElement(),
// never connects it, and selects it from _detachRange() in finally.
const restingSpot = document.createElement('div');
const monacoRange = document.createRange();
monacoRange.selectNodeContents(restingSpot);
const detachedMainSelected = [
restingSpot.ownerDocument === document,
monacoRange.startContainer === restingSpot,
monacoRange.startOffset,
monacoRange.endContainer === restingSpot,
monacoRange.endOffset
];
const parsedRestingSpot = parsed.createElement('div');
parsedRestingSpot.textContent = 'rest';
parsedRange.selectNodeContents(parsedRestingSpot);
const detachedParsedSelected = [
parsedRestingSpot.ownerDocument === parsed,
parsedRange.startContainer === parsedRestingSpot,
parsedRange.endContainer === parsedRestingSpot,
parsedRange.endOffset
];
let detachedWrongDocument = '';
try { monacoRange.selectNodeContents(parsedRestingSpot); }
catch (error) { detachedWrongDocument = error.name; }
const reuseStarted = performance.now();
for (let cycle = 0; cycle < 10000; ++cycle) {
parsedRange.setStart(parsedText, cycle % 4);
Expand All @@ -2214,13 +2238,14 @@ void test_live_range_boundary_contract(webscene_engine* engine)
return {
cross, marker, selected, collapsedForward, collapsedBackward,
invalid, parsedBefore, wrongDocument, parsedAtomic, parsedSelected,
detachedMainSelected, detachedParsedSelected, detachedWrongDocument,
reuseCycles: 10000,
reuseWithinBudget: reuseDuration <= 2000
};
})()
)JS", "native-live-range-boundaries.js");
require(
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})",
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})",
"live Range boundary contract regressed: " + result);
std::cout << "Range document-owner reuse gate: cycles=10000 durationMs="
<< evaluate(engine, "__rangeDocumentOwnerReuseDuration",
Expand Down
Loading