How Ego-Lite Implements the State Singleton Pattern in `src/state.ts`
Ego-Lite uses a plain JavaScript object exported as state from src/state.ts to implement a global singleton that holds all mutable runtime information, shared across modules through Node's module caching system.
The state singleton pattern in ego-lite provides a lightweight, testable way to manage browser session data and utility functions without class overhead or complex dependency injection. This pattern is central to how the ego-browser package coordinates navigation, waiting, and CDP (Chrome DevTools Protocol) communication.
What the State Singleton Pattern Looks Like
The implementation is deliberately minimal. Rather than a class with private constructors or static instance getters, ego-lite uses a single exported constant object:
// package/ego-browser/src/state.ts
export const state = {
sessionId: "",
sessionTargetId: "",
defaultTimeout: 10_000,
networkDomainEnabled: false,
// ... additional properties
now(): number { return Date.now(); },
sleep(ms: number): Promise<void> { /* ... */ },
writeFile(path: string, data: Buffer): Promise<void> { /* ... */ },
isOverridden(): boolean { /* ... */ }
};
Node's module system guarantees that every import { state } from "./state.js" receives the same object reference. This fulfills the singleton contract without ceremony.
Core Properties of the State Singleton
The state object holds mutable fields that evolve as the browser runtime executes:
| Property | Purpose | Typical Mutations |
|---|---|---|
sessionId |
Active CDP session identifier | Set after Target.attachToTarget in src/driver/nav.ts |
sessionTargetId |
Target ID for the current page | Updated on navigation events |
defaultTimeout |
Milliseconds for wait operations | Modified via setOverrides() in tests |
networkDomainEnabled |
Whether Network domain events are subscribed | Toggled during page load monitoring |
These properties are accessed directly by helpers throughout the codebase. For example, src/driver/waits.ts reads state.defaultTimeout to determine how long to poll for conditions.
Built-In Utility Methods
The singleton bundles commonly needed utilities:
state.now()— ReturnsDate.now(), enabling time-aware operations without global referencesstate.sleep(ms)— Returns aPromisethat resolves aftermsmilliseconds, used for backoff and pacingstate.writeFile(path, data)— File system helper for saving screenshots or downloads
These methods are attached to the state object itself, making them available wherever the singleton is imported without additional dependencies.
Test-Friendly Override System
The setOverrides() function enables safe mutation in unit tests:
// package/ego-browser/src/state.ts
export function setOverrides(overrides: Partial<typeof state>): () => void {
const original = { ...state };
Object.assign(state, overrides);
return () => {
Object.assign(state, original);
};
}
This shallow-copy approach:
- Captures current values before modification
- Applies the provided overrides
- Returns a cleanup function that restores original state
Example test usage:
import { state, setOverrides } from "../src/state.js";
test("waits respect custom timeout", async () => {
const restore = setOverrides({ defaultTimeout: 500 });
// Test behavior with 500ms timeout
await expect(waitForSelector("#missing")).rejects.toThrow();
restore(); // Original timeout restored
});
Detecting Transport Overrides
The state.isOverridden() method provides runtime introspection:
if (state.isOverridden()) {
// CDP transport replaced — likely running under test mocks
console.warn("Using mock CDP transport");
}
This check examines whether state.send (the CDP message dispatcher) or state.cdpOverride has been replaced, allowing conditional logic when the default transport is bypassed.
How the Singleton Is Used Across the Codebase
Navigation Module (src/driver/nav.ts)
The navigation layer updates session identifiers after attaching to new targets:
import { state } from "../state.js";
async function attachToTarget(targetId: string): Promise<void> {
const { sessionId } = await send("Target.attachToTarget", { targetId });
state.sessionId = sessionId;
state.sessionTargetId = targetId;
}
Waiting Module (src/driver/waits.ts)
Wait helpers consume the singleton for timing and pacing:
import { state } from "../state.js";
async function waitForCondition(
predicate: () => boolean,
timeoutMs = state.defaultTimeout
): Promise<void> {
const deadline = state.now() + timeoutMs;
while (!predicate()) {
if (state.now() > deadline) throw new TimeoutError();
await state.sleep(50);
}
}
Helpers Module (src/helpers.ts)
Higher-level conveniences expose state-derived values:
import { state } from "./state.js";
export function agentWorkspace(): string {
// Derives path from state configuration
return state.workspacePath || defaultWorkspace();
}
Why This Pattern Works for Ego-Lite
Several design decisions make this singleton appropriate:
- Module caching ensures single instance — No manual instance management required
- Plain object avoids class overhead — Direct property access, no getter indirection
- Explicit mutation sites — State changes are grep-able and reviewable
setOverrides()enables testing — No dependency injection boilerplate needed- TypeScript provides compile-time safety — The
typeof statetype travels with the export
The trade-off is global mutable state, which the codebase mitigates through:
- Clear ownership (only specific driver modules write to each field)
- Override cleanup requirements in tests
isOverridden()detection for unexpected modifications
Summary
- The state singleton in
src/state.tsis a plain exported object that Node's module system guarantees is shared across all importers - Mutable properties like
sessionId,defaultTimeout, andnetworkDomainEnabledtrack runtime browser state - Utility methods (
now(),sleep(),writeFile()) provide globally available helpers without extra imports setOverrides()enables safe test mutation with automatic cleanup of original valuesisOverridden()detects mocked environments where CDP transport has been replaced- Consumer modules in
src/driver/nav.ts,src/driver/waits.ts, andsrc/helpers.tsread and write the singleton to coordinate behavior
Frequently Asked Questions
What makes ego-lite's state singleton different from a class-based singleton?
Ego-lite skips the class entirely. Instead of StateManager.getInstance() or private constructors, the pattern relies on Node's module caching of export const state. Every import receives the same object reference. This eliminates boilerplate while achieving identical singleton semantics. The source code in package/ego-browser/src/state.ts implements this in roughly 50 lines versus a typical class singleton's ceremony.
How does setOverrides() prevent test pollution?
The function performs a shallow copy of current state before applying overrides, then returns a restoration closure. When called, this closure reassigns all properties to their original values. This ensures that even if a test throws or exits early, paired restore() calls guarantee clean state for subsequent tests. The implementation uses standard Object.assign() rather than structured cloning, which matches ego-lite's preference for simple, inspectable code.
Why attach utility methods like sleep() directly to the state object?
Co-location of shared dependencies reduces import complexity. Any module importing state gains immediate access to timing and I/O utilities without tracking separate utils or helpers imports. This proves especially useful in src/driver/waits.ts, where state.now() and state.sleep() appear together in polling loops. The pattern also simplifies mocking—replacing state.sleep with a synchronous stub affects all consumers uniformly.
Could this singleton pattern cause issues with parallel test execution?
Yes—the global mutable nature requires coordination. Ego-lite's test suite likely runs with setOverrides() or executes tests serially within a process. For true parallel execution, each worker would need isolated state, which would require architectural changes (e.g., passing context objects explicitly). The current design prioritizes simplicity for single-browser automation over concurrent safety.
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 →