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

> Discover how ego-browser's RefMap stores and retrieves backend node ID mappings using an in-memory Map. Learn about automatic population for seamless ref identifier linking.

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

---

**The RefMap in ego-browser uses an in-memory `Map<string, any>` in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) when refs are accessed before a snapshot exists.**

The `ego-browser` package from the [citrolabs/ego-lite](https://github.com/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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```ts
// 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:

```ts
// 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:

```ts
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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) normalizes varied user inputs into the canonical numeric string:

```ts
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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts)**. The `ensureRefMapForRef` function guarantees that refs always resolve to valid backend node IDs, even when accessed before any snapshot:

```ts
// 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

```ts
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

```ts
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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | Defines `RefMap` class, storage methods, and `parseRef` |
| [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`.