refactor(webmcp): share registry notifications and invocation aborts

This commit is contained in:
ldm0
2026-10-05 15:25:06 +08:00
committed by Donough Liu
parent d6f905fbfb
commit 0923d2cefa
6 changed files with 110 additions and 93 deletions
@@ -13,6 +13,7 @@ mod events;
mod execution;
mod lifecycle;
mod navigation;
mod registry;
mod state;
mod tasks;
@@ -2,13 +2,12 @@ use super::bindings::{check_target, document_for_target, document_owner, dom_err
use super::events::queue_tool_change;
use super::state::{AbortRegistration, CallbackTool, RegisteredTool, ToolExecutor, ToolMetadata};
use super::tasks::{queue_task, remove_abort_registration, signal_reason, task_context, task_data};
use super::{conversion, devtools, execution};
use super::{conversion, devtools, execution, registry};
use crate::document_runtime::DomHandle;
use crate::native_bridge::JsContextHost;
use crate::util::context_host_ptr_from_global_bridge;
use crate::web_api_interfaces;
use crate::window_webidl_callback::WindowWebIdlCallbackFunction;
use moli_page_types::{RendererWebMcpEvent, RendererWebMcpToolId};
use crate::{abort_signal_route::ResolvedAbortSignal, webidl};
@@ -168,28 +167,15 @@ fn register_tool<'s>(
registration_resolver: Some(v8::Global::new(scope, resolver)),
}),
};
let observed = devtools::observes_tree(
unsafe { &*host_ptr },
unsafe { &*host_ptr }.native_bridge().web_mcp.documents[&document].frame_tree,
);
let (snapshot, origin, origins) = {
let entry = unsafe { &mut *host_ptr }
.native_bridge_mut()
.web_mcp
.documents
.get_mut(&document)
.expect("active ModelContext");
let snapshot = observed.then(|| devtools::protocol_tool(entry, &definition.name, &tool));
let (origin, origins) = {
let host = unsafe { &mut *host_ptr };
let origin = host.native_bridge().web_mcp.documents[&document]
.origin
.clone();
let origins = tool.exposed_to.clone();
entry.tools.insert(definition.name, tool);
(snapshot, entry.origin.clone(), origins)
registry::insert_tool(host, document, definition.name, tool);
(origin, origins)
};
if let Some(snapshot) = snapshot {
devtools::emit(
unsafe { &*host_ptr },
RendererWebMcpEvent::ToolsAdded(vec![snapshot]),
);
}
queue_tool_change(scope, host_ptr, document, &origin, &origins);
let ack = v8::Function::builder(registration_ack_callback)
.data(data.into())
@@ -244,9 +230,9 @@ fn unregister_callback<'s>(
return;
};
let name = name.to_rust_string_lossy(scope);
let removed = {
let store = &mut unsafe { &mut *host_ptr }.native_bridge_mut().web_mcp;
let Some(entry) = store.documents.get_mut(&document) else {
let origin = {
let store = &unsafe { &*host_ptr }.native_bridge().web_mcp;
let Some(entry) = store.documents.get(&document) else {
return;
};
if entry
@@ -258,19 +244,9 @@ fn unregister_callback<'s>(
{
return;
}
entry.tools.remove(&name).map(|tool| {
(
tool,
entry.origin.clone(),
entry.frame_id.clone(),
entry.frame_tree,
)
})
entry.origin.clone()
};
if let Some((tool, origin, frame_id, tree)) = removed {
devtools::emit_in_tree(unsafe { &*host_ptr }, tree, || {
RendererWebMcpEvent::ToolsRemoved(vec![RendererWebMcpToolId { frame_id, name }])
});
if let Some(tool) = registry::take_tool(unsafe { &mut *host_ptr }, document, name) {
let ToolExecutor::Callback(callback) = tool.executor else {
unreachable!("registration identity checked before removal")
};
@@ -3,11 +3,10 @@
use super::bindings::{check_target, document_owner, model_context_for_document};
use super::state::{FormInvocationState, RegisteredTool, ToolExecutor, ToolMetadata};
use super::tasks::queue_task;
use super::{devtools, events, execution};
use super::{events, execution, registry};
use crate::document_runtime::DomHandle;
use crate::native_bridge::JsContextHost;
use crate::util::context_host_ptr_from_global_bridge;
use moli_page_types::{RendererWebMcpEvent, RendererWebMcpToolId};
use crate::dom::native::{DomHost, DomMutationEffects, Node};
use std::collections::{HashMap, HashSet};
@@ -115,19 +114,10 @@ pub(crate) fn note_mutation(
}
}
for (document, name, _) in invalid {
let (frame_id, tree, origin) = {
let store = &mut host.native_bridge_mut().web_mcp;
let entry = store.documents.get_mut(&document).expect("form document");
entry.tools.remove(&name);
(
entry.frame_id.clone(),
entry.frame_tree,
entry.origin.clone(),
)
};
devtools::emit_in_tree(host, tree, || {
RendererWebMcpEvent::ToolsRemoved(vec![RendererWebMcpToolId { frame_id, name }])
});
let _ = registry::take_tool(host, document, name);
let origin = host.native_bridge().web_mcp.documents[&document]
.origin
.clone();
events::queue_form_removal_change(scope, host, document, &origin);
}
for pending in host.native_bridge_mut().web_mcp.pending.values_mut() {
@@ -541,19 +531,7 @@ fn synchronize_document(
);
}
let host = unsafe { &mut *host_ptr };
let entry = host
.native_bridge_mut()
.web_mcp
.documents
.get_mut(&document)
.expect("form document");
entry.tools.remove(&name);
let frame_id = entry.frame_id.clone();
devtools::emit_in_tree(
host,
host.native_bridge().web_mcp.documents[&document].frame_tree,
|| RendererWebMcpEvent::ToolsRemoved(vec![RendererWebMcpToolId { frame_id, name }]),
);
let _ = registry::take_tool(host, document, name);
changes += 1;
}
for definition in definitions {
@@ -565,12 +543,6 @@ fn synchronize_document(
continue;
}
let backend_node_id = host.renderer_backend_node_id_for_live_handle(definition.handle);
let observed = devtools::observes_tree(
host,
host.native_bridge().web_mcp.documents[&document].frame_tree,
);
let store = &mut host.native_bridge_mut().web_mcp;
let entry = store.documents.get_mut(&document).expect("form document");
let tool = RegisteredTool {
metadata: definition.metadata,
stack_trace: None,
@@ -581,11 +553,7 @@ fn synchronize_document(
backend_node_id,
},
};
let snapshot = observed.then(|| devtools::protocol_tool(entry, &definition.name, &tool));
entry.tools.insert(definition.name, tool);
if let Some(snapshot) = snapshot {
devtools::emit(host, RendererWebMcpEvent::ToolsAdded(vec![snapshot]));
}
registry::insert_tool(host, document, definition.name, tool);
changes += 1;
}
if changes > 0 {
@@ -515,16 +515,39 @@ pub(super) fn cancel_invocation(
result: RendererWebMcpResult::Canceled,
}
});
if let Some(resolver) = pending.resolver.take() {
let resolver = v8::Local::new(scope, &resolver);
let reason = crate::native_bridge::abort::abort_error_value(scope);
let _ = resolver.reject(scope, reason);
}
abort_target(scope, host_ptr, &pending);
abort_taken_invocation(scope, host_ptr, pending, InvocationAbortCause::Canceled);
true
}
pub(super) fn abort_target(
pub(super) enum InvocationAbortCause {
Canceled,
DocumentRetired,
}
// Call after cleanup_invocation and any protocol notification. Keeping that
// boundary preserves cancel's cleanup -> CDP -> caller rejection ordering.
pub(super) fn abort_taken_invocation(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
mut pending: PendingInvocation,
cause: InvocationAbortCause,
) {
if let Some(resolver) = pending.resolver.take() {
let resolver = v8::Local::new(scope, &resolver);
let error = match cause {
InvocationAbortCause::Canceled => crate::native_bridge::abort::abort_error_value(scope),
InvocationAbortCause::DocumentRetired => dom_error(
scope,
"UnknownError",
"The tool document is no longer active.",
),
};
let _ = resolver.reject(scope, error);
}
abort_target(scope, host_ptr, &pending);
}
fn abort_target(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
pending: &PendingInvocation,
@@ -153,15 +153,11 @@ fn retirement_callback<'s>(
}
for mut pending in invocations {
execution::cleanup_invocation(scope, host_ptr, &mut pending);
if let Some(resolver) = pending.resolver.take() {
let resolver = v8::Local::new(scope, &resolver);
let error = dom_error(
scope,
"UnknownError",
"The tool document is no longer active.",
);
let _ = resolver.reject(scope, error);
}
execution::abort_target(scope, host_ptr, &pending);
execution::abort_taken_invocation(
scope,
host_ptr,
pending,
execution::InvocationAbortCause::DocumentRetired,
);
}
}
@@ -0,0 +1,53 @@
//! Registry mutations and CDP notifications; callers own author task scheduling.
use super::devtools;
use super::state::RegisteredTool;
use crate::document_runtime::DomHandle;
use crate::native_bridge::JsContextHost;
use moli_page_types::{RendererWebMcpEvent, RendererWebMcpToolId};
pub(super) fn insert_tool(
host: &mut JsContextHost,
document: DomHandle,
name: String,
tool: RegisteredTool,
) {
let observed = devtools::observes_tree(
host,
host.native_bridge().web_mcp.documents[&document].frame_tree,
);
let snapshot = {
let entry = host
.native_bridge_mut()
.web_mcp
.documents
.get_mut(&document)
.expect("active ModelContext");
let snapshot = observed.then(|| devtools::protocol_tool(entry, &name, &tool));
entry.tools.insert(name, tool);
snapshot
};
if let Some(snapshot) = snapshot {
devtools::emit(host, RendererWebMcpEvent::ToolsAdded(vec![snapshot]));
}
}
pub(super) fn take_tool(
host: &mut JsContextHost,
document: DomHandle,
name: String,
) -> Option<RegisteredTool> {
let (tool, frame_id, tree) = {
let entry = host
.native_bridge_mut()
.web_mcp
.documents
.get_mut(&document)?;
let tool = entry.tools.remove(&name)?;
(tool, entry.frame_id.clone(), entry.frame_tree)
};
devtools::emit_in_tree(host, tree, || {
RendererWebMcpEvent::ToolsRemoved(vec![RendererWebMcpToolId { frame_id, name }])
});
Some(tool)
}