# How Ego-Browser Wait Functions Work: `waitForSelector`, `waitForFunction`, `waitForLoadState`, and `waitForTimeout`

> Discover how Ego-Browser's wait functions like waitForSelector and waitForFunction work. Learn about CDP evaluation, error retries, and timeout management for efficient browser automation.

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

---

**Ego-Browser's wait functions in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) provide Playwright-style pausing, polling, and DOM monitoring by leveraging Chrome DevTools Protocol (CDP) evaluation, transient error retries, and a shared state clock for timeout management.**

The citrolabs/ego-lite library exposes four essential wait helpers that mirror Playwright's API but implement a lighter-weight waiting mechanism based on CDP runtime evaluation. These functions reside in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) and share common infrastructure for **timeouts**, **polling intervals**, and **error handling** through the central `state` singleton defined in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts). Understanding their implementation reveals how Ego-Browser achieves reliable synchronization with dynamic web pages without the overhead of full browser automation frameworks.


## `waitForTimeout`: Simple Millisecond Delays

The simplest of the wait functions, `waitForTimeout` creates an explicit pause in script execution. It accepts a single parameter defaulting to `1000` milliseconds and delegates to the shared state clock.

```typescript
export async function waitForTimeout(ms = 1000) {
  await state.sleep(ms);
}

```

