Ego-Lite Download Tracking System and How drainEvents Works

Ego-lite tracks file downloads through Chrome DevTools Protocol (CDP) events in 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 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.

Step 1: Create an Isolated Download Directory

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

// 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:

// 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
// 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:

// 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 simply forwards to the runtime implementation:

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

The actual implementation lives in 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
// 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

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

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 CDP event handling, temporary directory management, façade creation
Event draining (export) package/ego-browser/src/driver/observe.ts Public API surface for drainEvents()
Event draining (implementation) package/ego-browser/src/browser-runtime.ts Internal buffer management, drainBrowserEvents()
Public helpers package/ego-browser/src/helpers.ts Aggregated exports including waitForEvent and drainEvents
CDP utilities 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 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 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, 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, 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 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.

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 →