# How Ego-Browser Implements Download Tracking and the waitForEvent System

> Discover how ego-browser implements download tracking and the waitForEvent system using CDP messages for seamless file download monitoring. Learn more about this Playwright compatible API.

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

---

**Ego-browser provides a Playwright-compatible `page.waitForEvent` API that currently supports only the download event, using CDP messages to track file downloads from start to completion.**

The `waitForEvent` system in ego-browser (from the [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) repository) gives automation scripts a reliable way to capture file downloads. This article breaks down the implementation across four core modules, explains the step-by-step download flow, and shows practical code examples.

## The Architecture of waitForEvent and Download Tracking

The **download tracking and waitForEvent system** spans two primary modules with support from runtime utilities. Each layer has a distinct responsibility:

| Module | File Path | Purpose |
|--------|-----------|---------|
| Download engine | [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts) | Orchestrates CDP communication, folder setup, and download lifecycle |
| Public API | [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Exposes `page.waitForEvent` and validates event names |
| Event primitive | [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Provides `waitForBrowserEvent` for generic CDP message waiting |
| Configuration | [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Supplies `defaultTimeout` and other runtime settings |

### Key Design Decisions

The implementation favors **temporary sandboxing** over persistent directories. Each `waitForEvent('download')` call creates a unique temporary folder rather than polluting system Downloads folders. This isolation prevents collisions when multiple concurrent downloads occur.

CDP protocol version differences are handled gracefully. The code attempts `Browser.setDownloadBehavior` first, then falls back to `Page.setDownloadBehavior` if the browser doesn't support the modern command.

## How Download Tracking Works Step-by-Step

Understanding the **download tracking internals** helps debug failures and extend the system. Here's the exact flow in [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts):

### 1. Temporary Directory Creation

```typescript
const downloadDir = path.join(tmpdir(), `ego-download-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(downloadDir, { recursive: true });

```

The path includes process ID, timestamp, and random suffix to guarantee uniqueness across parallel browser instances.

### 2. Configuring Browser Download Behavior

```typescript
async function setDownloadBehavior(downloadDir: string): Promise<void> {
  try {
    await sendCDPCommand('Browser.setDownloadBehavior', {
      behavior: 'allowAndName',
      downloadPath: downloadDir,
      eventsEnabled: true
    });
  } catch {
    // Fallback for older Chrome versions
    await sendCDPCommand('Page.setDownloadBehavior', {
      behavior: 'allow',
      downloadPath: downloadDir
    });
  }
}

```

The `eventsEnabled: true` flag is critical — without it, the browser won't emit `downloadWillBegin` or `downloadProgress` events.

### 3. Waiting for CDP Download Events

The download tracker uses `waitForBrowserEvent` from [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) twice:

```typescript
// First: capture the download GUID and metadata
const willBeginEvent = await waitForBrowserEvent(
  (msg) => msg.method === "Page.downloadWillBegin",
  timeout
);
const { guid, suggestedFilename, url } = willBeginEvent.params;

// Second: track completion status
const progressEvent = await waitForBrowserEvent(
  (msg) => msg.method === "Page.downloadProgress" && 
           msg.params.guid === guid &&
           (msg.params.state === "completed" || msg.params.state === "canceled"),
  timeout
);

```

The **GUID matching** ensures correct tracking when multiple downloads overlap.

### 4. Building the Download Façade

On completion, the system returns an object with four methods:

| Method | Returns | Description |
|--------|---------|-------------|
| `suggestedFilename()` | `string` | Original filename from HTTP headers or URL |
| `url()` | `string` | Source URL where download originated |
| `path()` | `Promise<string>` | Absolute path to temporary file location |
| `saveAs(targetPath)` | `Promise<void>` | Copies file to permanent destination |

If the progress event reports `state: "canceled"`, the function throws: `Download canceled: ${suggestedFilename}`.

## The waitForEvent API Reference

The public `page.waitForEvent` interface in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) is intentionally minimal:

```typescript
waitForEvent: (eventName: 'download', options?: { timeout?: number }) => Promise<DownloadFaçade>

```

**Validation logic** rejects unsupported events:

```typescript
if (eventName !== 'download') {
  throw new Error(`waitForEvent: unsupported event "${eventName}". Only "download" is currently supported.`);
}

```

The `timeout` parameter defaults to `state.defaultTimeout` (defined in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)). This centralizes timeout configuration across all wait operations.

## Complete Code Examples

### Basic Download Capture

```javascript
const page = await browser.newPage();

// Trigger download via click or navigation
const [download] = await Promise.all([
  page.waitForEvent('download'),
  page.click('a#export-report')  // element that initiates download
]);

console.log('Filename:', download.suggestedFilename());
console.log('From:', download.url());
console.log('Temp location:', await download.path());

```

### Saving to Permanent Location

```javascript
const download = await page.waitForEvent('download');

// Copy from temp sandbox to user-chosen destination
await download.saveAs('/home/user/Documents/quarterly-report.pdf');

// Optional: verify file exists
const fs = require('fs');
const stats = fs.statSync('/home/user/Documents/quarterly-report.pdf');
console.log(`Saved ${stats.size} bytes`);

```

### Custom Timeout with Error Handling

```javascript
try {
  const download = await page.waitForEvent('download', { 
    timeout: 60_000  // 60 second limit for large files
  });
  
  const tempPath = await download.path();
  await download.saveAs('/mnt/secure-storage/archive.zip');
  
} catch (err) {
  if (err.message.includes('timeout')) {
    console.error('Download did not start or complete within 60 seconds');
  } else if (err.message.includes('canceled')) {
    console.error('Download was canceled by user or server');
  } else {
    throw err;
  }
}

```

## Extending waitForEvent for Other Event Types

The current **waitForEvent system** is architected for expansion. The pattern in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) supports additional CDP events:

```typescript
// Generic primitive used by downloads and available for extension
function waitForBrowserEvent<T>(
  predicate: (msg: CDPMessage) => boolean,
  timeout: number
): Promise<T>

```

Potential future events following this pattern:

- `'request'` and `'response'` — network interception
- `'dialog'` — JavaScript alerts and confirms
- `'filechooser'` — native file picker dialogs

Each would add a branch in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) validation and a dedicated handler module paralleling [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts).

## Performance and Resource Considerations

| Aspect | Behavior |
|--------|----------|
| Disk usage | Temporary folders persist until `saveAs()` copies or manual cleanup |
| Memory footprint | Minimal — only GUID and metadata held, not file contents |
| Concurrent downloads | Supported via unique temp directories per `waitForEvent` call |
| Timeout handling | Uses `Promise.race` with cleanup to prevent listener leaks |

The **temporary folder retention** is intentional. The façade doesn't auto-delete because:
- Scripts may want to inspect files before deciding to keep
- `saveAs` might fail (permission issues, disk full) and need retry
- Parallel processing pipelines may handle files asynchronously

## Summary

- **Download tracking** uses Chrome DevTools Protocol messages (`Page.downloadWillBegin`, `Page.downloadProgress`) intercepted via `waitForBrowserEvent` in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts)

- **Temporary sandboxing** creates isolated download directories per call, avoiding file collisions and system folder pollution

- **`page.waitForEvent`** currently supports only the `'download'` event, validated in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) before delegating to `waitForDownload` in [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts)

- **The download façade** exposes `suggestedFilename()`, `url()`, `path()`, and `saveAs()` for script-friendly file handling without manual path management

- **Timeout defaults** come from `state.defaultTimeout` in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts), overrideable per call

## Frequently Asked Questions

### What events does `page.waitForEvent` support in ego-browser?

Currently only the **download** event. The helper in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) explicitly validates the event name and throws for any other value. The architecture supports future expansion for network, dialog, and file chooser events.

### Why does the download go to a temporary folder instead of my Downloads directory?

Each `waitForEvent('download')` call creates a **unique temporary directory** to isolate downloads. This prevents filename collisions during parallel execution and gives scripts explicit control via `saveAs()` for permanent placement. The temporary path is accessible via `await download.path()`.

### How do I change the default timeout for all waitForEvent calls?

Modify `state.defaultTimeout` in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) or pass `{ timeout: milliseconds }` to individual calls. The default applies across all wait operations including `waitForSelector` and `waitForFunction`.

### What happens if a download is canceled by the user or server?

The progress listener in [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts) detects `state: "canceled"` and throws `Download canceled: ${filename}`. Wrap `waitForEvent` in try/catch to handle this case, or check for cancellation explicitly if extending the download tracker.