# How ego-lite's Wait Functions Handle Load States and Selectors: A Complete Technical Guide

> Explore ego-lites wait functions and their unified polling architecture using CDP and timeouts to manage page load states and selector availability for efficient testing.

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

---

**ego-lite's wait functions use a unified polling architecture that combines CDP (Chrome DevTools Protocol) communication with configurable timeouts to pause execution until pages reach specific load states or selectors become available.**

The **citrolabs/ego-lite** open-source browser automation library provides a Playwright-like waiting API designed specifically for AI agents. This guide examines how `waitForLoadState`, `waitForURL`, `waitForSelector`, and network wait helpers manage different conditions, with full reference to the implementation in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) and supporting modules.

---

## The Core Wait Architecture

All wait helpers share a common pattern implemented in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts):

1. **Timeout resolution** — read user-provided `timeout` or fall back to `state.defaultTimeout`
2. **Deadline calculation** — `state.now() + timeout`
3. **Polling loop** — repeatedly query the browser via CDP until success or deadline expiry
4. **Resource cleanup** — guaranteed release of CDP domains (Network, Runtime, etc.)

This architecture ensures consistent behavior across load states, selectors, URLs, and network events.

---

## How waitForLoadState Handles Different Load States

`waitForLoadState` accepts either a string (`"load"`, `"domcontentloaded"`, `"networkidle"`) or an options object. The implementation normalizes arguments (lines 58-79) and delegates to specialized handlers:

### `"load"` — Full Document Completion

