use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::backend::AnyPage;
use crate::element_handle::ElementHandle;
use crate::error::{FerriError, Result};
use crate::page::Page;
use crate::protocol::HandleId;
#[derive(Debug, Clone)]
pub enum HandleRemote {
Cdp(Arc<str>),
Bidi { shared_id: String, handle: Option<String> },
WebKit(Arc<str>),
}
#[derive(Debug, Clone)]
pub enum JSHandleBacking {
Remote(HandleRemote),
Value(crate::protocol::SerializedValue),
}
#[derive(Debug, Clone)]
pub enum EvaluateResult {
Value(crate::protocol::SerializedValue),
Handle(JSHandleBacking, bool),
}
impl HandleRemote {
#[must_use]
pub fn to_handle_id(&self) -> HandleId {
match self {
Self::Cdp(obj) => HandleId::Cdp((**obj).to_string()),
Self::Bidi { shared_id, handle } => HandleId::Bidi {
shared_id: shared_id.clone(),
handle: handle.clone(),
},
Self::WebKit(obj) => HandleId::WebKit((**obj).to_string()),
}
}
#[must_use]
pub fn from_handle_id(id: HandleId) -> Self {
match id {
HandleId::Cdp(obj) => Self::Cdp(Arc::from(obj)),
HandleId::Bidi { shared_id, handle } => Self::Bidi { shared_id, handle },
HandleId::WebKit(obj) => Self::WebKit(Arc::from(obj)),
}
}
}
impl JSHandleBacking {
#[must_use]
pub fn to_serialized_argument(&self) -> crate::protocol::SerializedArgument {
match self {
Self::Remote(remote) => crate::protocol::SerializedArgument {
value: crate::protocol::SerializedValue::Handle(0),
handles: vec![remote.to_handle_id()],
},
Self::Value(v) => crate::protocol::SerializedArgument {
value: v.clone(),
handles: Vec::new(),
},
}
}
}
#[derive(Clone)]
pub struct JSHandle {
page: Arc<Page>,
backing: JSHandleBacking,
disposed: Arc<AtomicBool>,
is_node: bool,
}
impl JSHandle {
pub(crate) fn new(page: Arc<Page>, remote: HandleRemote) -> Self {
Self::from_backing(page, JSHandleBacking::Remote(remote), true)
}
pub(crate) fn from_backing(page: Arc<Page>, backing: JSHandleBacking, is_node: bool) -> Self {
Self {
page,
backing,
disposed: Arc::new(AtomicBool::new(false)),
is_node,
}
}
#[must_use]
pub fn page(&self) -> &Arc<Page> {
&self.page
}
#[must_use]
pub fn remote(&self) -> Option<&HandleRemote> {
match &self.backing {
JSHandleBacking::Remote(r) => Some(r),
JSHandleBacking::Value(_) => None,
}
}
#[must_use]
pub fn value(&self) -> Option<&crate::protocol::SerializedValue> {
match &self.backing {
JSHandleBacking::Value(v) => Some(v),
JSHandleBacking::Remote(_) => None,
}
}
#[must_use]
pub fn backing(&self) -> &JSHandleBacking {
&self.backing
}
#[must_use]
pub fn is_disposed(&self) -> bool {
self.disposed.load(Ordering::SeqCst)
}
pub(crate) fn any_page(&self) -> &AnyPage {
self.page.inner()
}
fn claim_dispose(&self) -> bool {
self
.disposed
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
}
pub async fn dispose(&self) -> Result<()> {
if !self.claim_dispose() {
return Ok(());
}
let JSHandleBacking::Remote(remote) = &self.backing else {
return Ok(());
};
let result = self.any_page().release_handle(remote).await;
if result.is_err() {
self.disposed.store(false, Ordering::SeqCst);
}
result
}
pub async fn evaluate(
&self,
fn_source: &str,
user_arg: crate::protocol::SerializedArgument,
is_function: Option<bool>,
) -> Result<crate::protocol::SerializedValue> {
if self.is_disposed() {
return Err(disposed_error());
}
let (args, handles) = build_handle_evaluate_args(&self.backing, user_arg);
let result = self
.any_page()
.call_utility_evaluate(fn_source, &args, &handles, None, is_function, true)
.await?;
match result {
EvaluateResult::Value(v) => Ok(v),
EvaluateResult::Handle(..) => Err(crate::error::FerriError::Evaluation(
"JSHandle::evaluate: backend returned handle in returnByValue=true mode".into(),
)),
}
}
pub async fn evaluate_handle(
&self,
fn_source: &str,
user_arg: crate::protocol::SerializedArgument,
is_function: Option<bool>,
) -> Result<JSHandle> {
if self.is_disposed() {
return Err(disposed_error());
}
let (args, handles) = build_handle_evaluate_args(&self.backing, user_arg);
let result = self
.any_page()
.call_utility_evaluate(fn_source, &args, &handles, None, is_function, false)
.await?;
match result {
EvaluateResult::Handle(backing, is_node) => Ok(JSHandle::from_backing(Arc::clone(&self.page), backing, is_node)),
EvaluateResult::Value(_) => Err(crate::error::FerriError::Evaluation(
"JSHandle::evaluate_handle: backend returned value in returnByValue=false mode".into(),
)),
}
}
pub async fn json_value(&self) -> Result<crate::protocol::SerializedValue> {
if let Some(v) = self.value() {
return Ok(v.clone());
}
self
.evaluate("h => h", crate::protocol::SerializedArgument::default(), Some(true))
.await
}
pub async fn get_property(&self, name: &str) -> Result<JSHandle> {
let escaped =
serde_json::to_string(name).map_err(|e| FerriError::Backend(format!("getProperty name escape: {e}")))?;
let expr = format!("h => h[{escaped}]");
self
.evaluate_handle(&expr, crate::protocol::SerializedArgument::default(), Some(true))
.await
}
pub async fn get_properties(&self) -> Result<Vec<(String, JSHandle)>> {
use crate::protocol::SerializedValue;
let keys_value = self
.evaluate(
"h => (h && typeof h === 'object') ? Object.keys(h) : []",
crate::protocol::SerializedArgument::default(),
Some(true),
)
.await?;
let keys: Vec<String> = match keys_value {
SerializedValue::Array { items, .. } => items
.into_iter()
.filter_map(|v| match v {
SerializedValue::Str(s) => Some(s),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let mut out = Vec::with_capacity(keys.len());
for key in keys {
let handle = self.get_property(&key).await?;
out.push((key, handle));
}
Ok(out)
}
#[must_use]
pub fn as_element(&self) -> Option<ElementHandle> {
if self.is_disposed() || !self.is_node {
return None;
}
let remote = self.remote()?;
let any_element = crate::backend::element_from_remote(self.any_page(), remote).ok()?;
Some(ElementHandle::from_js_handle_and_element(self.clone(), any_element))
}
}
impl std::fmt::Debug for JSHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JSHandle")
.field("backing", &self.backing)
.field("disposed", &self.is_disposed())
.finish_non_exhaustive()
}
}
fn build_handle_evaluate_args(
receiver: &JSHandleBacking,
user_arg: crate::protocol::SerializedArgument,
) -> (Vec<crate::protocol::SerializedValue>, Vec<crate::protocol::HandleId>) {
let crate::protocol::SerializedArgument {
value: user_value,
handles: user_handles,
} = user_arg;
if let JSHandleBacking::Value(inline) = receiver {
let args = vec![inline.clone(), user_value];
return (args, user_handles);
}
let JSHandleBacking::Remote(remote) = receiver else {
unreachable!("JSHandleBacking has only Remote and Value variants");
};
let shifted_user_value = shift_handle_indices(user_value, 1);
let args = vec![crate::protocol::SerializedValue::handle(0), shifted_user_value];
let mut handles = Vec::with_capacity(1 + user_handles.len());
handles.push(remote.to_handle_id());
handles.extend(user_handles);
(args, handles)
}
fn shift_handle_indices(value: crate::protocol::SerializedValue, offset: u32) -> crate::protocol::SerializedValue {
use crate::protocol::{PropertyEntry, SerializedValue};
match value {
SerializedValue::Handle(i) => SerializedValue::Handle(i + offset),
SerializedValue::Array { id, items } => SerializedValue::Array {
id,
items: items.into_iter().map(|v| shift_handle_indices(v, offset)).collect(),
},
SerializedValue::Object { id, entries } => SerializedValue::Object {
id,
entries: entries
.into_iter()
.map(|e| PropertyEntry {
k: e.k,
v: shift_handle_indices(e.v, offset),
})
.collect(),
},
other => other,
}
}
pub(crate) fn disposed_error() -> FerriError {
FerriError::TargetClosed {
reason: Some("JSHandle is disposed".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handle_remote_roundtrips_through_handle_id() {
let cases = [
HandleRemote::Cdp(Arc::from("obj-42")),
HandleRemote::Bidi {
shared_id: "shared-42".into(),
handle: Some("h-1".into()),
},
HandleRemote::Bidi {
shared_id: "shared-43".into(),
handle: None,
},
HandleRemote::WebKit(Arc::from("obj-42")),
];
for original in cases {
let id = original.to_handle_id();
let back = HandleRemote::from_handle_id(id);
assert_eq!(format!("{original:?}"), format!("{back:?}"));
}
}
#[test]
fn disposed_error_message_matches_playwright() {
let e = disposed_error();
assert!(e.to_string().contains("JSHandle is disposed"), "message drift: {e}");
assert_eq!(e.name(), "TargetClosedError");
}
}