# How Ego-Lite Maps backendNodeId to Ref Numbers: Inside the Ref Map System

> Discover how ego-lite maps backendNodeId to ref numbers using its Ref Map system for concise element references in agent scripts. Learn the internal workings of this essential mapping.

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

---

**Ego-Lite maintains a Ref Map that assigns sequential numeric identifiers (e.g., `@21`) to Chrome DevTools Protocol backend node IDs, enabling concise element references in agent scripts.**

Ego-Lite is an open-source browser automation framework that simplifies interaction with the Chrome DevTools Protocol (CDP). When manipulating DOM elements, the system translates low-level `backendNodeId` values into human-readable **ref numbers** to streamline agent scripting. This mapping mechanism is central to how Ego-Lite handles element identification across snapshots and CDP calls.

## Understanding the Ref Map Architecture

Chrome DevTools Protocol identifies DOM nodes using opaque `backendNodeId` integers that are difficult to reference manually. Ego-Lite solves this usability challenge through an internal **Ref Map** data structure.

The Ref Map serves as a bidirectional lookup table stored in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts). It assigns each unique `backendNodeId` discovered during snapshot capture a short, sequential **ref number** prefixed with `@` (e.g., `@1`, `@2`, `@21`). This abstraction allows automation scripts to interact with elements using memorable identifiers rather than raw CDP values.

## The Four-Stage Mapping Workflow

The conversion from `backendNodeId` to ref number follows a precise lifecycle implemented across the Ego-Lite runtime:

### 1. Snapshot Capture

When `ego.snapshot()` is invoked, the browser runtime traverses the current DOM tree and records every visible node. Each node’s `backendNodeId`—the raw CDP identifier—is extracted and stored temporarily before ref assignment.

This process is orchestrated in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), which handles the CDP layer communication and ensures all DOM nodes are accounted for regardless of depth or visibility.

### 2. Ref Generation

Following snapshot capture, the Ref Map iterates over the collected `backendNodeId` values and creates numeric references for each. The mapping is stored as a `Map<number, BackendNodeId>` where the key represents the sequential ref number and the value stores the original CDP identifier.

```ts
// Type definition from src/ref-map.ts
type RefMap = Map<number, BackendNodeId>;

// Sequential assignment during snapshot processing
const refNumber = map.size + 1; // Generates 1, 2, 3...
map.set(refNumber, backendNodeId);

```

### 3. Runtime Lookup Resolution

When an agent script references an element using ref syntax, Ego-Lite performs a reverse lookup to retrieve the original CDP identifier. The resolution logic in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) strips the `@` prefix and queries the current Ref Map.

```ts
// Simplified lookup function (as implemented in src/ref-map.ts)
function resolveRef(ref: string): BackendNodeId {
  const refNum = Number(ref.slice(1)); // Strip leading '@'
  const backendId = currentRefMap.get(refNum);
  if (!backendId) throw new Error('Ref not found');
  return backendId;
}

```

This retrieved `backendNodeId` is then passed to subsequent CDP calls for actions like clicking, typing, or evaluating JavaScript against the specific element.

### 4. Automatic Refresh Mechanism

The Ref Map is invalidated during navigation or significant DOM mutations. If a script attempts to use a ref when the map is empty or stale, Ego-Lite automatically triggers `ego.snapshot()` before executing the command. This ensures refs remain valid without requiring manual state management from the agent.

```js
// Automatic refresh occurs transparently
await ego.type('@5', 'hello world'); // Triggers snapshot if map is empty

```

## Implementation in src/ref-map.ts and src/ref-state.ts

The core mapping logic resides in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), which exports functions for building the ref table and performing lookups. The module maintains the invariant that ref numbers are dense (no gaps) and start from 1 for each snapshot session.

Related state persistence is handled in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), which stores the current Ref Map instance and provides helper functions for determining when a refresh is necessary. This separation of concerns allows the mapping engine to remain stateless while the state module manages snapshot lifecycle and map validity.

## Usage Examples

Agent scripts interact with the ref system through high-level commands that accept `@<number>` syntax:

```js
// Populate the Ref Map explicitly
await ego.snapshot();

// Use refs to interact with elements
await ego.click('@12');
await ego.type('@5', 'hello world');

```

Behind the scenes, these calls resolve to CDP commands using the stored `backendNodeId` mappings from [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts). If the map lacks the requested ref, [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) orchestrates an automatic snapshot refresh before retrying the operation.

## Summary

- **Ego-Lite** translates CDP `backendNodeId` values into human-readable **ref numbers** (e.g., `@21`) via an internal Ref Map.
- The mapping is created during **snapshot capture** in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) and stored as a `Map<number, BackendNodeId>` in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts).
- **Ref resolution** occurs in [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts), converting `@` prefixed strings back to `backendNodeId` values for CDP calls.
- An **automatic refresh mechanism** ensures the Ref Map stays synchronized with DOM changes without manual intervention.
- State management is delegated to [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), which tracks map validity across navigation events.

## Frequently Asked Questions

### What happens if I use a ref number that doesn't exist?

If you reference an invalid ref like `@999` when no such mapping exists in the current Ref Map, Ego-Lite throws a "Ref not found" error. The system does not automatically create mappings for non-existent refs; you must first capture a snapshot that includes the target element.

### How long do ref numbers remain valid?

Ref numbers are valid only for the current snapshot session. If the page navigates or the DOM structure changes significantly, the Ref Map is cleared. However, Ego-Lite's automatic refresh mechanism in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) triggers a new snapshot transparently when you next attempt to use a ref, regenerating valid mappings for the current page state.

### Can I predict which ref number an element will receive?

No, ref numbers are assigned sequentially based on the order elements are discovered during DOM traversal during snapshot capture. The specific number assigned to an element depends on its position in the DOM tree and the traversal algorithm implemented in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), making refs unsuitable for stable long-term element identification across sessions.

### Where is the Ref Map stored during execution?

The active Ref Map instance is maintained in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts), which exports getter and setter functions for the current map. This module ensures that [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) and other consumers always access the most recent snapshot data without direct dependency on the snapshot trigger logic.