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 noderole– The resolved accessibility role (e.g.,"button","link")name– The accessible name of the elementnth– Optional index for repeated selector matchesselector– Populated later if a stable selector is generatedframeId(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-symbolref=21– key-value style21– 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:
- Check if the ref map contains any entries
- If empty, invoke the registered
snapshotImplcallback to capture current page state - 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
Mapwith O(1) access - Storage occurs via
add()oraddWithFrame()during snapshot processing - Retrieval uses
get()with normalized ref strings fromparseRef() - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →