How the RefMap in ego-browser Stores and Retrieves Backend Node ID Mappings

The RefMap in ego-browser uses an in-memory Map<string, any> in src/ref-map.ts to link textual ref identifiers (like @21) to Chrome DevTools Protocol backend node IDs, with automatic population via src/ref-state.ts when refs are accessed before a snapshot exists.

The ego-browser package from the citrolabs/ego-lite repository provides a lightweight browser automation runtime that bridges high-level agent instructions with Chrome's debugging protocol. At the heart of this bridge lies the Ref Map—a registry that translates human-readable refs into the low-level backendNodeId values required for DOM manipulation.

RefMap Data Structure and Storage Format

The core implementation resides in src/ref-map.ts. The RefMap class wraps a native JavaScript Map where each entry connects a ref string to a metadata object containing:

  • backendNodeId – The CDP identifier for the DOM node
  • role – The resolved accessibility role (e.g., "button", "link")
  • name – The accessible name of the element
  • nth – Optional index for repeated selector matches
  • selector – Populated later if a stable selector is generated
  • frameId (optional) – The frame context for iframe support

Adding Mappings to the RefMap

When a page snapshot is captured, the system extracts node information and populates the map via RefMap.add() or RefMap.addWithFrame() for cross-frame elements:

// src/ref-map.ts – addWithFrame implementation
this.map.set(refId, {
  backendNodeId,
  role,
  name,
  nth,
  selector: undefined,
  frameId,
});

The refId is a stringified number (e.g., "21") that serves as the canonical key. This design choice ensures consistent lookup regardless of how users format their ref inputs.

Retrieving Backend Node IDs from RefMap

Consumers retrieve entries through RefMap.get(refId), which returns the complete metadata object:

// src/ref-map.ts – get implementation
get(refId) {
  return this.map.get(refId);
}

This direct map access provides O(1) lookup performance. Callers extract the backendNodeId property to construct CDP commands for DOM operations:

import { parseRef, browserRefMap } from "./ref-state.js";

async function focusElement(refInput) {
  const refId = parseRef(refInput);     // normalize "@42" → "42"
  const entry = browserRefMap.get(refId);
  
  if (!entry) throw new Error(`Unknown ref: ${refInput}`);
  
  await ego.sendCDPMessage("DOM.focus", {
    backendNodeId: entry.backendNodeId,
  });
}

Parsing Ref Strings with parseRef

The parseRef utility in src/ref-map.ts normalizes varied user inputs into the canonical numeric string:

export function parseRef(input) {
  const trimmed = String(input || "").trim();
  // …try three candidates…
  if (candidate && /^\d+$/.test(candidate)) return candidate;
  return null;
}

Supported formats include:

  • @21 – prefixed with at-symbol
  • ref=21 – key-value style
  • 21 – plain number

The function returns null for invalid inputs, enabling consistent error handling upstream.

Automatic RefMap Population via ref-state.ts

A critical reliability feature lives in src/ref-state.ts. The ensureRefMapForRef function guarantees that refs always resolve to valid backend node IDs, even when accessed before any snapshot:

// src/ref-state.ts – ensureRefMapForRef core logic
if (browserRefMap.map.size > 0) return;
await snapshotImpl();   // repopulate the map

This lazy initialization pattern works as follows:

  1. Check if the ref map contains any entries
  2. If empty, invoke the registered snapshotImpl callback to capture current page state
  3. Proceed with the now-populated map

The singleton browserRefMap instance and its snapshot callback are managed in this same file, creating a cohesive state layer above the raw RefMap storage.

Complete Usage Examples

Adding entries after a snapshot capture

import { browserRefMap } from "./ref-state.js";

function processSnapshotNodes(nodes) {
  for (const nodeInfo of nodes) {
    const refId = String(nodeInfo.index);  // generated ref number
    browserRefMap.add(
      refId,
      nodeInfo.backendNodeId,
      nodeInfo.role,
      nodeInfo.name,
      nodeInfo.nth
    );
  }
}

Safe ref access with automatic population

import { ensureRefMapForRef, browserRefMap } from "./ref-state.js";
import { parseRef } from "./ref-map.js";

async function clickByRef(refInput) {
  await ensureRefMapForRef(refInput);  // triggers snapshot if needed
  
  const refId = parseRef(refInput);
  const entry = browserRefMap.get(refId);
  
  await ego.sendCDPMessage("DOM.resolveNode", {
    backendNodeId: entry.backendNodeId,
  });
}

Key Implementation Files

File Purpose
src/ref-map.ts Defines RefMap class, storage methods, and parseRef
src/ref-state.ts Singleton management, lazy snapshot integration

Summary

  • RefMap stores ref-to-backendNodeId mappings in a native Map with O(1) access
  • Storage occurs via add() or addWithFrame() during snapshot processing
  • Retrieval uses get() with normalized ref strings from parseRef()
  • Automatic refresh via ensureRefMapForRef() prevents "ref not found" errors
  • Frame awareness enables cross-iframe element targeting through optional frameId

Frequently Asked Questions

What happens if I request a ref before taking a snapshot?

The ensureRefMapForRef function in src/ref-state.ts detects an empty map and automatically triggers a snapshot before proceeding. This lazy initialization guarantees valid backend node IDs without requiring explicit setup.

Can RefMap handle elements inside iframes?

Yes. Use addWithFrame() instead of add() to include the frameId parameter. This stores the frame context alongside the backendNodeId, enabling CDP commands to target the correct execution context.

Why does parseRef return a string instead of a number?

The map uses string keys to ensure consistent lookup regardless of input formatting. While the ref represents a numeric index, string keys avoid type coercion issues when comparing "21" (from @21) against 21 (from plain input).

How do I check if a ref exists without triggering a snapshot?

Access browserRefMap.map.size directly or call browserRefMap.get(parseRef(input)) and check for undefined. These operations bypass the automatic snapshot logic in ensureRefMapForRef.

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 →