playwright_core/channel_owner.rs
1// Copyright 2024 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3//
4// Channel Owner - Base trait for all Playwright protocol objects
5//
6// Architecture Reference:
7// - Python: playwright-python/playwright/_impl/_connection.py (ChannelOwner class)
8// - Java: playwright-java/.../impl/ChannelOwner.java
9// - JavaScript: playwright/.../client/channelOwner.ts
10//
11// All Playwright objects (Browser, Page, etc.) implement ChannelOwner to:
12// - Represent remote objects on the server via GUID
13// - Participate in parent-child lifecycle management
14// - Handle protocol events
15// - Communicate via Channel proxy
16
17use crate::channel::Channel;
18use crate::connection::ConnectionLike;
19use parking_lot::Mutex;
20use serde_json::Value;
21use std::any::Any;
22use std::collections::HashMap;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::{Arc, Weak};
25
26/// Type alias for the children registry mapping GUIDs to ChannelOwner objects
27type ChildrenRegistry = HashMap<Arc<str>, Arc<dyn ChannelOwner>>;
28
29/// Reason why an object was disposed
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum DisposeReason {
32 /// Object was explicitly closed by user code
33 Closed,
34 /// Object was garbage collected by the server
35 GarbageCollected,
36}
37
38/// Parent can be either another ChannelOwner or the root Connection
39pub enum ParentOrConnection {
40 Parent(Arc<dyn ChannelOwner>),
41 Connection(Arc<dyn ConnectionLike>),
42}
43
44/// Base trait for all Playwright protocol objects.
45///
46/// Every object in the Playwright protocol (Browser, Page, BrowserContext, etc.)
47/// implements this trait to enable:
48/// - GUID-based object identity and lookup
49/// - Hierarchical parent-child lifecycle management
50/// - Channel-based RPC communication
51/// - Protocol event handling
52///
53/// # Architecture
54///
55/// All official Playwright bindings (Python, Java, .NET) follow this pattern:
56///
57/// 1. **GUID Identity**: Each object has a unique GUID from the server
58/// 2. **Parent-Child Tree**: Objects form a hierarchy (e.g., Browser → BrowserContext → Page)
59/// 3. **Dual Registry**: Objects are registered in both connection (global) and parent (lifecycle)
60/// 4. **Channel Communication**: Objects send/receive messages via their Channel
61/// 5. **Event Handling**: Protocol events are dispatched to objects by GUID
62///
63/// # Example
64///
65/// ```ignore
66/// # use playwright_core::channel_owner::ChannelOwner;
67/// # use std::sync::Arc;
68/// # fn example(browser: Arc<dyn ChannelOwner>) {
69/// // Get object identity
70/// println!("Object GUID: {}", browser.guid());
71/// println!("Object type: {}", browser.type_name());
72///
73/// // Handle lifecycle
74/// browser.dispose(playwright_core::channel_owner::DisposeReason::Closed);
75/// # }
76/// ```
77pub trait ChannelOwner: Send + Sync {
78 /// Returns the unique GUID for this object.
79 ///
80 /// The GUID is assigned by the Playwright server and used for:
81 /// - Looking up objects in the connection registry
82 /// - Routing protocol messages to the correct object
83 /// - Parent-child relationship tracking
84 fn guid(&self) -> &str;
85
86 /// Returns the protocol type name (e.g., "Browser", "Page").
87 fn type_name(&self) -> &str;
88
89 /// Returns the parent object, if any.
90 ///
91 /// The root Playwright object has no parent.
92 fn parent(&self) -> Option<Arc<dyn ChannelOwner>>;
93
94 /// Returns the connection this object belongs to.
95 fn connection(&self) -> Arc<dyn ConnectionLike>;
96
97 /// Returns the raw initializer JSON from the server.
98 ///
99 /// The initializer contains the object's initial state sent
100 /// in the `__create__` protocol message.
101 fn initializer(&self) -> &Value;
102
103 /// Returns the channel for RPC communication.
104 fn channel(&self) -> &Channel;
105
106 /// Disposes this object and all its children.
107 ///
108 /// Called when:
109 /// - Server sends `__dispose__` message
110 /// - User explicitly closes the object
111 /// - Parent is disposed (cascades to children)
112 ///
113 /// # Arguments
114 /// * `reason` - Why the object is being disposed
115 fn dispose(&self, reason: DisposeReason);
116
117 /// Adopts a child object (moves from old parent to this parent).
118 ///
119 /// Called when server sends `__adopt__` message, typically when:
120 /// - A page is moved between browser contexts
121 /// - An object's ownership changes
122 fn adopt(&self, child: Arc<dyn ChannelOwner>);
123
124 /// Adds a child object to this parent's registry.
125 ///
126 /// Called during object creation and adoption.
127 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>);
128
129 /// Removes a child object from this parent's registry.
130 ///
131 /// Called during disposal and adoption.
132 fn remove_child(&self, guid: &str);
133
134 /// Handles a protocol event sent to this object.
135 ///
136 /// # Arguments
137 /// * `method` - Event name (e.g., "close", "load")
138 /// * `params` - Event parameters as JSON
139 fn on_event(&self, method: &str, params: Value);
140
141 /// Returns true if this object was garbage collected.
142 fn was_collected(&self) -> bool;
143
144 /// Enables downcasting to concrete types.
145 ///
146 /// Required for converting `Arc<dyn ChannelOwner>` to specific types
147 /// like `Arc<Browser>` when retrieving objects from the connection.
148 fn as_any(&self) -> &dyn Any;
149}
150
151/// Base implementation of ChannelOwner that can be embedded in protocol objects.
152///
153/// This struct provides the common functionality for all ChannelOwner implementations.
154/// Protocol objects (Browser, Page, etc.) should contain this as a field and
155/// delegate trait methods to it.
156///
157/// # Example
158///
159/// ```ignore
160/// use playwright_core::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection, DisposeReason};
161/// use playwright_core::channel::Channel;
162/// use playwright_core::connection::ConnectionLike;
163/// use std::sync::Arc;
164/// use std::any::Any;
165/// use serde_json::Value;
166///
167/// pub struct Browser {
168/// base: ChannelOwnerImpl,
169/// // ... browser-specific fields
170/// }
171///
172/// impl Browser {
173/// pub fn new(
174/// parent: Arc<dyn ChannelOwner>,
175/// type_name: String,
176/// guid: Arc<str>,
177/// initializer: Value,
178/// ) -> Self {
179/// let base = ChannelOwnerImpl::new(
180/// ParentOrConnection::Parent(parent),
181/// type_name,
182/// guid,
183/// initializer,
184/// );
185/// Self { base }
186/// }
187/// }
188///
189/// impl ChannelOwner for Browser {
190/// fn guid(&self) -> &str { self.base.guid() }
191/// fn type_name(&self) -> &str { self.base.type_name() }
192/// fn parent(&self) -> Option<Arc<dyn ChannelOwner>> { self.base.parent() }
193/// fn connection(&self) -> Arc<dyn ConnectionLike> { self.base.connection() }
194/// fn initializer(&self) -> &Value { self.base.initializer() }
195/// fn channel(&self) -> &Channel { self.base.channel() }
196/// fn dispose(&self, reason: DisposeReason) { self.base.dispose(reason) }
197/// fn adopt(&self, child: Arc<dyn ChannelOwner>) { self.base.adopt(child) }
198/// fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
199/// self.base.add_child(guid, child)
200/// }
201/// fn remove_child(&self, guid: &str) { self.base.remove_child(guid) }
202/// fn on_event(&self, method: &str, params: Value) { self.base.on_event(method, params) }
203/// fn was_collected(&self) -> bool { self.base.was_collected() }
204/// fn as_any(&self) -> &dyn Any { self }
205/// }
206/// ```
207pub struct ChannelOwnerImpl {
208 guid: Arc<str>,
209 type_name: String,
210 parent: Option<Weak<dyn ChannelOwner>>,
211 connection: Arc<dyn ConnectionLike>,
212 children: Arc<Mutex<ChildrenRegistry>>,
213 channel: Channel,
214 initializer: Value,
215 was_collected: AtomicBool,
216}
217
218impl Clone for ChannelOwnerImpl {
219 fn clone(&self) -> Self {
220 Self {
221 guid: self.guid.clone(),
222 type_name: self.type_name.clone(),
223 parent: self.parent.clone(),
224 connection: Arc::clone(&self.connection),
225 children: Arc::clone(&self.children),
226 channel: self.channel.clone(),
227 initializer: self.initializer.clone(),
228 was_collected: AtomicBool::new(
229 self.was_collected.load(std::sync::atomic::Ordering::SeqCst),
230 ),
231 }
232 }
233}
234
235impl ChannelOwnerImpl {
236 /// Creates a new ChannelOwner base implementation.
237 ///
238 /// This constructor:
239 /// 1. Extracts the connection from parent or uses provided connection
240 /// 2. Creates the channel for RPC communication
241 /// 3. Stores the initializer data
242 /// 4. Registers itself in the connection (done by caller via Connection::register_object)
243 /// 5. Registers itself in parent (done by caller via parent.add_child)
244 ///
245 /// # Arguments
246 /// * `parent` - Either a parent ChannelOwner or the root Connection
247 /// * `type_name` - Protocol type name (e.g., "Browser")
248 /// * `guid` - Unique GUID from server
249 /// * `initializer` - Initial state from `__create__` message
250 pub fn new(
251 parent: ParentOrConnection,
252 type_name: String,
253 guid: Arc<str>,
254 initializer: Value,
255 ) -> Self {
256 let (connection, parent_opt) = match parent {
257 ParentOrConnection::Parent(p) => {
258 let conn = p.connection();
259 (conn, Some(Arc::downgrade(&p)))
260 }
261 ParentOrConnection::Connection(c) => (c, None),
262 };
263
264 // Arc<str> allows efficient sharing of GUID without cloning
265 let channel = Channel::new(Arc::clone(&guid), connection.clone());
266
267 Self {
268 guid,
269 type_name,
270 parent: parent_opt,
271 connection,
272 children: Arc::new(Mutex::new(HashMap::new())),
273 channel,
274 initializer,
275 was_collected: AtomicBool::new(false),
276 }
277 }
278
279 /// Returns the unique GUID for this object.
280 pub fn guid(&self) -> &str {
281 &self.guid
282 }
283
284 /// Returns the protocol type name.
285 pub fn type_name(&self) -> &str {
286 &self.type_name
287 }
288
289 /// Returns the parent object, if any.
290 pub fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
291 self.parent.as_ref().and_then(|p| p.upgrade())
292 }
293
294 /// Returns the connection.
295 pub fn connection(&self) -> Arc<dyn ConnectionLike> {
296 self.connection.clone()
297 }
298
299 /// Returns the initializer JSON.
300 pub fn initializer(&self) -> &Value {
301 &self.initializer
302 }
303
304 /// Returns the channel for RPC.
305 pub fn channel(&self) -> &Channel {
306 &self.channel
307 }
308
309 /// Disposes this object and all children recursively.
310 ///
311 /// # Arguments
312 /// * `reason` - Why the object is being disposed
313 pub fn dispose(&self, reason: DisposeReason) {
314 // Mark as collected if garbage collected
315 if reason == DisposeReason::GarbageCollected {
316 self.was_collected.store(true, Ordering::SeqCst);
317 }
318
319 // Remove from parent
320 if let Some(parent) = self.parent() {
321 parent.remove_child(&self.guid);
322 }
323
324 // Remove from connection (spawn to avoid blocking in sync context)
325 let connection = self.connection.clone();
326 let guid = Arc::clone(&self.guid);
327 tokio::spawn(async move {
328 connection.unregister_object(&guid).await;
329 });
330
331 // Dispose all children (snapshot to avoid holding lock)
332 let children: Vec<_> = {
333 let guard = self.children.lock();
334 guard.values().cloned().collect()
335 };
336
337 for child in children {
338 child.dispose(reason);
339 }
340
341 // Clear children
342 self.children.lock().clear();
343 }
344
345 /// Adopts a child object (moves from old parent to this parent).
346 pub fn adopt(&self, child: Arc<dyn ChannelOwner>) {
347 // Remove from old parent
348 if let Some(old_parent) = child.parent() {
349 old_parent.remove_child(child.guid());
350 }
351
352 // Add to this parent
353 // Convert &str to Arc<str> for the hashmap key
354 self.add_child(Arc::from(child.guid()), child);
355 }
356
357 /// Adds a child to this parent's registry.
358 pub fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
359 self.children.lock().insert(guid, child);
360 }
361
362 /// Removes a child from this parent's registry.
363 pub fn remove_child(&self, guid: &str) {
364 // Create Arc<str> for lookup
365 let guid_arc: Arc<str> = Arc::from(guid);
366 self.children.lock().remove(&guid_arc);
367 }
368
369 /// Handles a protocol event (default implementation logs it).
370 ///
371 /// Subclasses should override this to handle specific events.
372 pub fn on_event(&self, method: &str, params: Value) {
373 tracing::debug!(
374 "Event on {} ({}): {} -> {:?}",
375 self.guid,
376 self.type_name,
377 method,
378 params
379 );
380 }
381
382 /// Returns true if object was garbage collected.
383 pub fn was_collected(&self) -> bool {
384 self.was_collected.load(Ordering::SeqCst)
385 }
386}
387
388// Note: ChannelOwner testing is done via integration tests since it requires:
389// - Real Connection with object registry
390// - Multiple connected objects (parent-child relationships)
391// - Protocol messages from the server
392// See: crates/playwright-core/tests/connection_integration.rs