aboutsummaryrefslogtreecommitdiff
path: root/src/cdp/session.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 20:25:38 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit330767d862458386e804bde240a1f0d7835b9b8e (patch)
tree13a5f2086d265959c1bfa10bb88641a6804fd1f5 /src/cdp/session.ts
parent12d7a38ba0f1b1e4e301fce81e22f4b93036d211 (diff)
refactor(cdp): strip comment noise
Removes module banners, section dividers, and comments restating the code. Keeps one-line notes for the window.open/browserContextId constraint, the seed-tab claim, the unverifiable-pin reuse rule, the SeedTargetProvider rationale, the deliberate dialog retention, the discovery-time browser quirks, and the guarded CDP decoder assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/cdp/session.ts')
-rw-r--r--src/cdp/session.ts81
1 files changed, 4 insertions, 77 deletions
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index 80fec16..de3fb28 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -10,10 +10,6 @@ import { createConsoleBuffer, type ConsoleDrainResult, type ConsoleFilter } from
import { attachTo } from "./attach";
import { getWindowId } from "./window";
-// Per-tab state. One TabSession exists per known targetId.
-// Dialog, page-info dirty flag, and CDP buffers are per-tab — switching
-// tabs preserves the previous tab's data in its TabSession instead of
-// clearing it.
type TabSession = {
sessionId: string;
targetId: string;
@@ -21,12 +17,7 @@ type TabSession = {
pageInfoDirty: boolean;
networkBuffer: ReturnType<typeof createNetworkBuffer>;
consoleBuffer: ReturnType<typeof createConsoleBuffer>;
- // Stable element refs (e1, e2, …) → CDP backendNodeId, from the latest
- // snapshot of this tab. Replaced wholesale on every snapshot — old refs are
- // stale anyway, so this stays bounded (~50–60 KB worst case).
refMap: Map<string, number>;
- // Per-ref signature ("role|name|value") from the latest snapshot, used as the
- // baseline for the post-mutation auto-diff. Replaced alongside refMap.
refSig: Map<string, string>;
};
@@ -37,11 +28,8 @@ export type CdpSession = {
attachFirstPage(): Promise<Result<{ readonly targetId: string; readonly sessionId: string }, CdpError>>;
switchTo(targetId: string): Promise<Result<void, CdpError>>;
current(): { readonly sessionId: string; readonly targetId: string } | null;
- /** Replace the active tab's ref map and ref signatures (from a fresh snapshot). */
setRefMap(refMap: Map<string, number>, refSig: Map<string, string>): void;
- /** Resolve a ref (e.g. "e12") to its backendNodeId on the active tab, or undefined if unknown/stale. */
resolveRef(ref: string): number | undefined;
- /** The active tab's prior ref signatures — baseline for the post-mutation diff. */
refSignatures(): ReadonlyMap<string, string>;
refMappings(): ReadonlyMap<string, number>;
call<M extends CdpMethod>(method: M, params?: ParamsOf<M>, opts?: { timeoutMs?: number }): Promise<Result<ResultOf<M>, CdpError>>;
@@ -53,15 +41,7 @@ export type CdpSession = {
drainConsoleBuffer(filter: ConsoleFilter): ConsoleDrainResult;
};
-/**
- * Supplies the seed tab when attachFirstPage finds no owned tab to attach to.
- * Injected by the client so that seeding goes through the target factory —
- * which, with a profile pinned, opens the window inside that profile. Without
- * it, attachFirstPage's own `Target.createTarget` would land in whichever
- * profile currently has focus.
- *
- * The provider is responsible for its own ownership bookkeeping.
- */
+// Injected so seeding goes through the target factory; attachFirstPage's own `Target.createTarget` would land in whichever profile has focus.
export type SeedTargetProvider = () => Promise<Result<string, CdpError>>;
export const createCdpSession = (
@@ -72,15 +52,11 @@ export const createCdpSession = (
let sessionId: string | null = null;
let targetId: string | null = null;
- // Per-tab tracking: maps targetId → TabSession and sessionId → targetId for
- // event routing. Lazy: a TabSession is created on the first visit to a tab.
const tabs = new Map<string, TabSession>();
const sessionIdToTargetId = new Map<string, string>();
let activeConsumer: Promise<void> = Promise.resolve();
- // Event → TabSession resolver. Uses sessionId from CDP events to find the
- // correct TabSession, falling back to the current targetId when no sessionId.
const resolveTab = (evSessionId?: string): TabSession | undefined => {
const tid = evSessionId ? sessionIdToTargetId.get(evSessionId) : targetId;
return tid ? tabs.get(tid) : undefined;
@@ -88,8 +64,6 @@ export const createCdpSession = (
const consumeEvents = async (): Promise<void> => {
for await (const ev of transport.events()) {
- // Filter: skip events from sessions we're not currently tracking.
- // Target.targetDestroyed is browser-level (no sessionId) — always process.
if (ev.method !== "Target.targetDestroyed") {
if (ev.sessionId && !sessionIdToTargetId.has(ev.sessionId)) continue;
}
@@ -109,10 +83,7 @@ export const createCdpSession = (
: { type: "alert", message: "" };
break;
}
- // Page.javascriptDialogClosed is intentionally NOT cleared here —
- // the dialog stays in the buffer until takeDialog() is called.
- // This prevents fast dismiss flows from dropping a dialog the agent
- // was about to read. (Fix for spec §7 predictability bug #2.)
+ // Intentionally NOT cleared on javascriptDialogClosed: a fast dismiss would otherwise drop a dialog the agent was about to read.
case "Page.frameNavigated":
case "Page.loadEventFired": {
const tab = resolveTab(ev.sessionId);
@@ -120,11 +91,6 @@ export const createCdpSession = (
break;
}
case "Target.targetCreated": {
- // Adopt tabs opened BY a tab we already own (window.open, target=_blank,
- // popups). Chrome sets openerId to the opener target; if that opener is
- // owned, the child belongs to this session's window and must be
- // controllable + cleaned up. Targets the user opens themselves have no
- // owned opener, so they are never adopted.
if (!ownership) break;
const decoded = decodeEvent(ev.method, ev.params);
if (!decoded.success) break;
@@ -140,7 +106,6 @@ export const createCdpSession = (
if (!decoded.success || !decoded.data.targetId) break;
const destroyed = decoded.data.targetId;
ownership.remove(destroyed);
- // Prune per-tab state for the destroyed target
const tab = tabs.get(destroyed);
if (tab) {
sessionIdToTargetId.delete(tab.sessionId);
@@ -186,10 +151,6 @@ export const createCdpSession = (
const restartConsumer = (): void => {
activeConsumer = activeConsumer.then(() => consumeEvents()).catch((e: unknown) => {
- // The .then() chain calls consumeEvents() which iterates the transport's
- // events() AsyncIterable. The iterable resolves cleanly on close (returns
- // {done:true}); any rejection here is an unexpected bug in the event
- // handler, not a normal termination. Surface it on stderr so it's not lost.
console.warn("[pi-browser-harness] CDP event consumer crashed:", e);
});
};
@@ -200,26 +161,16 @@ export const createCdpSession = (
targetId = null;
tabs.clear();
sessionIdToTargetId.clear();
- // Do NOT clear `dialog` here — same rationale as inside consumeEvents:
- // the agent may have a pending takeDialog() call that should still see it.
+ // Do NOT clear `dialog` here: a pending takeDialog() must still see it.
restartConsumer();
});
- // TODO(perf): the four enable calls are sequential here for predictability.
- // Switching to Promise.all over a single WS pipelines the round-trips and
- // saves ~3× on tab-switch latency. Defer until session.ts has tests.
const enableDomains = async (sid: string): Promise<void> => {
for (const d of ["Page", "DOM", "Runtime", "Network", "Accessibility", "Log"]) {
await transport.request(`${d}.enable`, {}, { sessionId: sid });
}
};
- // The one place `unknown` ends for every CDP call this session makes — its
- // own internal bookkeeping calls (attach/switch) and the public
- // call/callOnTarget/callBrowser all funnel through here. A single
- // transport-then-decode implementation means a future change (tracing,
- // error wrapping, the short-circuit on failure) lands in one place, and a
- // new internal call site can't forget to decode.
const req = async <M extends CdpMethod>(
method: M,
params: ParamsOf<M> | undefined,
@@ -236,14 +187,11 @@ export const createCdpSession = (
const session: CdpSession = {
async attachFirstPage() {
- // Subscribe to Target.* events so we can react to targetDestroyed.
- // Best-effort: failing to enable discovery is not fatal for attach.
await req("Target.setDiscoverTargets", { discover: true }, null);
const targets = await req("Target.getTargets", {}, null);
if (!targets.success) return targets;
const allPages = targets.data.targetInfos.filter((t) => t.type === "page");
- // Reconcile the persisted ownership set against live targets — drop dead IDs.
if (ownership) {
const live = new Set(allPages.map((p) => p.targetId));
const survivors = ownership.list().filter((id) => live.has(id));
@@ -253,22 +201,12 @@ export const createCdpSession = (
const anchorId = survivors[0];
if (anchorId === undefined) {
- // No owned tab survived — the harness window is gone (user closed it,
- // or Chrome restarted and reassigned all ids). The persisted windowId
- // is now meaningless and could even collide with one of the user's
- // windows, so drop it. attachFirstPage will mint a fresh window below.
ownership.setHarnessWindowId(undefined);
} else {
- // Self-heal ownership from the window itself. We anchor on a tab we
- // KNOW we own (a survivor) and re-adopt only its live window-mates —
- // popups/children from last session, tabs reopened in our window.
- // Ownership is thus DERIVED from the real window, never from the bare
- // persisted integer (which Chrome may reassign to a user window on
- // restart — anchoring on a survivor makes that misfire impossible).
const anchorWin = await getWindowId(session, anchorId);
if (anchorWin.success) {
const anchorWindowId = anchorWin.data;
- ownership.setHarnessWindowId(anchorWindowId); // refresh to the live id
+ ownership.setHarnessWindowId(anchorWindowId);
const windows = await Promise.all(
allPages.map((p) =>
p.targetId === anchorId
@@ -286,7 +224,6 @@ export const createCdpSession = (
if (!ownership.harnessWindow()) ownership.setHarnessWindow(anchorId);
}
}
- // Prune per-tab state for pages that no longer exist
const liveTargetIds = new Set(allPages.map((p) => p.targetId));
for (const tid of tabs.keys()) {
if (!liveTargetIds.has(tid)) {
@@ -296,17 +233,12 @@ export const createCdpSession = (
}
}
- // Prefer attaching to a tab this session already owns. Falls back to
- // creating a fresh harness-owned tab in a dedicated window — never
- // grabs the user's foreground tab.
let pickTargetId: string | undefined;
if (ownership) {
const ownedLive = ownership.list().filter((id) => allPages.some((p) => p.targetId === id));
pickTargetId = ownedLive[0];
}
if (!pickTargetId && createSeedTarget) {
- // Delegated seeding (see SeedTargetProvider): the provider creates the
- // window in the right profile and records ownership itself.
const seeded = await createSeedTarget();
if (!seeded.success) return seeded;
pickTargetId = seeded.data;
@@ -322,8 +254,6 @@ export const createCdpSession = (
if (ownership) {
ownership.setHarnessWindow(created.data.targetId);
ownership.add(created.data.targetId);
- // Capture the real Chrome windowId — the durable identity of the
- // window this session initialized (survives seed-tab closure).
const win = await getWindowId(session, created.data.targetId);
if (win.success) ownership.setHarnessWindowId(win.data);
}
@@ -354,7 +284,6 @@ export const createCdpSession = (
const attached = await attachTo(session, tid);
if (!attached.success) return attached;
const attachedSessionId = attached.data;
- // Reuse existing TabSession or create one on first visit
const existing = tabs.get(tid);
const tab: TabSession = existing ?? {
sessionId: attachedSessionId,
@@ -367,7 +296,6 @@ export const createCdpSession = (
refSig: new Map(),
};
if (existing) {
- // Each Target.attachToTarget produces a new sessionId — update it.
sessionIdToTargetId.delete(existing.sessionId);
existing.sessionId = attachedSessionId;
} else {
@@ -375,7 +303,6 @@ export const createCdpSession = (
}
sessionIdToTargetId.set(attachedSessionId, tid);
await enableDomains(attachedSessionId);
- // Update global pointers to point at the new active tab
sessionId = tab.sessionId;
targetId = tid;
return ok(undefined);