*Source: [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (lines 44‑46)*

This implementation relies on `state.sleep()`, which manages the async delay using the runtime's default timeout configuration. Unlike browser-native `setTimeout`, this helper ensures consistency with Ego-Browser's global timing controls and cancellation semantics.


## `waitForFunction`: Polling for Runtime Conditions

`waitForFunction` repeatedly evaluates a user-supplied script in the browser context until it returns a truthy value or the deadline expires. The function constructs a polling loop using `state.now()` for deadline calculations and `cdp("Runtime.evaluate")` for script execution.

```typescript
export async function waitForFunction(
  pageFunction,
  argOrOptions = undefined,
  options = {},
) {
  const [arg, effectiveOptions] = normalizeWaitForFunctionArgs(
    arguments.length,
    argOrOptions,
    options,
  );
  const timeout = effectiveOptions.timeout ?? state.defaultTimeout;
  const polling = effectiveOptions.polling ?? 100;
  const deadline = state.now() + timeout;
  const expression = buildWaitForFunctionExpression(pageFunction, arg);
  
  while (state.now() < deadline) {
    const response = await cdp("Runtime.evaluate", {
      expression,
      returnByValue: true,
      awaitPromise: true,
    });
    const value = runtimeValue(response, expression);
    if (value) return value;
    await state.sleep(polling);
  }
  return false;
}

```

*Source: [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (lines 55‑81)*

The helper uses **argument normalization** to handle overloads where the second parameter might be the polling argument or an options object. By default, it polls every **100 ms**, but you can override this via the `polling` option. The `buildWaitForFunctionExpression` utility wraps function references into executable strings for the CDP `Runtime.evaluate` call.


## `waitForLoadState`: Document Readiness and Network Idle

This helper waits for specific page lifecycle milestones. It supports three states: `"load"`, `"domcontentloaded"`, and `"networkidle"`, with normalization logic that accepts either a string state or an options object.

```typescript
export async function waitForLoadState(
  loadState: LoadState | WaitForLoadStateOptions = "load",
  options: WaitForLoadStateOptions = {},
) {
  const [stateName, effectiveOptions] = normalizeLoadStateArgs(
    loadState,
    options,
  );
  if (stateName === "networkidle") {
    return waitForNetworkIdle(effectiveOptions);
  }
  return waitForDocumentLoad({
    timeout: effectiveOptions.timeout,
    until: stateName === "domcontentloaded" ? "domcontentloaded" : "load",
  });
}

```

*Source: [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (lines 54‑69)*

- **`"load"`** and **`"domcontentloaded"`** delegate to `waitForDocumentLoad` in [`src/driver/load.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/load.ts), which monitors `document.readyState` via CDP.
- **`"networkidle"`** enables the CDP **Network** domain temporarily (via `acquireNetworkEvents`) and waits for a period of network inactivity defined by `idleMs` (defaulting to 500 ms).

The temporary enabling of the Network domain prevents performance overhead during periods when network monitoring is unnecessary.


## `waitForSelector`: Element Resolution and Visibility

The most complex wait function, `waitForSelector` locates elements using CSS selectors, `@ref` references, or XPath, and optionally validates their visibility. It implements a retry loop with specific handling for **transient resolution errors** (when an element is not yet attached to the DOM).

```typescript
export async function waitForSelector(
  selector: string,
  options: WaitForSelectorOptions = {},
) {
  const timeout = options.timeout ?? state.defaultTimeout;
  const requireVisible = options.state === "visible";
  const deadline = state.now() + timeout;
  const visibilityFn =
    "function(){if(typeof this.checkVisibility==='function')" +
    "return this.checkVisibility({checkOpacity:true,checkVisibilityCSS:true});" +
    "const s=getComputedStyle(this);return s.display!=='none'&&s.visibility!=='hidden'&&s.opacity!=='0';}";
    
  while (state.now() < deadline) {
    let handle;
    try {
      handle = await resolveHandle(selector);
    } catch (err) {
      if (err instanceof ElementResolutionError && err.kind === "transient") {
        await state.sleep(300);
        continue;
      }
      throw err;
    }
    try {
      if (!requireVisible) return true;
      const response = await cdp(
        "Runtime.callFunctionOn",
        {
          functionDeclaration: visibilityFn,
          objectId: handle.objectId,
          returnByValue: true,
          awaitPromise: false,
        },
        handle.sessionId,
      );
      if (response.result?.value) return true;
    } catch {
      // Element disappeared between resolveHandle and visibility check – retry.
    } finally {
      await releaseHandle(handle.objectId, handle.sessionId);
    }
    await state.sleep(300);
  }
  return false;
}

```

*Source: [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) (lines 91‑132)*

When `options.state` is set to `"visible"`, the function injects a `visibilityFn` script via `Runtime.callFunctionOn` that checks `checkVisibility()` (modern browsers) or falls back to computed style inspection for `display`, `visibility`, and `opacity`. The function strictly manages remote object references by releasing handles after each attempt to prevent memory leaks in the CDP session.


## Practical Usage Examples

These patterns demonstrate how the wait functions behave in actual Ego-Browser scripts (exported via the `page` façade in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)):

```typescript
// Simple pause
await page.waitForTimeout(2000);

// Wait for custom JavaScript condition
await page.waitForFunction(() => !!document.querySelector('#ready'), {
  timeout: 10000,
  polling: 250,
});

// Wait for page load events
await page.waitForLoadState(); // 'load'
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle', { idleMs: 800 });

// Wait for element presence
const ok = await page.waitForSelector('#submit', {
  timeout: 5000,
  state: 'visible',
});
if (!ok) throw new Error('Submit button never appeared');

```


## Summary

- **Centralized timing**: All wait functions rely on `state.defaultTimeout` and `state.sleep()` from [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) for consistent deadline management.
- **CDP evaluation**: `waitForFunction` and visibility checks in `waitForSelector` use `Runtime.evaluate` and `Runtime.callFunctionOn` from [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts).
- **Transient error resilience**: `waitForSelector` catches `ElementResolutionError` with `kind === "transient"` to retry when elements are not yet attached, while permanent errors (bad selectors) throw immediately.
- **Network domain hygiene**: `waitForLoadState('networkidle')` temporarily enables the CDP Network domain via `acquireNetworkEvents` and disables it afterward to minimize side effects.
- **Handle cleanup**: `waitForSelector` explicitly releases remote object handles after each polling iteration to prevent CDP object reference leaks.


## Frequently Asked Questions

### How does `waitForSelector` handle elements that disappear between resolution and visibility checks?

The function wraps the visibility check in a `try/catch` block that silently triggers a retry loop iteration if the element handle becomes invalid between `resolveHandle` and the `Runtime.callFunctionOn` call. This handles race conditions where DOM mutations remove elements during the wait cycle.

### What is the difference between `waitForLoadState('networkidle')` and `'load'`?

The `'load'` state resolves when `document.readyState` becomes `"complete"`, indicating that the initial HTML and critical sub-resources have loaded. The `'networkidle'` state requires an additional period of zero network activity (default 500 ms), making it suitable for SPAs or pages with heavy post-load AJAX traffic.

### Can I customize polling intervals in `waitForFunction`?

Yes. Pass a number as the second argument or include a `polling` property in the options object. The default is 100 ms, but you can increase it for slower pages or decrease it for faster feedback loops, keeping in mind that shorter intervals increase CDP command overhead.

### Why does `waitForTimeout` use the state singleton instead of a raw `setTimeout`?

Using `state.sleep()` ensures the delay respects Ego-Browser's global timeout configuration and enables consistent cancellation and timing semantics across the entire driver. This abstraction allows the runtime to coordinate pauses with other asynchronous operations and maintain accurate `state.now()` clock synchronization.