How the Ref-Map Gets Rebuilt on Snapshots to Keep @N References Valid in ego-lite
The ref-map is rebuilt after every snapshot by clearing the existing RefMap and repopulating it with fresh @N to backendNodeId mappings from the latest snapshot data, ensuring references always point to current DOM nodes.
In the citrolabs/ego-lite browser automation framework, @N references provide short-lived identifiers to DOM elements. When the page state changes, these references must remain accurate. The framework achieves this by automatically rebuilding the global ref-map each time a new snapshot is captured.
The Three-Step Ref-Map Rebuild Process
When ego-lite captures a page snapshot, it orchestrates a coordinated rebuild of the reference map through three distinct phases.
Step 1: Capture the Raw Snapshot
The process begins in src/driver/observe.ts, where the snapshotRaw() function invokes the underlying ego runtime via ego.snapshot. This returns a structured snapshot object containing a refs array. Each entry in this array maps a snapshot-generated reference number to its corresponding backendNodeId, along with metadata including role, name, and optional frameId.
Step 2: Register the Refresh Callback
Immediately after capturing the snapshot, observe.ts registers a callback using registerSnapshotForRefRefresh(() => snapshotRaw()). This callback is stored in src/state.ts and executes automatically after every successful snapshot operation.
Step 3: Clear and Repopulate the RefMap
The registered callback triggers refreshRefs(), implemented in src/ref-state.ts. This function performs the actual rebuild:
- It calls
browserRefMap.clear()to wipe the previous mappings (implemented insrc/ref-map.tsasRefMap.clear()) - It iterates over the
refsarray from the latest snapshot - For each reference, it invokes
browserRefMap.addWithFrame(refId, backendNodeId, role, name, nth, frameId)to store a fresh mapping from the textual@Nidentifier to the currentbackendNodeIdand frame context
Runtime Resolution and Automatic Recovery
When automation scripts invoke helpers like click("@12") or js("@5"), the system resolves these identifiers through RefMap.get(). If the map lacks the requested reference, src/element-resolver.ts automatically triggers a new snapshot. This ensures the ref-map is always current before resolving element queries.
// Taking a snapshot automatically rebuilds the ref-map
const snap = await page.snapshot(); // Contains refs like "@3", "@7"...
// Using a reference triggers lookup against the fresh map
await page.click("@3"); // Resolves via RefMap.get("@3")
Manual Ref-Map Refresh
While the system handles ref-map rebuilds automatically, you can force a manual refresh when necessary:
import { refreshRefs } from "ego-browser/src/ref-state.js";
// Forces a fresh snapshot and complete ref-map rebuild
await refreshRefs();
Summary
- Snapshot-driven rebuilds: Every call to
snapshotRaw()insrc/driver/observe.tstriggers a complete ref-map refresh through the registered callback mechanism - Complete replacement: The
RefMapis cleared entirely and rebuilt from therefsarray rather than incrementally updated - Backend node mapping: Each
@Nreference maps to abackendNodeIdstored with optional frame context viaaddWithFrame() - Automatic recovery:
src/element-resolver.tsdetects stale or missing references and initiates new snapshots automatically - Key files:
src/ref-map.tsdefines the map structure,src/ref-state.tsmanages the singleton instance, andsrc/driver/observe.tsorchestrates the snapshot cycle
Frequently Asked Questions
What triggers a ref-map rebuild in ego-lite?
A rebuild triggers automatically after every successful snapshot. The registerSnapshotForRefRefresh() callback in src/driver/observe.ts executes refreshRefs(), which clears and repopulates the map from the latest snapshot data.
Why does the ref-map get cleared instead of updated incrementally?
The browserRefMap.clear() approach ensures consistency. Since snapshots capture the complete DOM state, incremental updates risk leaving stale entries. A full rebuild guarantees that every @N reference points to a node that actually exists in the current snapshot.
How does ego-lite handle invalid or expired @N references?
When src/element-resolver.ts encounters a reference not present in the current map, it automatically triggers a fresh snapshot. This rebuilds the ref-map with current data, allowing the reference to resolve against the new DOM state.
Can I use @N references across page navigations?
No, @N references are short-lived identifiers tied to a specific snapshot. After navigation, you must capture a new snapshot to generate valid references for the new page state.
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 →