How Ref-Map Rebuilds on Every Snapshot and What Triggers Auto Re-snapshot in ego-lite
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, this function implements a clear-and-repopulate strategy:
// 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:
- No stale references — previous snapshot data is discarded immediately
- Consistent state — the map always reflects the current DOM
- Validation — entries without
backendNodeIdare filtered out
This function is invoked internally by snapshotRaw (in 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:
// 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 byparseRef) - 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:
// 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
// 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 |
RefMap class definition and parseRef utility |
src/ref-state.ts |
Singleton browserRefMap, ensureRefMapForRef, callback registration |
src/browser-runtime.ts |
browserSnapshotRefsToRefMap — the rebuild implementation |
src/driver/observe.ts |
snapshotRaw, snapshot helpers, one-time callback registration |
src/helpers.ts |
Public API (click, type, elementCenter) that depend on ref-map state |
Summary
- Every snapshot rebuilds the ref-map completely via
browserSnapshotRefsToRefMapclearing and repopulating entries from fresh browser data - Automatic re-snapshot triggers when a helper receives a
@refstring andbrowserRefMap.map.size === 0 - The
ensureRefMapForRefguard insrc/ref-state.tsimplements lazy, race-condition-safe regeneration - Registration happens once in
src/driver/observe.ts, bindingsnapshotRaw()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.
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 →