# How Page Events Are Buffered and Drained in the Ego Lite Runtime

> Discover how Ego Lite buffers CDP events in memory and drains them using drainBrowserEvents. Learn about real-time callbacks and predicate-based resolution with waitForBrowserEvent.

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

---

**The Ego‑Lite runtime buffers CDP events in a capped in‑memory array and drains them via `drainBrowserEvents()`, with subscribers receiving real‑time callbacks and `waitForBrowserEvent()` enabling predicate‑based resolution.**

The Ego‑Lite browser automation framework provides a robust event‑handling system for Chrome DevTools Protocol (CDP) messages. Understanding how page events are buffered and drained in the runtime is essential for building reliable agents that capture browser activity without exhausting memory. This article examines the implementation in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts), including buffer management, subscriber dispatch, and retrieval patterns.

## Event Buffer Architecture

The runtime maintains a single in‑memory queue for all incoming page events. This design prioritizes low latency over persistence, making it ideal for ephemeral automation tasks.

### Buffer Storage and Limits

The core buffer is defined in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts):

- **`events`** (line 20): An array that stores every ingested CDP event
- **`MAX_BUFFERED_EVENTS`** (line 8): A constant fixed at **10 000 entries**

When the buffer reaches capacity, the oldest events are discarded to maintain this bound. The enforcement logic appears at lines 88‑90, ensuring memory usage remains predictable during long‑running sessions.

## Event Ingestion Pipeline

The `handleMessage` callback (lines 31‑87) processes every raw CDP message from the browser:

1. Parses the message structure
2. Checks against **screencast frame** duplicates (already delivered to subscribers are skipped)
3. Invokes matching subscribers from `eventSubscribers` (lines 77‑85)
4. Appends the event to `events` (line 87)

This ordering—**notify first, buffer second**—ensures subscribers receive events immediately while the buffer captures history for later retrieval.

### Subscriber Matching Logic

Subscribers are stored in `eventSubscribers` and matched by:
- **`method`**: The CDP method name (e.g., `"Page.loadEventFired"`)
- **`sessionId`** (optional): For multi‑session browser connections

When both criteria match, the listener fires synchronously before buffering continues.

## Draining the Event Buffer

The runtime exports two primary mechanisms for consuming buffered events:

### `drainBrowserEvents()`

Located at lines 64‑66, this function empties the entire buffer in a single operation:

```typescript
import { drainBrowserEvents } from "./browser-runtime.js";

// Retrieve and clear all buffered events
const allEvents = drainBrowserEvents();
console.log(`Recovered ${allEvents.length} events from buffer`);

```

The implementation uses `events.splice(0, events.length)` for O(n) extraction and immediate queue reset. This pattern suits end‑of‑task cleanup or batch analysis scenarios.

### `waitForBrowserEvent(predicate, timeoutMs)`

For targeted event retrieval, lines 70‑86 expose a promise‑based waiter:

```typescript
import { waitForBrowserEvent } from "./browser-runtime.js";

// Pause execution until a specific network request completes
const targetEvent = await waitForBrowserEvent(
  (e) => e.method === "Network.responseReceived" 
    && e.params?.response?.url?.includes("/api/data"),
  5000  // 5 second timeout
);

```

Waiters register in `eventWaiters` (line 21) and evaluate against every incoming event (lines 92‑106). The first matching event resolves the promise and removes the waiter.

## Practical Usage Patterns

### Real‑Time Subscription

Capture live events without buffering overhead for the consumer:

```typescript
import { subscribeBrowserEvent } from "./browser-runtime.js";

const unsubscribe = subscribeBrowserEvent(
  "Page.navigatedWithinDocument",
  undefined,
  (event) => console.log("Fragment navigation:", event.params.url)
);

// Later: unsubscribe() to stop receiving callbacks

```

### Batch Event Recovery

Combine draining with processing for post‑action analysis:

```typescript
// After completing a workflow...
const buffered = drainBrowserEvents();

const networkErrors = buffered.filter(
  e => e.method === "Network.loadingFailed"
);

console.error(`Detected ${networkErrors.length} failed requests`);

```

## Key Implementation Files

| File | Role |
|------|------|
| [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) | Core buffer (`events`), `MAX_BUFFERED_EVENTS` enforcement, `drainBrowserEvents()`, `handleMessage()` dispatch loop, and `waitForBrowserEvent()` implementation (lines 8, 20‑21, 31‑106) |
| [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts) | Runtime‑wide configuration including default timeouts for waiter operations |
| [`driver/waits.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/waits.ts) | Consumer patterns using `drainBrowserEvents()` for network idle detection |

## Summary

- The Ego‑Lite runtime buffers CDP events in a **10 000‑entry capped array** to prevent unbounded memory growth
- **Subscribers receive synchronous callbacks** before events enter the buffer, eliminating latency for real‑time consumers
- **`drainBrowserEvents()`** provides bulk retrieval with complete buffer clearance via `splice()`
- **`waitForBrowserEvent()`** enables precise, predicate‑driven event resolution with configurable timeouts
- The **screencast frame deduplication** logic prevents duplicate deliveries for frame‑heavy operations

## Frequently Asked Questions

### What happens when the event buffer exceeds 10 000 entries?

The oldest events are automatically removed. The runtime enforces `MAX_BUFFERED_EVENTS` at lines 88‑90 of [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) using array truncation, preserving only the most recent entries.

### Can multiple subscribers receive the same event?

Yes. The `eventSubscribers` iteration at lines 77‑85 invokes every matching subscriber before buffering. There is no early‑return; all registered listeners for a given method and sessionId receive the event.

### How does `waitForBrowserEvent` differ from `subscribeBrowserEvent`?

`subscribeBrowserEvent` registers a persistent callback for all future matching events, while `waitForBrowserEvent` creates a one‑time promise that resolves on the first predicate match. Waiters are destroyed after resolution; subscribers persist until explicitly unsubscribed.

### Is the event buffer persisted to disk?

No. The `events` array exists only in memory. For durability, agents must call `drainBrowserEvents()` and store results externally before process termination.