Ego-Browser CDP Transport Layer Architecture: How It Works
Ego-browser's CDP transport layer is a thin wrapper around the host-provided globalThis.ego.sendCDPMessage API that handles message framing, pending request tracking, automatic session management, and resilient error recovery.
The ego-browser SDK from the citrolabs/ego-lite repository provides a streamlined interface for controlling headless Chrome instances via the Chrome DevTools Protocol (CDP). At its foundation lies a specialized transport layer that abstracts raw protocol complexity behind a robust promise-based API, managing everything from message correlation to session lifecycle.
Core Message Handling
Message Framing and ID Management
Every outgoing CDP request is assigned a monotonically increasing numeric ID via nextMessageId and wrapped in a JSON payload. In src/browser-runtime.ts (lines 38-53), the transport constructs messages that optionally include a sessionId when targeting specific pages, ensuring proper routing through the host's globalThis.ego.sendCDPMessage API.
Pending Request Tracking with Promise Resolution
The transport maintains a Map named pending that stores resolver functions for each in-flight request. When the host returns a response, the handleMessage function matches the response id to its pending entry, resolving or rejecting the associated Promise (lines 39-50, 59-65, 71-77). This correlation mechanism ensures that asynchronous CDP commands return the correct results to the caller.
Configurable Timeout Protection
To prevent hanging calls, each request is wrapped in a Promise that automatically rejects after RESPONSE_TIMEOUT_MS (default 15 seconds), as implemented in lines 55-58 of src/browser-runtime.ts. This hard limit ensures that network stalls or unresponsive targets do not block execution indefinitely.
Session Lifecycle Management
Automatic Session Attachment
For page-level commands (those that do not start with Target. or Browser.), the transport ensures a session is attached to the active tab. The ensureSession function inspects cached state from src/state.ts and, if stale, enumerates available tabs via ego.listTabs(), selects the preferred or most recent target, and executes Target.attachToTarget to obtain a fresh sessionId (lines 7-23, 84-103).
Session Caching and TTL
To optimize performance, the transport caches session identifiers with a time-to-live (TTL) of SESSION_TTL_MS (2 seconds). This caching strategy balances responsiveness with freshness, automatically refreshing the session when the TTL expires or when targeting a different context.
Transparent Recovery from Session Loss
If a CDP request fails with a "session lost" error (matching the SESSION_LOST regex), the transport clears the cached session from state, creates a new one via ensureSession, and retries the original request transparently without throwing an error to the caller (lines 96-103). This resilience pattern handles transient disconnections gracefully.
Event Buffering and Subscription
Bounded Circular Buffer for Events
Incoming CDP events are parsed in handleMessage and stored in a bounded circular buffer limited to MAX_BUFFERED_EVENTS (10,000 entries). This architectural choice prevents unbounded memory growth during long-running browser automations (lines 84-92, 164-172). When the buffer reaches capacity, older events are overwritten to maintain stable memory usage.
Event Subscription Interface
The transport exposes subscribeBrowserEvent to allow user scripts to listen for specific CDP events. Subscribers receive events from the buffer in real-time, and the function returns an unsubscribe handler for cleanup. This decouples event consumption from the core message handling loop.
Error Handling and Propagation
Normalized Error Objects
Local send failures—such as when the host task becomes inactive—are caught by handleSendError, which iterates through all pending promises and rejects them with a normalized EgoError object defined in src/ego-errors.ts (lines 24-31). This ensures consistent error shapes across the SDK, regardless of where the failure originates.
Practical Usage Examples
// Example: sending a CDP command that automatically gets a page session.
import { browserCdp } from 'ego-browser';
// Navigate to a URL
await browserCdp('Page.navigate', { url: 'https://example.com' });
// Evaluate an expression in the page context
const result = await browserCdp('Runtime.evaluate', {
expression: 'document.title',
});
console.log('Page title:', result.result.value);
// Example: subscribing to a CDP event (e.g., console messages)
import { subscribeBrowserEvent } from 'ego-browser';
const unsubscribe = subscribeBrowserEvent(
'Runtime.consoleAPICalled',
undefined, // listen on any session
(event) => console.log('Console:', event.params.args)
);
// Later – stop listening
unsubscribe();
// Example: manually creating a session for a specific tab (advanced)
import { ensureSession, browserCdp } from 'ego-browser';
// Force a new session (useful for a non‑active tab)
const sessionId = await ensureSession();
await browserCdp('Runtime.enable', {}, sessionId);
Summary
- The transport layer in
src/browser-runtime.tswrapsglobalThis.ego.sendCDPMessageto provide a promise-based CDP interface. - Message correlation relies on a monotonic ID system and a
pendingMap to match responses with requests. - Session management includes automatic attachment, 2-second TTL caching, and transparent retry logic for lost sessions.
- Memory safety is enforced through a 10,000-event circular buffer that prevents unbounded growth during long executions.
- Error handling normalizes all failures into
EgoErrorobjects and includes 15-second timeouts to prevent hanging operations.
Frequently Asked Questions
How does ego-browser correlate CDP requests with responses?
The transport assigns each outgoing message a unique numeric ID using nextMessageId and stores the corresponding Promise resolver in a pending Map. When the host returns a response containing the same id, the transport looks up and executes the resolver, ensuring accurate correlation even with out-of-order responses.
What happens when a CDP session expires during execution?
When the SESSION_TTL_MS (2 seconds) expires or a "session lost" error occurs, the transport automatically clears the cached sessionId from src/state.ts, calls Target.attachToTarget to create a new session, and retries the failed request. This recovery happens transparently without requiring manual intervention from the calling code.
How does the transport prevent memory leaks from CDP events?
Incoming events are stored in a circular buffer capped at MAX_BUFFERED_EVENTS (10,000 entries). Once the buffer reaches capacity, new events overwrite the oldest entries. This design ensures that long-running browser sessions do not consume unlimited memory while still preserving recent event history for subscribers.
What causes the EgoError objects and how are they structured?
EgoError objects are created in src/ego-errors.ts and propagated by handleSendError when the host environment fails to send a message (for example, if the underlying task becomes inactive). These normalized error objects wrap the underlying failure reason, allowing consistent error handling patterns across the entire ego-browser SDK.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →