Version 0.1: Protocol Foundation - Technical Summary
May 10, 2026 · View on GitHub
Completion Date: 2025-11-06 Status: ✅ Complete
What Was Built
Version 0.1 delivers the complete protocol foundation for playwright-rust, enabling communication between Rust code and the Playwright Node.js server.
Core Components
1. Server Management (server.rs)
- Automatic driver download via
build.rs(downloads Playwright server from npm during build) - Server lifecycle - Launch Playwright server as child process with
playwright run-server - Environment variable support -
PLAYWRIGHT_DRIVER_PATHfor custom driver locations - Cross-platform - Works on macOS, Linux (Windows TBD)
2. Transport Layer (transport.rs)
- Length-prefixed framing - 4-byte little-endian length + JSON message
- Bidirectional communication - Separate read/write streams over stdio
- Generic design -
Transport<W, R>parameterized overAsyncWriteandAsyncRead - Testability - Can use real server or mock duplex pipes for tests
3. Connection Layer (connection.rs)
- JSON-RPC client - Request/response correlation with sequential message IDs
- Message dispatch - Routes incoming messages by type (
__create__,__dispose__, events, responses) - Object registry - Global HashMap of all protocol objects by GUID
- Async message loop - Background task processes messages from transport
- Initialization protocol - Special handshake to create root Playwright object
4. Channel Owner Architecture (channel_owner.rs)
- Base trait - All protocol objects implement
ChannelOwner - GUID-based identity - Each object has unique server-assigned GUID
- Parent-child hierarchy - Objects form a tree (Playwright → BrowserType → Browser → ...)
- Dual registry - Objects registered in both connection (global) and parent (lifecycle)
- Disposal cascade - Disposing parent disposes all children recursively
- Channel RPC - Each object has a
Channelfor sending protocol messages
5. Object Factory (object_factory.rs)
- Type dispatch - Maps protocol type names to Rust constructors
- Extensible - Easy to add new protocol types in future versions
- Current types:
Playwright- Root object providing browser type accessBrowserType- Represents Chromium, Firefox, or WebKitRoot- Temporary initialization object (internal)
6. Protocol Types (protocol/)
- Playwright - Main entry point, provides
chromium(),firefox(),webkit() - BrowserType - Browser metadata (name, executable path)
- Root - Initialization helper with
initialize()method
7. High-Level API (playwright crate)
- Public re-exports - Clean API surface (
Playwright,Error,Result) - Entry point -
Playwright::launch().await?orchestrates full initialization - Example code - Working example in
examples/basic.rs
Key Technical Decisions
Synchronization Strategy
Decision: Use parking_lot::Mutex for object registry
Rationale:
- All official bindings (Python, Java, .NET) use synchronous disposal
- Critical sections are very short (just HashMap lookups/inserts)
parking_lotis faster thanstd::sync::Mutexand works in async contexts- Avoids async lock complexity and deadlock risks
See ADR-0002 for detailed research and rationale.
Message Dispatch Architecture
Decision: Match Python's handler pattern exactly Pattern:
match message {
Message::Event { id, method, params } => {
// Route event to object by GUID
let object = self.get_object(&id).await?;
object.on_event(&method, params);
}
Message::Response { id, result, error } => {
// Resolve pending request by ID
if let Some(callback) = self.callbacks.lock().remove(&id) {
callback.send(result_or_error);
}
}
}
Root Object Pattern
Decision: Use temporary Root object for initialization
Problem: Python's initialize_playwright() creates the root object by sending an RPC to a temporary "Root" object with empty GUID ("")
Solution:
- Create temporary
Rootobject with GUID "" - Call
root.initialize()which sendsinitializeRPC - Server responds with
__create__events for Playwright and BrowserTypes - Return actual Playwright object
- Root object discarded after initialization
This matches Python exactly and handles the initialization handshake properly.
Architecture Diagrams
Initialization Flow
┌─────────────────────────────────────────────────────────┐
│ 1. Playwright::launch() │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 2. PlaywrightServer::launch() │
│ - Spawn `playwright run-server` as child process │
│ - Get stdin/stdout handles │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 3. PipeTransport::new(stdin, stdout) │
│ - Create bidirectional transport │
│ - Spawn read loop for incoming messages │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 4. Connection::new(transport, message_rx) │
│ - Create connection with object registry │
│ - Create callbacks registry for pending requests │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 5. tokio::spawn(connection.run()) │
│ - Run message dispatch loop in background │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 6. connection.initialize_playwright() │
│ a. Create Root object with GUID "" │
│ b. root.initialize() sends "initialize" RPC │
│ c. Server responds with __create__ messages: │
│ - Playwright (guid: "playwright@...") │
│ - BrowserType chromium │
│ - BrowserType firefox │
│ - BrowserType webkit │
│ d. Object factory creates typed Rust objects │
│ e. Objects registered in connection registry │
│ f. Return Playwright object │
└────────────────┬────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────┐
│ 7. User code │
│ playwright.chromium() // Access browser types │
└─────────────────────────────────────────────────────────┘
Object Hierarchy After Initialization
Connection (object registry: HashMap<String, Arc<dyn ChannelOwner>>)
│
├── Playwright (guid: "playwright@...")
│ ├── chromium: BrowserType (guid: "browserType@chromium")
│ ├── firefox: BrowserType (guid: "browserType@firefox")
│ └── webkit: BrowserType (guid: "browserType@webkit")
│
└── (Future Version 0.2 objects)
├── Browser
│ └── BrowserContext
│ └── Page
│ └── Frame
Testing Strategy
Test Coverage
- 54 total tests (unit + integration + doc tests)
- 100% of Version 0.1 code paths tested
- Integration tests use real Playwright server (not mocks)
- Doc tests verify all examples compile
Test Organization
crates/playwright-core/
├── src/
│ ├── connection.rs (unit tests with mock duplex streams)
│ ├── transport.rs (unit tests with mock AsyncWrite/Read)
│ └── ... (doc tests in all modules)
├── tests/
│ ├── initialization_integration.rs (full server integration)
│ └── playwright_launch.rs (high-level API integration)
Testing Patterns Learned from Official Bindings
Based on research of Python, Java, and .NET implementations:
- All official bindings test against real servers - No mocking of protocol
- Integration tests are primary - Unit tests for isolated logic only
- Generics enable testability -
Transport<W, R>can use mock or real streams - Doc tests are valuable - Ensure examples stay up-to-date
Performance Characteristics
Message Throughput
- Lock-free read path - Transport reads don't block on locks
- Minimal lock contention - Object registry uses fast
parking_lot::Mutex - Lock hold time - Only held during HashMap lookup/insert (microseconds)
- Async-friendly - No locks held across await points
Memory Management
- Arc-based ownership - Objects shared between connection and parent
- Automatic cleanup - Objects removed from registries on disposal
- No leaks - All protocol objects cleaned up when connection drops
- Efficient cloning - Arc clones are cheap (atomic refcount increment)
Scalability
- Single connection per Playwright instance - Matches official bindings
- Background message loop - Doesn't block user code
- Bounded memory - Object registry grows with active objects only
- Cleanup on disposal - Old objects removed promptly
Scope and Limitations
Version 0.1 Scope (Complete)
Version 0.1 delivered the protocol foundation only:
- ✅ JSON-RPC communication with Playwright server
- ✅ Object factory and lifecycle management
- ✅ Playwright initialization flow
- ✅ Access to browser types (objects exist, not launched yet)
Out of Scope for Version 0.1
Browser automation features are intentionally deferred to future versions:
- Version 0.2: Browser launching, contexts, pages
- Version 0.3: Navigation, locators, actions
- Version 0.4: Screenshots, network interception, assertions
- Version 0.5: Mobile emulation, advanced features
See Version 0.2 Implementation Plan for next steps.
API Examples
Basic Usage (Version 0.1)
use playwright::Playwright;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Launch Playwright and connect to server
let playwright = Playwright::launch().await?;
// Access browser types (Version 0.1 - objects exist, browsers not launched yet)
let chromium = playwright.chromium();
let firefox = playwright.firefox();
let webkit = playwright.webkit();
println!("Chromium: {}", chromium.executable_path());
println!("Firefox: {}", firefox.executable_path());
println!("WebKit: {}", webkit.executable_path());
Ok(())
}
Running the Example
# Driver is auto-downloaded by build.rs into $OUT_DIR; no path setup needed.
cargo run --package playwright-rs --example basic
Output (paths show the driver living under Cargo's $OUT_DIR):
🚀 Launching Playwright...
✅ Playwright launched successfully!
📦 Available browser types:
• Chromium: target/debug/build/playwright-rs-<hash>/out/playwright-driver/playwright-<version>-<platform>/node_modules/playwright-core/...
• Firefox: target/debug/build/playwright-rs-<hash>/out/playwright-driver/playwright-<version>-<platform>/node_modules/playwright-core/...
• WebKit: target/debug/build/playwright-rs-<hash>/out/playwright-driver/playwright-<version>-<platform>/node_modules/playwright-core/...
Lessons Learned
What Worked Well
-
Vertical slicing approach
- Each slice delivered testable functionality
- Clear dependencies enabled incremental progress
- TDD workflow (Red → Green → Refactor) maintained quality
-
Research-driven implementation
- Studying all three official bindings before coding
- Identified common patterns across languages
- Avoided Rust-specific anti-patterns
-
Generic type parameters
Transport<W, R>andConnection<W, R>enabled excellent testability- Can test with mock streams or real server
- No compromise on production performance
-
Clear documentation
- Architecture Decision Records captured rationale
- Implementation plan tracked progress
- Rustdoc with examples for all public APIs
Challenges Overcome
-
Initialization complexity
- Root object pattern was not obvious from protocol.yml
- Required deep dive into Python implementation
- Solution: ADR-0002 documents the pattern for future reference
-
Sync/async boundaries
- ChannelOwner disposal is synchronous but needs async cleanup
- Research showed all official bindings handle this the same way
- Solution: parking_lot::Mutex + tokio::spawn (deferred cleanup)
-
Type erasure
- Connection stores
Arc<dyn ChannelOwner>but users need concrete types - Solution:
as_any()pattern for downcasting (standard Rust pattern)
- Connection stores
-
Message dispatch deadlocks
- Initial design with async locks caused deadlocks
- Solution: Switch to parking_lot::Mutex and keep lock scopes tight
What Would We Do Differently
- Earlier integration testing - Could have written full integration test earlier
- Mock transport sooner - DuplexStream pattern could have been introduced in Slice 2
- Document Root pattern earlier - Spent time confused about initialization
Version 0.2 Readiness
Version 0.1 provides a solid foundation for Version 0.2. The architecture is ready for browser launching and page automation.
See Version 0.2 Implementation Plan for details.
Metrics
- Lines of code: ~3,500 (core + public API)
- Test coverage: 54 tests, all passing
- Build time: < 1s (incremental), ~30s (clean)
- Example execution: ~200ms (server launch to output)
- Clippy warnings: 0
- Unsafe code: 0
- Documentation: 100% of public APIs documented