# How the Ego-Lite Reference System Maps Numeric Backend Node IDs

> Discover how the ego-lite reference system maps numeric backend node IDs using RefMap. Learn to quickly find DOM nodes with @N syntax for efficient debugging and development.

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

---

**The ego-lite reference system uses a RefMap class to store CDP backendNodeId values as string keys, enabling fast lookup of DOM nodes via @N syntax.**

The **citrolabs/ego-lite** library bridges the gap between raw Chrome DevTools Protocol (CDP) identifiers and human-readable references. When automating browser interactions, developers need stable ways to refer to DOM elements without complex selectors. This article explains how the **ego-lite reference system** maps numeric backend node IDs to ergonomic tokens like `@21` using a lightweight storage architecture.

## The Core Architecture: RefMap and Backend Node Storage

The foundation of the mapping system is the `RefMap` class defined in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts). This class maintains an internal `Map<string, any>` where string representations of numeric IDs serve as keys to objects containing the actual `backendNodeId` and metadata.

### The RefMap Class Structure

In [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts), the `RefMap` class initializes a private `map` property in its constructor. The class provides methods to add, retrieve, and clear reference entries while maintaining the association between user-facing tokens and internal CDP identifiers.

```typescript
// src/ref-map.ts
export class RefMap {
  map: Map<string, any>;

  constructor() {
    this.map = new Map();
  }

  add(refId, backendNodeId, role, name, nth = undefined) {
    this.addWithFrame(refId, backendNodeId, role, name, nth, undefined);
  }

  addWithFrame(
    refId,
    backendNodeId,
    role,
    name,
    nth = undefined,
    frameId = undefined,
  ) {
    this.map.set(refId, {
      backendNodeId,
      role,
      name,
      nth,
      selector: undefined,
      frameId,
    });
  }

  get(refId) {
    return this.map.get(refId);
  }
}

```

### How References Are Stored

When storing a reference, the `backendNodeId` is preserved as a numeric property within the value object, while the map key remains a string. This design allows the system to echo the CDP's unique identifier back to users as a concise `@N` token while maintaining the full node metadata needed for interaction.

## Parsing User Input with parseRef

The system accepts multiple input formats through the `parseRef` utility function, also located in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts). This function normalizes various reference syntaxes—such as `@21`, `ref=21`, or plain `21`—into a consistent string key for map lookup.

```typescript
// src/ref-map.ts
export function parseRef(input) {
  const trimmed = String(input || "").trim();
  for (const candidate of [
    trimmed.startsWith("@") ? trimmed.slice(1) : null,
    trimmed.startsWith("ref=") ? trimmed.slice(4) : null,
    trimmed,
  ]) {
    if (candidate && /^\d+$/.test(candidate)) {
      return candidate;               // → "21"
    }
  }
  return null;
}

```

The function validates that the parsed candidate consists only of digits using a regular expression. If no valid numeric string is found, it returns `null`, preventing invalid references from reaching the lookup mechanism.

## Snapshot Population and Lookup Flow

The reference system operates dynamically during browser snapshots, populating the map with current DOM state and handling lookups with automatic refresh capabilities.

### Populating During Snapshots

During snapshot creation in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), the runtime iterates over DOM elements reported by the CDP. For each element, it assigns a temporary numeric ref ID that mirrors the `backendNodeId`, then calls `refMap.add(refId, backendNodeId, …)` to populate the `RefMap`.

```typescript
// src/browser-runtime.ts (excerpt)
if (ref.backendNodeId === undefined || ref.backendNodeId === null) {
  // a new snapshot will assign a fresh numeric ref that matches the backend node ID
}

```

This ensures that the `RefMap` contains an entry for every accessible node immediately after a snapshot completes.

### Runtime Lookup and Automatic Re-Snapshot

When a script requests a reference like `@21`, the resolution pipeline first calls `parseRef` to obtain the string key `"21"`, then queries `refMap.get("21")`. If the entry is missing—indicating the DOM has changed since the last snapshot—the runtime automatically triggers a fresh snapshot to repopulate the map.

```typescript
// Example: Resolve a user-provided reference to a backend node ID
import { RefMap, parseRef } from "./ref-map.js";

const refMap = new RefMap();

// Simulate a snapshot that discovered an element with backendNodeId 42
refMap.add("42", 42, "button", "Submit");

// Later, a script passes "@42"
function resolveUserRef(input: string) {
  const refId = parseRef(input);           // → "42"
  const entry = refMap.get(refId);
  if (!entry) throw new Error("Reference not found");
  return entry.backendNodeId;               // → 42
}

console.log(resolveUserRef("@42")); // prints 42

```

```typescript
// Example: Automatic re‑snapshot when a reference is missing
async function getBackendNodeId(refStr: string) {
  let id = resolveUserRef(refStr);
  if (id === undefined) {
    await takeSnapshot();                  // repopulates RefMap
    id = resolveUserRef(refStr);
  }
  return id;
}

```

This architecture guarantees that **numeric backend node IDs** remain resolvable even as the underlying page mutates, with [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) handling the actual DOM interaction using the resolved IDs.

## Summary

- The **RefMap** class in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) stores CDP `backendNodeId` values as string keys mapped to metadata objects containing the numeric ID and element properties.
- The **parseRef** function normalizes multiple input syntaxes (`@N`, `ref=N`, `N`) into consistent string keys for map lookup.
- **Snapshot population** in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) automatically registers DOM elements by their `backendNodeId`, ensuring the reference map stays synchronized with the current page state.
- **Automatic re-snapshotting** occurs when lookups fail, preventing stale references from causing errors after navigation or DOM mutations.
- The system leverages CDP's guarantee that `backendNodeId` values are unique per page lifetime, eliminating the need for additional indirection layers.

## Frequently Asked Questions

### What is a backendNodeId in Chrome DevTools Protocol?

A `backendNodeId` is a unique numeric identifier assigned by the Chrome DevTools Protocol to every DOM node for the lifetime of the page. According to the CDP specification, these IDs remain stable until the page navigates or the node is garbage collected, making them reliable anchors for automation tools like ego-lite to track elements across operations.

### How does ego-lite handle stale references after DOM changes?

The reference system clears the `RefMap` on every new snapshot and implements automatic re-snapshot logic when lookups fail. If a script requests `@21` and `refMap.get("21")` returns undefined, the runtime triggers a fresh snapshot via [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), repopulating the map with current `backendNodeId` values and ensuring references always resolve to live nodes.

### What input formats does the parseRef function support?

The `parseRef` function in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) accepts three formats: the `@` prefix (e.g., `@21`), the `ref=` prefix (e.g., `ref=21`), or a plain numeric string (e.g., `21`). It validates each candidate against a `/^\d+$/` regular expression, returning the first valid numeric string or `null` if no valid reference is found.

### Where is the RefMap instantiated and used?

The `RefMap` class is defined in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) and utilized across multiple files including [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) for population during snapshots, [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) for resolving locators to DOM nodes, and [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) where public helper functions like `ref()` internally call `parseRef` and query the map instance.