# How Ref-Map Rebuilds on Every Snapshot and What Triggers Auto Re-snapshot in ego-lite

> Discover how ego-lite rebuilds its ref-map on every snapshot and the triggers for automatic re-snapshotting. Learn about browserSnapshotRefsToRefMap and snapshot references.

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

---

**In ego-lite, every snapshot triggers a complete ref-map rebuild via `browserSnapshotRefsToRefMap`, and automatic re-snapshoting occurs when helpers receive a snapshot reference (`@id`) while the ref-map is empty.**

The **ref-map** is a core data structure in ego-lite that maps snapshot reference IDs (e.g., `@23`) to underlying browser metadata like `backendNodeId`, ARIA role, and accessible name. Maintaining this mapping correctly across page changes is essential for reliable DOM interaction.

## How the Ref-Map Rebuilds on Every Snapshot

When you capture a page snapshot in ego-lite, the browser runtime returns an array of **ref descriptors**—one for each captured element. The system then reconstructs the ref-map from scratch to ensure consistency.

### The Rebuild Process in `browserSnapshotRefsToRefMap`

Located in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), this function implements a **clear-and-repopulate** strategy:

```typescript
// src/browser-runtime.ts
export function browserSnapshotRefsToRefMap(refMap, refs = []) {
  refMap.clear();                         // <‑‑ discard stale entries
  for (const ref of refs) {
    if (!ref || typeof ref !== "object") continue;
    if (ref.backendNodeId == null) continue;
    refMap.add(
      String(ref.backendNodeId),           // ref-id (e.g., "23")
      ref.backendNodeId,
      ref.role,
      ref.name,
      undefined,
    );
  }
}

```

**Three guarantees this provides:**

1. **No stale references** — previous snapshot data is discarded immediately
2. **Consistent state** — the map always reflects the current DOM
3. **Validation** — entries without `backendNodeId` are filtered out

This function is invoked internally by `snapshotRaw` (in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)), meaning **every explicit or implicit snapshot refreshes the entire ref-map**.

## What Triggers Automatic Re-snapshot in ego-lite

ego-lite detects when the ref-map is stale and automatically captures a fresh snapshot before executing operations. This prevents "ghost" references to DOM nodes that no longer exist.

### The `ensureRefMapForRef` Guard

All helpers that accept either a CSS selector or snapshot reference—`elementCenter`, `click`, `type`, and others—first call `ensureRefMapForRef` from [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts):

```typescript
// src/ref-state.ts
export async function ensureRefMapForRef(selectorOrRef: unknown) {
  if (ensuring) return;                    // prevent concurrent execution
  if (typeof selectorOrRef !== "string") return;
  if (!parseRef(selectorOrRef)) return;    // not a @ref format
  if (browserRefMap.map.size > 0) return;  // map already populated
  if (!snapshotImpl) return;               // no impl registered

  ensuring = true;
  try {
    await snapshotImpl();                  // <-- triggers fresh snapshot
  } finally {
    ensuring = false;
  }
}

```

**Trigger conditions (all must pass):**

- The argument is a **string** starting with `@` (validated by `parseRef`)
- The **current ref-map is empty** (`browserRefMap.map.size === 0`)
- A **snapshot implementation is registered**

### Snapshot Implementation Registration

The actual snapshot function is registered once during module initialization in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts):

```typescript
// src/driver/observe.ts
registerSnapshotForRefRefresh(() => snapshotRaw());

```

This establishes `snapshotRaw()` as the callback that `ensureRefMapForRef` invokes when regeneration is needed.

## Common Scenarios That Trigger Auto Re-snapshot

| Scenario | Why the map empties | Result |
|----------|---------------------|--------|
| **Page navigation** | New document, old refs invalid | Fresh snapshot before next `@ref` use |
| **Page reload** | Runtime context reset | Map cleared, auto-rebuild on demand |
| **Manual `refMap.clear()`** | Explicit cleanup | Next `@ref` operation triggers rebuild |
| **First operation in session** | Map never populated | Initial lazy initialization |

## Practical Usage Example

```typescript
// Example: Click using a snapshot reference
await page.click("@15");              // If map empty, auto-snapshots first

// Example: Explicit snapshot for fresh state
const snapshot = await page.snapshotRaw();  // Rebuilds ref-map immediately
console.log(snapshot.refs.length);          // Inspect captured elements

// Example: Helper implementation pattern
import { ensureRefMapForRef } from "./ref-state";
import { resolveElementCenter } from "./element-resolver";

export async function elementCenter(selectorOrRef: string) {
  await ensureRefMapForRef(selectorOrRef);  // Auto-resnapshot guard
  return resolveElementCenter(
    { sendRaw: cdp },
    undefined,
    browserRefMap,
    selectorOrRef,
  );
}

```

## Key Files in the Ref-Map Lifecycle

| File | Responsibility |
|------|---------------|
| [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | `RefMap` class definition and `parseRef` utility |
| [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) | Singleton `browserRefMap`, `ensureRefMapForRef`, callback registration |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | `browserSnapshotRefsToRefMap` — the rebuild implementation |
| [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) | `snapshotRaw`, `snapshot` helpers, one-time callback registration |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Public API (`click`, `type`, `elementCenter`) that depend on ref-map state |

## Summary

- **Every snapshot rebuilds the ref-map completely** via `browserSnapshotRefsToRefMap` clearing and repopulating entries from fresh browser data
- **Automatic re-snapshot triggers when** a helper receives a `@ref` string and `browserRefMap.map.size === 0`
- The `ensureRefMapForRef` guard in [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) implements lazy, race-condition-safe regeneration
- Registration happens once in [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts), binding `snapshotRaw()` as the refresh implementation

## Frequently Asked Questions

### What happens if I use a stale `@ref` after page navigation?

The ref-map empties on navigation (browser context reset). When you next use `@ref`, `ensureRefMapForRef` detects the empty map and triggers `snapshotRaw()` automatically, then resolves your reference against fresh data. No manual intervention required.

### Can I manually force a ref-map rebuild?

Yes—call `snapshotRaw()` directly. This invokes `browserSnapshotRefsToRefMap` with newly captured refs, immediately repopulating `browserRefMap`. Alternatively, `page.snapshot()` also refreshes the map while returning processed snapshot data.

### Why does the map clear rather than update incrementally?

Clear-and-rebuild guarantees **consistency**. DOM changes (reorders, removals, dynamic inserts) make incremental updates error-prone. A fresh array from the browser runtime ensures the map always matches the actual document state.

### Is there a risk of concurrent snapshot triggers?

No—`ensureRefMapForRef` uses an `ensuring` flag to prevent re-entrant execution. If multiple `@ref` operations race while the map is empty, only the first triggers `snapshotImpl()`; others await completion via the shared promise chain.