How Ego-Browser Handles Helper Readiness Signals and Queue Calls
TLDR: ego-browser uses a Promise-based readiness signal (readySignal) combined with a recursive wrapReady wrapper to automatically queue all helper calls until the underlying CDP runtime is fully initialized, then executes them in order while surfacing any initialization errors.
ego-browser, the core browser automation engine in the citrolabs/ego-lite repository, solves a critical problem in browser automation: how to expose a clean API to agents while ensuring no helper executes before the Chrome DevTools Protocol (CDP) session is ready. This article breaks down the exact mechanism—readiness signals, wrapper functions, and implicit queueing—implemented in the source code.
The Readiness Signal Architecture
All helper readiness logic centers on a single Promise created during SDK installation. In src/index.ts, the installEgoSdk function processes the ready option and constructs two key primitives: readySignal and readyError.
// src/index.ts — Lines 157-163
const readySignal = Promise.resolve(options.ready ?? true);
let readyError: Error | undefined;
readySignal.catch((e) => { readyError = e; });
readySignalresolves when the embedding application (or test harness) signals that the CDP connection is established.readyErrorcaptures any rejection, allowing subsequent calls to fail fast with the original initialization error.
This design decouples the SDK setup from runtime readiness, letting agents write linear code without defensive if (ready) checks.
Wrapping Helpers with wrapReady
The engine recursively wraps every exported helper using wrapReady (defined in src/index.ts, Lines 219-244). This function intercepts calls and enforces the readiness contract.
// src/index.ts — Lines 219-244 (conceptual structure)
function wrapReady<T extends Function>(fn: T): T {
return async function (...args: any[]) {
await readySignal; // Queue here until ready
if (readyError) throw readyError; // Surface init failures
const result = await fn(...args);
// Recursively wrap object-returning helpers
if (result && typeof result === 'object') {
return wrapObject(result);
}
return result;
} as unknown as T;
}
Key behaviors:
- Automatic queueing: The
await readySignalstatement (Lines 231-233) pauses execution without blocking the event loop. Calls made before readiness accumulate as pending microtasks. - Error propagation: If initialization fails,
readyError()provides the rejection reason, thrown immediately on the next call attempt. - Recursive wrapping: Return values that contain nested helpers (e.g., driver objects from
observe()) are traversed and wrapped at Line 244, ensuring deep consistency.
How Queue Calls Work in Practice
Unlike explicit queue data structures, ego-browser leverages JavaScript's native Promise mechanics. When an agent calls a helper before the runtime is ready:
// Agent code — executes immediately, but waits internally
await nav('https://example.com'); // queued at readySignal
await click('#submit'); // queued behind nav
Each call returns a Promise that resolves only after readySignal resolves. The event loop naturally orders these pending promises, creating an implicit FIFO queue. This eliminates race conditions without additional state management.
HelperContext and the Export Surface
The helperContext function (invoked during SDK setup) instantiates helpers and passes them through wrapReady. All public methods—nav, click, waitFor, observe, and their descendants—pass through this wrapping layer.
// Conceptual flow from helperContext
const rawHelpers = createHelpers(ctx);
const wrappedHelpers = mapValues(rawHelpers, wrapReady);
This ensures every method exposed to agents respects the readiness signal, regardless of when or how it was defined.
Error Handling and Debugging
Initialization failures propagate cleanly:
// If readySignal rejects with a CDP connection error:
// readyError stores the Error object
// Subsequent calls throw immediately with that error
await nav('https://example.com'); // throws: "CDP connection failed"
This fail-fast behavior prevents confusing partial states where some helpers work and others hang.
Integration with Driver and State Layers
The readiness mechanism integrates with lower-level components:
| Layer | Role |
|---|---|
src/driver/waits.ts (Lines 149-153) |
Uses readiness-aware helpers for network-idle detection |
src/state.ts |
Holds shared runtime state accessed only after readySignal resolves |
src/helpers.ts |
Defines the surface API that wrapReady intercepts |
This layering ensures that even complex operations—like waiting for network idle or capturing screenshots—execute only in a fully initialized environment.
Summary
- Readiness signal:
readySignalinsrc/index.ts(Lines 157-163) provides a single source of truth for runtime readiness. - Wrapper function:
wrapReady(Lines 219-244) intercepts all helper calls, enforcingawait readySignalbefore execution. - Implicit queueing: Pending calls naturally queue via
Promisemicrotasks, requiring no explicit data structure. - Recursive safety: Returned objects are traversed and wrapped (Line 244), protecting nested methods.
- Error surfacing:
readyError()captures and re-throws initialization failures, preventing silent hangs.
Frequently Asked Questions
What happens if I call a helper before the ego runtime is initialized?
The call pauses at await readySignal inside the wrapReady wrapper and resumes automatically once the runtime signals readiness. From the caller's perspective, execution simply awaits completion without explicit queue management.
How does ego-browser handle initialization failures?
The readySignal rejection is captured in readyError (Lines 159-162). Every wrapped helper checks this value after awaiting the signal and throws the stored error if present, surfacing the root cause immediately.
Can nested helper objects bypass the readiness check?
No. The wrapReady function recursively processes object return values at Line 244, applying the same wrapping to all nested methods. This ensures consistent behavior across the entire API surface.
Where is the readiness signal configured?
The ready option is passed to installEgoSdk in src/index.ts. The SDK converts this into readySignal—accepting Promise, boolean, or omitted values—with normalization at Lines 157-163.
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 →