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

Ego-Browser's wait functions in 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 and share common infrastructure for timeouts, polling intervals, and error handling through the central state singleton defined in 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.

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

Source: 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.

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 (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.

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 (lines 54‑69)

  • "load" and "domcontentloaded" delegate to waitForDocumentLoad in 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).

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 (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):

// 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 for consistent deadline management.
  • CDP evaluation: waitForFunction and visibility checks in waitForSelector use Runtime.evaluate and Runtime.callFunctionOn from 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.

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 →