# Ego-Lite Download Tracking System and How drainEvents Works

> Discover ego-lite download tracking using Chrome DevTools Protocol events. Learn how drainEvents flushes the internal browser event buffer for efficient tracking.

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

---

**Ego-lite tracks file downloads through Chrome DevTools Protocol (CDP) events in [`src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/downloads.ts), while `drainEvents` flushes the internal browser event buffer via `drainBrowserEvents()` implemented in the browser runtime.**

Ego-lite is a lightweight browser automation framework that provides Playwright-style APIs for controlling headless Chrome. This article explains its two core mechanisms for managing browser state: the download tracking pipeline that captures file downloads without external dependencies, and the event draining system that ensures clean process termination. Both systems are implemented in the `ego-browser` package according to the [citrolabs/ego-lite](https://github.com/citrolabs/ego-lite) source code.

---

## How Ego-Lite's Download Tracking System Works

The download tracking system operates entirely through CDP events, creating an isolated temporary environment for each file transfer. The implementation resides in [`package/ego-browser/src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/downloads.ts).

### Step 1: Create an Isolated Download Directory

When `page.waitForEvent('download')` is invoked, the system generates a unique sandboxed folder:

```typescript
// downloads.ts - temporary folder creation
const downloadDir = path.join(os.tmpdir(), `ego-browser-downloads-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fs.mkdirSync(downloadDir, { recursive: true });

```

This directory is automatically cleaned up on process exit via `fs.rmSync(downloadDir, { recursive: true, force: true })`.

### Step 2: Configure Browser Download Behavior

The system instructs Chrome where to save files using CDP's `setDownloadBehavior`:

```typescript
// downloads.ts - CDP configuration (lines 94-112)
await cdp.send('Browser.setDownloadBehavior', {
  behavior: 'allow',
  downloadPath: downloadDir
}).catch(() => 
  // Fallback to Page-scoped behavior for older Chrome versions
  cdp.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: downloadDir
  })
);

```

**Note:** The fallback ensures compatibility across Chrome versions where `Browser.setDownloadBehavior` may not be available.

### Step 3: Capture CDP Download Events

Two event listeners track the download lifecycle:

- **`Page.downloadWillBegin`** — captures the download GUID, source URL, and suggested filename
- **`Page.downloadProgress`** — monitors state transitions until completion or cancellation

```typescript
// downloads.ts - event handlers (lines 58-70)
cdp.on('Page.downloadWillBegin', ({ guid, url, suggestedFilename }) => {
  downloadState.guid = guid;
  downloadState.url = url;
  downloadState.suggestedFilename = suggestedFilename || guid;
});

cdp.on('Page.downloadProgress', ({ guid, state }) => {
  if (guid === downloadState.guid && (state === 'completed' || state === 'canceled')) {
    downloadState.completed = true;
    downloadState.state = state;
  }
});

```

### Step 4: Return a Download Facade Object

Once completed, the system returns an object exposing Playwright-compatible methods:

```typescript
// downloads.ts - download façade (lines 81-90)
{
  suggestedFilename: () => downloadState.suggestedFilename,
  url: () => downloadState.url,
  path: () => path.join(downloadDir, downloadState.suggestedFilename),
  saveAs: async (targetPath: string) => {
    await fs.promises.copyFile(
      path.join(downloadDir, downloadState.suggestedFilename),
      targetPath
    );
  }
}

```

---

## How drainEvents Works in Ego-Lite

The `drainEvents` mechanism ensures all buffered browser events are processed before process termination. This is critical for capturing late-arriving CDP messages like console logs or network responses.

### The Export Chain: observe.ts to browser-runtime.ts

The public API surface in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) simply forwards to the runtime implementation:

```typescript
// observe.ts - drainEvents export (lines 45-46)
export function drainEvents(): Promise<object[]> {
  return drainBrowserEvents();
}

