refactor(cdp): share node lookups with explicit chain execution

This commit is contained in:
ldm0
2026-10-01 16:51:46 +08:00
committed by Donough Liu
parent 6e30806c72
commit 117dce412b
11 changed files with 262 additions and 236 deletions
@@ -1,4 +1,5 @@
use crate::conn::CdpConnection;
pub(super) use crate::domains::native::NodeReferenceParams;
pub(super) use chromiumoxide_cdp::cdp::browser_protocol::accessibility::{
GetChildAxNodesParams as ChildAxNodesParams, GetFullAxTreeParams,
};
@@ -12,17 +13,6 @@ pub(super) struct FrameScopedParams {
pub(super) frame_id: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(super) struct NodeReferenceParams {
#[serde(default)]
pub(super) node_id: Option<u32>,
#[serde(default)]
pub(super) backend_node_id: Option<u32>,
#[serde(default)]
pub(super) object_id: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct AncestorsParams {
@@ -1,9 +1,9 @@
use super::*;
use crate::domains::native::{self, NativeCommandStep};
use crate::devtools_runtime::DevToolsDomNodeReference;
use crate::domains::native::{self, NativeCommandStep, NodeLookupExecution};
use moli_core::{
RendererNativeOperation as Operation, RendererNativeOperationStep as Step,
RendererNativeProtocolResponse as Response, RendererPageCommand as Command,
RendererPageReply as Reply,
RendererNativeOperation as Operation, RendererNativeProtocolResponse as Response,
RendererPageCommand as Command, RendererPageReply as Reply,
};
pub(crate) fn try_start(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> Option<NativeCommandStep> {
@@ -231,24 +231,11 @@ fn reference_operation(
let frontend_node_id = reference.node_id.ok_or_else(StartError::node_not_found)?;
let inspector_session_id =
conn.target_renderer_runtime_inspector_session_id_for_session(cmd.session_id);
Ok(Operation::then_on_nested_main(
Command::DocumentFrontendNodeBinding {
inspector_session_id,
frontend_node_id,
},
move |reply| match reply {
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::BackendNodeId(id),
)) => Step::Continue(backend_operation(id, frame_id, top_frame_id, operation)),
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::NotFound,
)) => Step::Complete(node_not_found()),
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not resolve frontend node binding: {error}"),
)),
_ => unreachable!("accessibility frontend-node lookup reply"),
},
Ok(native::with_backend_node(
inspector_session_id,
DevToolsDomNodeReference::FrontendNodeId(frontend_node_id),
NodeLookupExecution::NestedMain,
move |id| backend_operation(id, frame_id, top_frame_id, operation),
))
}
+1 -12
View File
@@ -3,6 +3,7 @@ pub(crate) mod native;
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
use crate::domains::actions::CssAction;
use crate::domains::command_output::CommandOutputPlan;
use crate::domains::native::NodeReferenceParams;
use chromiumoxide_cdp::cdp::browser_protocol::css::{
GetStyleSheetTextParams as StyleSheetIdParams, SetStyleSheetTextParams,
};
@@ -10,23 +11,11 @@ use moli_core::page::{
CompletedPageCommand, Page, PendingPageCommand, RendererDocumentNodeAttributesResolution,
};
use moli_css_parse::{DeclarationParseOptions, parse_declaration_list};
use serde::Deserialize;
use serde_json::{Value, json};
mod node_references;
mod style_sheets;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct NodeReferenceParams {
#[serde(default)]
node_id: Option<u32>,
#[serde(default)]
backend_node_id: Option<u32>,
#[serde(default)]
object_id: Option<String>,
}
pub(crate) struct PendingCssCommandDispatch {
command_id: Option<u64>,
owner_scope: CommandOwnerScope,
+9 -27
View File
@@ -1,9 +1,9 @@
use super::*;
use crate::domains::native::{self, NativeCommandStep};
use crate::devtools_runtime::DevToolsDomNodeReference;
use crate::domains::native::{self, NativeCommandStep, NodeLookupExecution};
use moli_core::{
RendererNativeOperation as Operation, RendererNativeOperationStep as Step,
RendererNativeProtocolResponse as Response, RendererPageCommand as Command,
RendererPageReply as Reply,
RendererNativeOperation as Operation, RendererNativeProtocolResponse as Response,
RendererPageCommand as Command, RendererPageReply as Reply,
};
pub(crate) fn try_start(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> Option<NativeCommandStep> {
@@ -133,29 +133,11 @@ pub(crate) fn try_start(conn: &mut CdpConnection, cmd: &Cmd<'_>) -> Option<Nativ
move |reply| project_style(reply, query),
)
} else if let Some(frontend_node_id) = params.node_id {
Operation::then_on_nested_main(
Command::DocumentFrontendNodeBinding {
inspector_session_id,
frontend_node_id,
},
move |reply| match reply {
Ok(Reply::DocumentFrontendNodeBinding(resolution)) => {
match node_references::backend_node_id_from_frontend_resolution(
resolution,
) {
Some(id) => Step::Continue(backend_style_operation(id, query)),
None => Step::Complete(Response::error(
-32000,
"Could not find node with given id",
)),
}
}
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not resolve frontend node binding: {error}"),
)),
_ => unreachable!("CSS frontend-node lookup reply"),
},
native::with_backend_node(
inspector_session_id,
DevToolsDomNodeReference::FrontendNodeId(frontend_node_id),
NodeLookupExecution::NestedMain,
move |id| backend_style_operation(id, query),
)
} else if let Some(backend_node_id) = params.backend_node_id {
backend_style_operation(backend_node_id, query)
@@ -1,17 +1,5 @@
use serde::Deserialize;
use crate::devtools_runtime::DevToolsDomNodeReference;
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(super) struct NodeReferenceParams {
#[serde(default)]
pub(super) node_id: Option<u32>,
#[serde(default)]
pub(super) backend_node_id: Option<u32>,
#[serde(default)]
pub(super) object_id: Option<String>,
}
pub(super) use crate::domains::native::NodeReferenceParams;
pub(super) fn devtools_node_reference_from_ids(
node_id: Option<u32>,
+75 -82
View File
@@ -1,8 +1,10 @@
use super::*;
use crate::domains::native::{self, NativeCommandStep};
use moli_core::page::{
RendererDomFrontendNodeBindingResolution, RendererDomSearchResultsResolution,
use crate::domains::native::{
self, NativeCommandStep,
NodeLookupExecution::{NestedMain, OwnerTurn},
with_backend_node as with_backend,
};
use moli_core::page::RendererDomSearchResultsResolution;
use moli_core::{
RendererNativeOperation as Operation, RendererNativeOperationStep as Step,
RendererNativeProtocolNotification as Notification, RendererNativeProtocolResponse as Response,
@@ -84,7 +86,7 @@ fn prepare(
return remote_object::prepare(conn, cmd);
}
if action == DomAction::SetFileInputFiles {
return files::prepare(conn, cmd).map(Operation::require_owner_turn);
return files::prepare(conn, cmd);
}
if mutation::handles(action) {
return mutation::prepare(conn, cmd, action);
@@ -146,23 +148,28 @@ fn prepare(
}
DomAction::GetAttributes => {
let params = build_cdp_get_attributes_command(conn, cmd)?;
Ok(with_backend(session, params.reference, |backend_node_id| {
Operation::new(
Command::DocumentNodeAttributesForBackendNodeId { backend_node_id },
|reply| match reply {
Ok(Reply::DocumentNodeAttributesResolution(resolution)) => {
match attributes_result_from_renderer_resolution(resolution) {
Ok(result) => Response::success(
json!({"attributes": result.attributes.into_iter().flat_map(|attr| [attr.name, attr.value]).collect::<Vec<_>>()}),
),
Err(error) => Response::error(error.code, error.message),
Ok(with_backend(
session,
params.reference,
NestedMain,
|backend_node_id| {
Operation::new(
Command::DocumentNodeAttributesForBackendNodeId { backend_node_id },
|reply| match reply {
Ok(Reply::DocumentNodeAttributesResolution(resolution)) => {
match attributes_result_from_renderer_resolution(resolution) {
Ok(result) => Response::success(
json!({"attributes": result.attributes.into_iter().flat_map(|attr| [attr.name, attr.value]).collect::<Vec<_>>()}),
),
Err(error) => Response::error(error.code, error.message),
}
}
}
Err(error) => Response::error(-32000, error.to_string()),
_ => unreachable!("DOM attributes reply"),
},
)
}))
Err(error) => Response::error(-32000, error.to_string()),
_ => unreachable!("DOM attributes reply"),
},
)
},
))
}
DomAction::QuerySelector | DomAction::QuerySelectorAll => {
let params =
@@ -181,24 +188,29 @@ fn prepare(
),
Some(reference) => {
let frontend = matches!(reference, DevToolsDomNodeReference::FrontendNodeId(_));
with_backend(session.clone(), reference, move |root_backend_node_id| {
let command = if frontend {
Command::DocumentQuerySelectorWithChildNodeSnapshotEventsForBackendNodeId {
with_backend(
session.clone(),
reference,
NestedMain,
move |root_backend_node_id| {
let command = if frontend {
Command::DocumentQuerySelectorWithChildNodeSnapshotEventsForBackendNodeId {
inspector_session_id: session, include_whitespace: whitespace, root_backend_node_id, selector, multiple,
}
} else {
Command::DocumentQuerySelectorForBackendNodeId {
inspector_session_id: session,
include_whitespace: whitespace,
root_backend_node_id,
selector,
multiple,
}
};
Operation::new(command, move |reply| {
project_query(reply, multiple, top_frame)
})
})
} else {
Command::DocumentQuerySelectorForBackendNodeId {
inspector_session_id: session,
include_whitespace: whitespace,
root_backend_node_id,
selector,
multiple,
}
};
Operation::new(command, move |reply| {
project_query(reply, multiple, top_frame)
})
},
)
}
})
}
@@ -212,6 +224,7 @@ fn prepare(
Ok(with_backend(
session.clone(),
params.reference,
NestedMain,
move |backend_node_id| {
Operation::new(
Command::DocumentChildNodeSnapshotEventsForBackendNodeId {
@@ -299,6 +312,7 @@ fn prepare(
Ok(with_backend(
session.clone(),
reference,
NestedMain,
move |backend_node_id| {
Operation::new(
Command::DocumentNodeSnapshotForBackendNodeIdInInspectorSession {
@@ -359,15 +373,20 @@ fn prepare(
params.reference.node_id,
params.reference.backend_node_id,
) {
Ok(with_backend(session, reference, move |backend_node_id| {
Operation::new(
Command::OuterHtmlForBackendNodeId {
backend_node_id,
include_shadow_dom: params.include_shadow_dom,
},
project,
)
}))
Ok(with_backend(
session,
reference,
NestedMain,
move |backend_node_id| {
Operation::new(
Command::OuterHtmlForBackendNodeId {
backend_node_id,
include_shadow_dom: params.include_shadow_dom,
},
project,
)
},
))
} else {
Ok(Operation::new(
Command::OuterHtmlForDocument {
@@ -413,12 +432,17 @@ fn prepare(
let reference =
devtools_node_reference_from_ids(params.node_id, params.backend_node_id)
.ok_or_else(StartError::node_not_found)?;
Ok(with_backend(session, reference, move |backend_node_id| {
Operation::new(
Command::DocumentGeometryForBackendNodeId { backend_node_id },
project,
)
}))
Ok(with_backend(
session,
reference,
NestedMain,
move |backend_node_id| {
Operation::new(
Command::DocumentGeometryForBackendNodeId { backend_node_id },
project,
)
},
))
}
}
DomAction::PushNodesByBackendIdsToFrontend => {
@@ -516,37 +540,6 @@ fn prepare(
}
}
fn with_backend(
session: Option<String>,
reference: DevToolsDomNodeReference,
next: impl FnOnce(u32) -> Operation + Send + 'static,
) -> Operation {
match reference {
DevToolsDomNodeReference::BackendNodeId(id) => next(id),
DevToolsDomNodeReference::FrontendNodeId(frontend_node_id) => {
Operation::then_on_nested_main(
Command::DocumentFrontendNodeBinding {
inspector_session_id: session,
frontend_node_id,
},
move |reply| match reply {
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::BackendNodeId(id),
)) => Step::Continue(next(id)),
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::NotFound,
)) => Step::Complete(node_not_found()),
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not resolve frontend node binding: {error}"),
)),
_ => unreachable!("DOM node binding reply"),
},
)
}
}
}
fn node_not_found() -> Response {
Response::error(-32000, "Could not find node with given id")
}
@@ -33,38 +33,47 @@ pub(super) fn prepare(conn: &CdpConnection, cmd: &Cmd<'_>) -> Result<Operation,
params.reference.backend_node_id,
)
.ok_or_else(StartError::invalid_params)?;
Ok(with_backend(session, reference, move |backend_node_id| {
Operation::then_on_nested_main(
Command::DocumentNodeSnapshotForBackendNodeId {
backend_node_id,
depth: 0,
pierce: false,
},
move |reply| match reply {
Ok(Reply::OptionalDocumentNodeObjectSnapshot(snapshot)) if snapshot.is_some() => {
match files {
Ok(files) => Step::Continue(Operation::new(
Command::SetFileInputFilesForBackendNodeId {
backend_node_id,
files,
append: false,
},
project,
)),
Err(error) => Step::Complete(Response::error(error.code, error.message)),
Ok(with_backend(
session,
reference,
OwnerTurn,
move |backend_node_id| {
Operation::then(
Command::DocumentNodeSnapshotForBackendNodeId {
backend_node_id,
depth: 0,
pierce: false,
},
move |reply| match reply {
Ok(Reply::OptionalDocumentNodeObjectSnapshot(snapshot))
if snapshot.is_some() =>
{
match files {
Ok(files) => Step::Continue(Operation::new(
Command::SetFileInputFilesForBackendNodeId {
backend_node_id,
files,
append: false,
},
project,
)),
Err(error) => {
Step::Complete(Response::error(error.code, error.message))
}
}
}
}
Ok(Reply::OptionalDocumentNodeObjectSnapshot(_)) => {
Step::Complete(node_not_found())
}
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not preflight file input node: {error}"),
)),
_ => unreachable!("DOM file input preflight reply"),
},
)
}))
Ok(Reply::OptionalDocumentNodeObjectSnapshot(_)) => {
Step::Complete(node_not_found())
}
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not preflight file input node: {error}"),
)),
_ => unreachable!("DOM file input preflight reply"),
},
)
},
))
}
fn project(reply: anyhow::Result<Reply>) -> Response {
@@ -43,20 +43,26 @@ pub(super) fn prepare(
// The first step only resolves a native node binding, but removal
// enters V8. Keep the complete chain on its owner turn; agent-only
// operations below inherit their backend entry requirement.
Ok(with_backend(session, params.reference, |backend_node_id| {
Operation::new(
Command::RemoveDocumentBackendNodeId { backend_node_id },
|reply| match reply {
Ok(Reply::Bool(true)) => Response::success(json!({})),
Ok(Reply::Bool(false)) => Response::error(-32000, "Could not remove node"),
Err(error) => {
Response::error(-32000, format!("Could not remove node: {error}"))
}
_ => unreachable!("DOM remove node reply"),
},
)
})
.require_owner_turn())
Ok(with_backend(
session,
params.reference,
OwnerTurn,
|backend_node_id| {
Operation::new(
Command::RemoveDocumentBackendNodeId { backend_node_id },
|reply| match reply {
Ok(Reply::Bool(true)) => Response::success(json!({})),
Ok(Reply::Bool(false)) => {
Response::error(-32000, "Could not remove node")
}
Err(error) => {
Response::error(-32000, format!("Could not remove node: {error}"))
}
_ => unreachable!("DOM remove node reply"),
},
)
},
))
}
DomAction::Focus => {
let params: NodeReferenceParams = cmd
@@ -83,13 +89,17 @@ pub(super) fn prepare(
} else {
"No node found for given backend id"
};
Ok(with_backend(session, reference, move |backend_node_id| {
Operation::new(
Command::FocusDocumentBackendNode { backend_node_id },
move |reply| focus(reply, missing),
)
})
.require_owner_turn())
Ok(with_backend(
session,
reference,
OwnerTurn,
move |backend_node_id| {
Operation::new(
Command::FocusDocumentBackendNode { backend_node_id },
move |reply| focus(reply, missing),
)
},
))
}
}
DomAction::SetAttributeValue | DomAction::RemoveAttribute => {
@@ -121,6 +131,7 @@ pub(super) fn prepare(
Ok(with_backend(
session,
DevToolsDomNodeReference::FrontendNodeId(id),
OwnerTurn,
move |backend_node_id| {
Operation::new(
Command::MutateDocumentBackendNodeAttribute {
@@ -130,8 +141,7 @@ pub(super) fn prepare(
attribute,
)
},
)
.require_owner_turn())
))
}
DomAction::MoveTo
| DomAction::SetAttributesAsText
@@ -166,16 +176,20 @@ pub(super) fn prepare(
params.reference.backend_node_id,
)
.ok_or_else(StartError::node_not_found)?;
Ok(with_backend(session, reference, move |backend_node_id| {
Operation::new(
Command::ScrollBackendNodeIntoViewIfNeeded {
backend_node_id,
rect,
},
scroll,
)
})
.require_owner_turn())
Ok(with_backend(
session,
reference,
OwnerTurn,
move |backend_node_id| {
Operation::new(
Command::ScrollBackendNodeIntoViewIfNeeded {
backend_node_id,
rect,
},
scroll,
)
},
))
}
}
DomAction::GetNodeForLocation => {
@@ -12,8 +12,11 @@ pub(super) fn prepare(conn: &CdpConnection, cmd: &Cmd<'_>) -> Result<Operation,
let session = conn.target_renderer_runtime_inspector_session_id_for_owner(&owner);
let top_frame = top_frame_id_for_owner(conn, &owner);
let whitespace = dom_agent_includes_whitespace_for_owner(conn, &owner);
Ok(
with_backend(session.clone(), params.reference, move |backend_node_id| {
Ok(with_backend(
session.clone(),
params.reference,
OwnerTurn,
move |backend_node_id| {
let prepare = move |cache| {
resolve(
session,
@@ -46,9 +49,8 @@ pub(super) fn prepare(conn: &CdpConnection, cmd: &Cmd<'_>) -> Result<Operation,
} else {
prepare(true)
}
})
.require_owner_turn(),
)
},
))
}
fn resolve(
+3
View File
@@ -4,6 +4,9 @@
//! Its wake-up order cannot decide when a successful reply reaches the client.
//! Internal queries deliberately keep their ordinary typed result channel.
mod node;
pub(crate) use node::{NodeLookupExecution, NodeReferenceParams, with_backend_node};
use serde_json::json;
use moli_core::page::{CompletedPageCommand, PendingPageCommand};
+69
View File
@@ -0,0 +1,69 @@
use moli_core::page::RendererDomFrontendNodeBindingResolution;
use moli_core::{
RendererNativeOperation as Operation, RendererNativeOperationStep as Step,
RendererNativeProtocolResponse as Response, RendererPageCommand as Command,
RendererPageReply as Reply,
};
use serde::Deserialize;
use crate::devtools_runtime::DevToolsDomNodeReference;
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NodeReferenceParams {
#[serde(default)]
pub(crate) node_id: Option<u32>,
#[serde(default)]
pub(crate) backend_node_id: Option<u32>,
#[serde(default)]
pub(crate) object_id: Option<String>,
}
/// The requirement covers the complete lookup and continuation, not merely
/// the native binding query. Each domain retains its own selector precedence.
pub(crate) enum NodeLookupExecution {
OwnerTurn,
NestedMain,
}
pub(crate) fn with_backend_node(
session: Option<String>,
reference: DevToolsDomNodeReference,
execution: NodeLookupExecution,
next: impl FnOnce(u32) -> Operation + Send + 'static,
) -> Operation {
match reference {
DevToolsDomNodeReference::BackendNodeId(id) => {
let operation = next(id);
match execution {
NodeLookupExecution::OwnerTurn => operation.require_owner_turn(),
NodeLookupExecution::NestedMain => operation,
}
}
DevToolsDomNodeReference::FrontendNodeId(frontend_node_id) => {
let command = Command::DocumentFrontendNodeBinding {
inspector_session_id: session,
frontend_node_id,
};
let continue_binding = move |reply| match reply {
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::BackendNodeId(id),
)) => Step::Continue(next(id)),
Ok(Reply::DocumentFrontendNodeBinding(
RendererDomFrontendNodeBindingResolution::NotFound,
)) => Step::Complete(Response::error(-32000, "Could not find node with given id")),
Err(error) => Step::Complete(Response::error(
-32000,
format!("Could not resolve frontend node binding: {error}"),
)),
_ => unreachable!("native frontend-node lookup reply"),
};
match execution {
NodeLookupExecution::OwnerTurn => Operation::then(command, continue_binding),
NodeLookupExecution::NestedMain => {
Operation::then_on_nested_main(command, continue_binding)
}
}
}
}
}