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; });
  • readySignal resolves when the embedding application (or test harness) signals that the CDP connection is established.
  • readyError captures 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 readySignal statement (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: readySignal in src/index.ts (Lines 157-163) provides a single source of truth for runtime readiness.
  • Wrapper function: wrapReady (Lines 219-244) intercepts all helper calls, enforcing await readySignal before execution.
  • Implicit queueing: Pending calls naturally queue via Promise microtasks, 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →