aboutsummaryrefslogtreecommitdiff
path: root/src/cdp
diff options
context:
space:
mode:
Diffstat (limited to 'src/cdp')
-rw-r--r--src/cdp/attach.ts13
-rw-r--r--src/cdp/evaluate.ts31
-rw-r--r--src/cdp/session.ts45
-rw-r--r--src/cdp/target-factory.ts10
-rw-r--r--src/cdp/window-binding.ts15
-rw-r--r--src/cdp/window.ts10
6 files changed, 83 insertions, 41 deletions
diff --git a/src/cdp/attach.ts b/src/cdp/attach.ts
new file mode 100644
index 0000000..9be539d
--- /dev/null
+++ b/src/cdp/attach.ts
@@ -0,0 +1,13 @@
+import { map, type Result } from "../util/result";
+import type { CdpError } from "./errors";
+import type { CdpSession } from "./session";
+
+/** Target.attachToTarget, unwrapped to the new session id. */
+export const attachTo = async (
+ session: CdpSession,
+ targetId: string,
+): Promise<Result<string, CdpError>> =>
+ map(
+ await session.callBrowser("Target.attachToTarget", { targetId, flatten: true }),
+ (d) => d.sessionId,
+ );
diff --git a/src/cdp/evaluate.ts b/src/cdp/evaluate.ts
new file mode 100644
index 0000000..cd68d61
--- /dev/null
+++ b/src/cdp/evaluate.ts
@@ -0,0 +1,31 @@
+import { type Result, err, ok } from "../util/result";
+import { type CdpError, cdpError } from "./errors";
+import type { CdpSession } from "./session";
+
+/**
+ * Runtime.evaluate, unwrapped and narrowed. Runs `expression`, surfaces a page
+ * exception as an error, then applies `check` to the returned value — the
+ * sanctioned way to turn `unknown` evaluate output into a typed `T` without a
+ * cast.
+ */
+export const evaluateJson = async <T>(
+ session: CdpSession,
+ expression: string,
+ check: (v: unknown) => v is T,
+): Promise<Result<T, CdpError>> => {
+ const r = await session.call("Runtime.evaluate", {
+ expression,
+ returnByValue: true,
+ awaitPromise: true,
+ });
+ if (!r.success) return r;
+ const { result, exceptionDetails } = r.data;
+ if (exceptionDetails !== undefined) {
+ return err(cdpError("remote_error", exceptionDetails.text, "Runtime.evaluate"));
+ }
+ const value = result.value;
+ if (!check(value)) {
+ return err(cdpError("invalid_response", `unexpected evaluate shape for: ${expression.slice(0, 60)}`, "Runtime.evaluate"));
+ }
+ return ok(value);
+};
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index dd5a9b3..99e68b4 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -6,6 +6,8 @@ import type { CdpTransport } from "./transport";
import { type CdpMethod, type ParamsOf, type ResultOf, decodeResult } from "./commands";
import { createNetworkBuffer, type DrainResult, type NetworkFilter } from "./network-buffer";
import { createConsoleBuffer, type ConsoleDrainResult, type ConsoleFilter } from "./console-buffer";
+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
@@ -200,7 +202,7 @@ export const createCdpSession = (
return decodeResult(method, raw.data);
};
- return {
+ 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.
@@ -231,20 +233,20 @@ export const createCdpSession = (
// persisted integer (which Chrome may reassign to a user window on
// restart — anchoring on a survivor makes that misfire impossible).
const anchorId = survivors[0]!;
- const anchorWin = await req("Browser.getWindowForTarget", { targetId: anchorId }, null);
+ const anchorWin = await getWindowId(session, anchorId);
if (anchorWin.success) {
- const anchorWindowId = anchorWin.data.windowId;
+ const anchorWindowId = anchorWin.data;
ownership.setHarnessWindowId(anchorWindowId); // refresh to the live id
const windows = await Promise.all(
allPages.map((p) =>
p.targetId === anchorId
? Promise.resolve(anchorWin)
- : req("Browser.getWindowForTarget", { targetId: p.targetId }, null),
+ : getWindowId(session, p.targetId),
),
);
allPages.forEach((p, i) => {
const r = windows[i];
- if (r?.success && r.data.windowId === anchorWindowId) {
+ if (r?.success && r.data === anchorWindowId) {
ownership.add(p.targetId);
}
});
@@ -290,19 +292,19 @@ export const createCdpSession = (
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 req("Browser.getWindowForTarget", { targetId: created.data.targetId }, null);
- if (win.success) ownership.setHarnessWindowId(win.data.windowId);
+ const win = await getWindowId(session, created.data.targetId);
+ if (win.success) ownership.setHarnessWindowId(win.data);
}
}
- const attached = await req("Target.attachToTarget", { targetId: pickTargetId, flatten: true }, null);
+ const attached = await attachTo(session, pickTargetId);
if (!attached.success) return attached;
- const a = attached.data;
- sessionId = a.sessionId;
+ const attachedSessionId = attached.data;
+ sessionId = attachedSessionId;
targetId = pickTargetId;
- await enableDomains(a.sessionId);
+ await enableDomains(attachedSessionId);
tabs.set(pickTargetId, {
- sessionId: a.sessionId,
+ sessionId: attachedSessionId,
targetId: pickTargetId,
dialog: null,
pageInfoDirty: false,
@@ -311,19 +313,19 @@ export const createCdpSession = (
refMap: new Map(),
refSig: new Map(),
});
- sessionIdToTargetId.set(a.sessionId, pickTargetId);
- return ok({ targetId: pickTargetId, sessionId: a.sessionId });
+ sessionIdToTargetId.set(attachedSessionId, pickTargetId);
+ return ok({ targetId: pickTargetId, sessionId: attachedSessionId });
},
async switchTo(tid) {
const activated = await req("Target.activateTarget", { targetId: tid }, null);
if (!activated.success) return activated;
- const attached = await req("Target.attachToTarget", { targetId: tid, flatten: true }, null);
+ const attached = await attachTo(session, tid);
if (!attached.success) return attached;
- const a = attached.data;
+ const attachedSessionId = attached.data;
// Reuse existing TabSession or create one on first visit
const existing = tabs.get(tid);
const tab: TabSession = existing ?? {
- sessionId: a.sessionId,
+ sessionId: attachedSessionId,
targetId: tid,
dialog: null,
pageInfoDirty: true,
@@ -335,12 +337,12 @@ export const createCdpSession = (
if (existing) {
// Each Target.attachToTarget produces a new sessionId — update it.
sessionIdToTargetId.delete(existing.sessionId);
- existing.sessionId = a.sessionId;
- sessionIdToTargetId.set(a.sessionId, tid);
+ existing.sessionId = attachedSessionId;
+ sessionIdToTargetId.set(attachedSessionId, tid);
} else {
tabs.set(tid, tab);
- await enableDomains(a.sessionId);
- sessionIdToTargetId.set(a.sessionId, tid);
+ await enableDomains(attachedSessionId);
+ sessionIdToTargetId.set(attachedSessionId, tid);
}
// Update global pointers to point at the new active tab
sessionId = tab.sessionId;
@@ -398,4 +400,5 @@ export const createCdpSession = (
return tab.consoleBuffer.drain(filter);
},
};
+ return session;
};
diff --git a/src/cdp/target-factory.ts b/src/cdp/target-factory.ts
index fb22339..718a8d0 100644
--- a/src/cdp/target-factory.ts
+++ b/src/cdp/target-factory.ts
@@ -30,7 +30,9 @@
import { randomUUID } from "node:crypto";
import type { BrowserClient } from "../client";
import { type Result, err, ok } from "../util/result";
+import { attachTo } from "./attach";
import { type CdpError, cdpError } from "./errors";
+import { getWindowId } from "./window";
/** How long to wait for a window.open-spawned target to appear. */
const SPAWN_DEADLINE_MS = 8_000;
@@ -63,9 +65,7 @@ const listPageTargets = async (client: BrowserClient): Promise<Result<ReadonlyAr
const sessionForTarget = async (client: BrowserClient, targetId: string): Promise<Result<string, CdpError>> => {
const current = client.current();
if (current?.targetId === targetId) return ok(current.sessionId);
- const attached = await client.session().callBrowser("Target.attachToTarget", { targetId, flatten: true });
- if (!attached.success) return attached;
- return ok(attached.data.sessionId);
+ return attachTo(client.session(), targetId);
};
/**
@@ -189,8 +189,8 @@ const createDedicatedWindow = async (client: BrowserClient): Promise<Result<stri
const ownership = client.ownership();
ownership.setHarnessWindow(targetId);
ownership.add(targetId);
- const win = await client.session().callBrowser("Browser.getWindowForTarget", { targetId });
- if (win.success) ownership.setHarnessWindowId(win.data.windowId);
+ const win = await getWindowId(client.session(), targetId);
+ if (win.success) ownership.setHarnessWindowId(win.data);
return ok(targetId);
};
diff --git a/src/cdp/window-binding.ts b/src/cdp/window-binding.ts
deleted file mode 100644
index 4ae1ac3..0000000
--- a/src/cdp/window-binding.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { BrowserClient } from "../client";
-
-/**
- * Capture the real Chrome windowId of a freshly-created harness window and bind
- * it as the session's durable window identity. Best-effort: a failed CDP call
- * leaves the previous binding untouched. Call this on every code path that
- * spawns a NEW dedicated window (newWindow:true), so the windowId never points
- * at a window that has since been replaced.
- */
-export const bindHarnessWindowId = async (client: BrowserClient, targetId: string): Promise<void> => {
- const r = await client.session().callBrowser("Browser.getWindowForTarget", { targetId });
- if (!r.success) return;
- const wid = (r.data as { windowId?: number }).windowId;
- if (typeof wid === "number") client.ownership().setHarnessWindowId(wid);
-};
diff --git a/src/cdp/window.ts b/src/cdp/window.ts
new file mode 100644
index 0000000..f16838c
--- /dev/null
+++ b/src/cdp/window.ts
@@ -0,0 +1,10 @@
+import { map, type Result } from "../util/result";
+import type { CdpError } from "./errors";
+import type { CdpSession } from "./session";
+
+/** Browser.getWindowForTarget, unwrapped to the windowId. */
+export const getWindowId = async (
+ session: CdpSession,
+ targetId: string,
+): Promise<Result<number, CdpError>> =>
+ map(await session.callBrowser("Browser.getWindowForTarget", { targetId }), (d) => d.windowId);