# How Ego-Browser Handles Helper Readiness Signals and Queue Calls

> Learn how ego-browser manages helper readiness signals and queue calls using Promise-based wrappers for seamless CDP runtime initialization and ordered execution.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-29

---

**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](https://github.com/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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts), the `installEgoSdk` function processes the `ready` option and constructs two key primitives: `readySignal` and `readyError`.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts), Lines 219-244). This function intercepts calls and enforces the readiness contract.

```typescript
// 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:

```javascript
// 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.

```typescript
// 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:

```javascript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (Lines 149-153) | Uses readiness-aware helpers for network-idle detection |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Holds shared runtime state accessed only after `readySignal` resolves |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts). The SDK converts this into `readySignal`—accepting `Promise`, `boolean`, or omitted values—with normalization at Lines 157-163.