```

The actual implementation lives in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) as `drainBrowserEvents()`.

### What drainBrowserEvents Actually Does

The runtime maintains an internal event buffer that accumulates CDP messages between consumer polling cycles. `drainBrowserEvents()`:

1. **Atomically extracts** all pending events from the buffer
2. **Resolves immediately** if the buffer is empty
3. **Forces event loop advancement** to the `beforeExit` phase

This "draining" action guarantees:
- No events are lost between script completion and process exit
- The Node.js event loop reaches a stable state
- File descriptor cleanup occurs predictably

```javascript
// Typical usage pattern
const download = await page.waitForEvent('download');
await download.saveAs('/target/path.pdf');

// Flush any remaining console messages, network events, etc.
const buffered = await page.drainEvents();
console.log('Captured events:', buffered.length);

```

---

## Complete Working Examples

### Example 1: Full Download Workflow

```javascript
const { chromium } = require('ego-lite');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  // Navigate to a page with a download trigger
  await page.goto('https://example.com/reports');
  
  // Start download and wait for completion
  const [download] = await Promise.all([
    page.waitForEvent('download'),           // blocks until CDP signals completion
    page.click('#download-report-button')     // triggers the download
  ]);
  
  console.log('Filename:', download.suggestedFilename());
  console.log('Source URL:', download.url());
  
  // Persist to permanent location
  await download.saveAs(`./downloads/${download.suggestedFilename()}`);
  
  await browser.close();
})();

```

### Example 2: Event Draining with Error Handling

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

// Capture all console messages during navigation
const consoleMessages = [];
page.on('console', msg => consoleMessages.push(msg.text()));

await page.goto('https://example.com');

// Ensure we got everything, including late messages
const pendingEvents = await page.drainEvents();
console.log('Total events drained:', pendingEvents.length);

// pendingEvents contains structured objects:
// [{ type: 'console', level: 'log', text: '...', location: {...} }, ...]

```

---

## Key Implementation Files

| Component | File Path | Purpose |
|-----------|-----------|---------|
| Download tracking | [`package/ego-browser/src/driver/downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/downloads.ts) | CDP event handling, temporary directory management, façade creation |
| Event draining (export) | [`package/ego-browser/src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/observe.ts) | Public API surface for `drainEvents()` |
| Event draining (implementation) | [`package/ego-browser/src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) | Internal buffer management, `drainBrowserEvents()` |
| Public helpers | [`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) | Aggregated exports including `waitForEvent` and `drainEvents` |
| CDP utilities | [`package/ego-browser/src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts) | Low-level Chrome DevTools Protocol wrappers |

---

## Summary

- **Ego-lite download tracking** uses CDP's `Page.downloadWillBegin` and `Page.downloadProgress` events with sandboxed temporary directories—no external download managers required
- The **`waitForEvent('download')`** API returns a façade object with `suggestedFilename()`, `url()`, `path()`, and `saveAs()` methods
- **`drainEvents()`** flushes the internal browser event buffer through `drainBrowserEvents()` in the runtime, ensuring clean process termination
- Both systems are exposed through [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) for zero-configuration access in agent scripts

---

## Frequently Asked Questions

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

The `downloadProgress` event handler in [`downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/downloads.ts) explicitly checks for `state === 'canceled'` alongside `completed`. When canceled, the promise from `waitForEvent('download')` still resolves, but the resulting façade will indicate the canceled state. The temporary file is cleaned up on process exit regardless of outcome.

### Does ego-lite require Playwright or Puppeteer for download tracking?

No. According to the source code in [`downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/downloads.ts), ego-lite implements download tracking natively through Chrome DevTools Protocol events. The API surface mirrors Playwright's design for familiarity, but the implementation has zero external browser automation dependencies.

### Why is drainEvents necessary instead of just awaiting async operations?

Browser events like console messages and network responses can arrive asynchronously through CDP even after your script's promises resolve. `drainEvents()` forces the Node.js event loop to process these remaining callbacks before exit, preventing event loss. As implemented in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts), this is particularly important for short-lived automation scripts that terminate immediately after their main logic completes.

### Can multiple downloads be tracked simultaneously?

Yes. The [`downloads.ts`](https://github.com/citrolabs/ego-lite/blob/main/downloads.ts) implementation creates a unique temporary directory per `waitForEvent('download')` call, identified by `process.pid`, timestamp, and random suffix. Each download operation is isolated in its own sandbox, so concurrent downloads do not interfere with each other's file paths or state tracking.