How the Browser Runtime Module Manages Chrome DevTools Protocol (CDP) Communication in ego‑lite
The browser runtime module in ego‑lite provides a robust, session‑aware CDP transport layer with automatic retry logic, event buffering, and timeout protection—abstracting raw Chrome DevTools Protocol calls into a clean async API.
The ego‑lite browser automation library (see citrolabs/ego-lite) implements its CDP integration through a dedicated runtime module. This article examines how browser-runtime.ts orchestrates protocol communication, from low‑level message transport to higher‑level session management and event subscription.
Runtime Detection and Environment Setup
Before attempting any CDP communication, ego‑lite verifies it's running inside a browser host environment.
isBrowserRuntime() checks for the existence of globalThis.ego.sendCDPMessage (browser-runtime.ts#L25-L29):
export const isBrowserRuntime = (): boolean => {
return (
typeof globalThis.ego !== 'undefined' &&
typeof globalThis.ego.sendCDPMessage === 'function'
);
};
This guard prevents runtime errors when the code executes outside the expected browser context.
Low‑Level CDP Transport with rawCdp()
The raw CDP transport function handles the complete request‑response lifecycle.
rawCdp() (browser-runtime.ts#L38-L76):
- Generates a unique
requestIdusing a monotonic counter - Structures the JSON payload with
id,method, and optionalparams - Registers callbacks for success (
onCDPMessage) and failure (onSendCDPMessageError) - Sends via
ego.sendCDPMessage - Sets a 15‑second timeout (
RESPONSE_TIMEOUT_MS = 15000) that rejects pending requests if no response arrives (browser-runtime.ts#L55-L58)
// Sending a raw CDP command
const result = await rawCdp(
'Runtime.evaluate',
{ expression: 'document.title', returnByValue: true },
'my-session-id' // optional session ID
);
The pending‑request map stores promise resolvers/rejectors keyed by requestId, enabling asynchronous response correlation.
Session Management with ensureSession()
CDP requires a target session for most operations. The runtime automates session lifecycle to hide this complexity.
ensureSession() (browser-runtime.ts#L7-L37) performs:
- Tab enumeration via
ego.listTabs() - Target selection (preferred tab or active fallback)
- Session attachment via
Target.attachToTarget - Domain enablement (e.g.,
Page.enablefor page events)
Sessions are cached for 2 seconds (SESSION_TTL_MS = 2000) to avoid redundant attachment overhead (browser-runtime.ts#L8-L14). An "in‑flight" guard prevents duplicate concurrent session creation attempts.
// Ensure a session exists (creates or reuses cached)
const sessionId = await ensureSession();
// Use the session for subsequent calls
await rawCdp('Page.reload', { ignoreCache: false }, sessionId);
Session Recovery and Retry Logic
When the underlying browser target detaches (e.g., page navigation), CDP returns session‑lost errors. The runtime auto‑detects and recovers.
If rawCdp() encounters a SESSION_LOST error pattern (browser-runtime.ts#L96-L103):
- The stale session is invalidated
- The request is automatically retried with a fresh session
This transparent recovery eliminates manual session handling for most use cases.
Public API: browserCdp()
browserCdp() serves as the primary entry point for CDP communication throughout ego‑lite (browser-runtime.ts#L79-L89).
Key behaviors:
- Automatic session injection: Adds the current session ID unless the method is
Browser.*orTarget.*(top‑level browser/target calls) - Test override support: Respects
state.cdpOverridefor mocking in test environments - Delegates to
rawCdpfor actual transport
// Navigate to a URL (session injected automatically)
await browserCdp('Page.navigate', { url: 'https://example.com' });
// Evaluate JavaScript in the page context
const { result } = await browserCdp('Runtime.evaluate', {
expression: '1 + 1',
returnByValue: true
});
console.log(result.value); // 2
Event Buffering and Subscription
CDP is bidirectional: the browser pushes events (Page.loadEventFired, Runtime.consoleAPICalled, etc.) without prior requests. The runtime provides mechanisms to consume these.
Event Buffer
Incoming events are stored in a bounded array (events) with a 10,000‑entry maximum (MAX_BUFFERED_EVENTS) to prevent memory exhaustion (browser-runtime.ts#L6-L8, browser-runtime.ts#L86-L90). Old events are evicted when the limit is reached.
subscribeBrowserEvent()
Create persistent subscriptions for specific CDP methods (browser-runtime.ts#L88-L95):
// Subscribe to all console messages
const unsubscribe = subscribeBrowserEvent(
'Runtime.consoleAPICalled',
undefined, // any session
(event) => {
const { type, args } = event.params;
console.log(`[${type}]`, args.map(a => a.value).join(' '));
}
);
// Later: clean up
unsubscribe();
waitForBrowserEvent()
Wait for a one‑off event matching a predicate, with timeout protection (browser-runtime.ts#L69-L85):
// Wait for page load completion
const loadEvent = await waitForBrowserEvent(
(e) => e.method === 'Page.loadEventFired' && e.params.sessionId === sessionId,
30000 // 30 second timeout
);
console.log('Loaded at:', loadEvent.params.timestamp);
Dialog Tracking
JavaScript dialogs (alert, confirm, prompt) block page execution. The runtime tracks these via dedicated event handlers.
Dialog state is maintained in pendingDialogs per session, updated on Page.javascriptDialogOpening and Page.javascriptDialogClosed events (browser-runtime.ts#L66-L75).
// Navigate to a page that triggers an alert
await browserCdp('Page.navigate', { url: 'https://example.com/dialog' });
// Check for pending dialog
const dialog = pendingDialog();
if (dialog) {
console.log('Dialog type:', dialog.type); // 'alert' | 'confirm' | 'prompt'
console.log('Message:', dialog.message);
// Accept or dismiss
await browserCdp('Page.handleJavaScriptDialog', {
accept: true,
promptText: 'user input here' // for prompts
});
}
Error Handling and Cleanup
Transport failures (host process termination, network issues) are centralized through handleSendError() (browser-runtime.ts#L22-L30), which:
- Rejects all pending requests with a descriptive
EgoError - Clears the pending‑request map
- Triggers any registered error callbacks
This ensures no hanging promises when the underlying connection fails.
Complete Working Example
import {
isBrowserRuntime,
ensureSession,
browserCdp,
subscribeBrowserEvent,
waitForBrowserEvent,
pendingDialog
} from './browser-runtime';
async function automationExample() {
// 1. Verify environment
if (!isBrowserRuntime()) {
throw new Error('Not running in ego browser host');
}
// 2. Establish session (cached automatically)
const sessionId = await ensureSession();
// 3. Subscribe to console events
const unsubConsole = subscribeBrowserEvent(
'Runtime.consoleAPICalled',
sessionId,
(e) => console.log('Console:', e.params.args)
);
// 4. Navigate and wait for load
await browserCdp('Page.navigate', { url: 'https://example.com' });
await waitForBrowserEvent(
(e) => e.method === 'Page.loadEventFired',
30000
);
// 5. Evaluate script
const { result } = await browserCdp('Runtime.evaluate', {
expression: 'document.title',
returnByValue: true
});
console.log('Page title:', result.value);
// 6. Check and handle any dialog
const dialog = pendingDialog();
if (dialog) {
await browserCdp('Page.handleJavaScriptDialog', { accept: true });
}
// 7. Cleanup
unsubConsole();
}
automationExample().catch(console.error);
Key Source Files
| File | Responsibility | Link |
|---|---|---|
browser-runtime.ts |
Core CDP transport, session management, event buffering, public API (browserCdp, ensureSession, etc.) |
View source |
state.ts |
Mutable runtime state: current session ID, preferred target, test overrides | View source |
cdp-eval.ts |
Higher‑level wrappers (cdp(), js()) that simplify common evaluation patterns |
View source |
helpers.ts |
Exposes runtime utilities to user scripts through helperContext() |
View source |
Summary
- Detection:
isBrowserRuntime()verifies the ego host environment before CDP operations - Transport:
rawCdp()implements request correlation, timeouts, and error handling at the protocol level - Sessions:
ensureSession()automates target attachment with caching and transparent retry on session loss - Public API:
browserCdp()provides the clean interface used throughout the codebase, with automatic session injection - Events: Bounded buffering (
MAX_BUFFERED_EVENTS = 10000) plussubscribeBrowserEvent()andwaitForBrowserEvent()enable flexible event consumption - Dialogs: Automatic tracking of JavaScript dialogs with
pendingDialog()for synchronous‑style inspection
Frequently Asked Questions
What happens if a CDP request times out?
The rawCdp() function enforces a 15‑second timeout via RESPONSE_TIMEOUT_MS. When triggered, the pending promise rejects with a timeout error and the request is removed from the internal pending map to prevent memory leaks.
Can I mock CDP responses for testing?
Yes. The browserCdp() function checks state.cdpOverride before proceeding to the real transport. Setting this property to a mock function intercepts all CDP calls, enabling isolated unit tests without a live browser.
How does event subscription handle high‑volume CDP traffic?
Events are buffered in a fixed‑size array capped at 10,000 entries. When the limit is reached, oldest events are discarded. Subscriptions filter this buffer by method name and optional session ID, so consumers receive only relevant events without processing overhead.
Why does session creation have a 2‑second TTL?
The 2000ms cache (SESSION_TTL_MS) balances performance and correctness. CDP session creation involves multiple round‑trips (list tabs → attach → enable domains). Caching avoids this overhead for rapid successive calls, while the short TTL ensures timely recovery if the underlying target changes.
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 →