Calls `waitForDocumentLoad` with `until: "load"` from [`src/driver/load.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/load.ts). The helper polls `document.readyState` until it equals `"complete"` (lines 165-168).

```typescript
// Wait for full page load including images and subresources
await waitForLoadState('load');

```

### `"domcontentloaded"` — DOM Interactive

Calls `waitForDocumentLoad` with `until: "domcontentloaded"`. Resolves when `readyState` reaches `"interactive"` — the DOM is parsed but external resources may still load (lines 165-168).

```typescript
// Wait only for DOM structure, not full resource loading
await waitForLoadState('domcontentloaded');

```

### `"networkidle"` — Network Silence

Delegates to the private helper `waitForNetworkIdle` (lines 346-376). This helper:

- Enables the CDP `Network` domain via `acquireNetworkEvents`
- Tracks in-flight requests using CDP `Network.requestWillBeSent` and `Network.loadingFinished` events
- Returns `true` only after zero pending requests for a configurable `idleMs` window (default behavior, lines 462-473)

```typescript
// Wait 500ms of network silence before proceeding
await waitForLoadState('networkidle', { idleMs: 500 });

```

The `idleMs` parameter lets you tune sensitivity — lower values resolve faster but risk catching transient request gaps; higher values ensure true stability.

---

## How waitForURL Combines URL Matching with Load States

`waitForURL` polls `location.href` via `Runtime.evaluate` until the supplied matcher succeeds (lines 94-104). Matchers support:

- **String** — exact match
- **Glob** — patterns like `**/checkout*`
- **RegExp** — pattern matching
- **Predicate** — custom function `(url: string) => boolean`

After URL matching, it respects the `waitUntil` option:

| `waitUntil` value | Behavior |
|-------------------|----------|
| **`commit`** | Resolves immediately when URL matches; useful for `about:blank` navigation |
| **`load`** (default) | Forwards to `waitForLoadState("load")` |
| **`networkidle`** | Forwards to `waitForLoadState("networkidle")` |

Special handling for `about:blank` (lines 106-110): document-load checks are skipped unless `networkidle` is explicitly requested, preventing hangs on empty pages.

```typescript
// Match exact URL and wait for full load
await waitForURL('https://example.com/dashboard', { waitUntil: 'load' });

// Glob match, resolve immediately on navigation commit
await waitForURL('**/checkout*', { waitUntil: 'commit' });

```

---

## How waitForSelector Handles Selector States

`waitForSelector` implements two distinct visibility states through different polling strategies (lines 100-108):

### `"attached"` — Element Existence (Default)

Repeatedly calls `resolveHandle(selector)` to obtain a CDP `RemoteObjectId`. If resolution fails (element not yet in DOM), sleeps 300ms and retries. Returns immediately upon handle acquisition, regardless of visual properties.

### `"visible"` — Element Visibility

Requires additional JavaScript evaluation via `visibilityFn` (lines 99-101, 121-124). The injected script verifies:

- `element.offsetParent !== null` (not detached from layout)
- `window.getComputedStyle(element).display !== 'none'`
- `window.getComputedStyle(element).visibility !== 'hidden'`
- `parseFloat(window.getComputedStyle(element).opacity) > 0`

Failed visibility checks are treated as "not ready yet" — the polling loop continues rather than throwing.

```typescript
// Wait for element to exist in DOM (fastest)
await waitForSelector('#submit-button');

// Wait for element to be visually rendered (slower, more reliable)
await waitForSelector('#submit-button', { state: 'visible' });

```

The 300ms polling interval balances responsiveness with CDP overhead. Unlike Playwright's rAF-based waiting, ego-lite uses fixed intervals for predictable timing in agent workflows.

---

## Network-Level Waiting: waitForRequest and waitForResponse

Both functions call `waitForNetworkMatch` (lines 125-135, 250-267), which:

1. Ensures Network domain enabled via `acquireNetworkEvents`
2. Listens for CDP events (`Network.requestWillBeSent`, `Network.responseReceived`)
3. Applies `networkMatches` logic to compare against matcher (lines 330-337)

**Matcher types:**
- **String** — URL contains substring
- **RegExp** — pattern test against full URL
- **Predicate** — custom `(request|response) => boolean`

Important restriction: async predicates throw a clear error (lines 330-337) — only synchronous functions are supported due to CDP event handling constraints.

```typescript
// Wait for any PNG image request
const pngReq = await waitForRequest(/\.png$/);

// Wait for successful API response using predicate
const apiResp = await waitForResponse(r => 
  r.status() === 200 && r.url().includes('/api/')
);

```

Network waiting shares timeout handling (`networkTimeout`, `browserEventTimeout`) and cleanup logic with other waits (lines 373-382), ensuring the Network domain is always released after completion or failure.

---

## Timeout and Resource Management

All waits integrate with [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) for timing utilities:

- **`state.now()`** — high-resolution timestamp for deadline calculation
- **`state.sleep(ms)`** — async delay between polling iterations
- **`state.defaultTimeout`** — fallback when user timeout unspecified

Critical cleanup guarantees (lines 373-382):

- Network domain disabled after `waitForNetworkIdle`, `waitForRequest`, `waitForResponse`
- JavaScript handles released after `waitForSelector` visibility checks
- Event listeners drained via `drainEvents` from [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)

This prevents resource leaks during long-running agent sessions.

---

## Summary

- **Unified polling architecture** — all waits use `state.now() + timeout` deadlines with CDP-based condition checking
- **`waitForLoadState`** delegates to `waitForDocumentLoad` (load/DOMContentLoaded) or `waitForNetworkIdle` (networkidle)
- **`waitForURL`** combines flexible URL matching with optional post-navigation load state waiting
- **`waitForSelector`** distinguishes `"attached"` (DOM presence) from `"visible"` (rendered, interactive) via JavaScript evaluation
- **Network waits** (`waitForRequest`/`waitForResponse`) leverage CDP Network domain with synchronous matcher constraints
- **Resource safety** — guaranteed cleanup of CDP domains and handles via shared timeout/cleanup logic

---

## Frequently Asked Questions

### What is the default timeout for ego-lite wait functions?

Wait functions fall back to `state.defaultTimeout` when no explicit timeout is provided. According to [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts), this provides a consistent baseline across all operations, though the exact millisecond value depends on the state configuration at initialization.

### Why does waitForSelector have a 300ms polling interval?

The 300ms fixed interval in `waitForSelector` (lines 100-108) balances responsiveness with CDP communication overhead. Unlike Playwright's `requestAnimationFrame`-based approach, this predictable timing helps AI agents reason about execution timing without browser frame-layer complexity.

### Can I use async predicates with waitForRequest or waitForResponse?

No. The `networkMatches` helper in [`src/driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/waits.ts) explicitly detects and throws on async predicates (lines 330-337). This restriction exists because CDP event handlers execute synchronously — use synchronous functions that inspect request/response properties directly.

### How does networkidle detection differ from Playwright?

ego-lite's `waitForNetworkIdle` (lines 462-473) uses CDP Network domain events to track in-flight requests, similar to Playwright, but exposes `idleMs` for direct configuration. The implementation in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) provides the `drainEvents` utility for event queue management, tailored for agent automation rather than heavy testing workloads.