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

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. 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, 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.

// 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. This function normalizes various reference syntaxes—such as @21, ref=21, or plain 21—into a consistent string key for map lookup.

// 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, 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.

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

// 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
// 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 handling the actual DOM interaction using the resolved IDs.

Summary

  • The RefMap class in 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 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, 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 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 and utilized across multiple files including src/browser-runtime.ts for population during snapshots, src/element-resolver.ts for resolving locators to DOM nodes, and src/helpers.ts where public helper functions like ref() internally call parseRef and query the map instance.

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 →