diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-27 00:48:45 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | 9f2ef1cf87251ce12f6b6432c48c6e7d2017b6b5 (patch) | |
| tree | 2fad3353b016a9a35a049a7250b58ab2d165b50e /src | |
| parent | ae3a6dcdbe584a9131a514ea3d738abaec6460f3 (diff) | |
refactor(cdp): make session.call generic over the command table
call/callOnTarget/callBrowser now take a known method and return its
decoded result type. The session is where unknown ends; transport keeps
returning unknown. Removes the casts in session.ts and target-factory.ts,
including the session's own internal Target.*/Browser.* calls, which now
decode through the same path as the public call methods.
Also completes two params-schema gaps in commands.ts surfaced by this
migration: Target.createTarget was missing openerId and Runtime.evaluate
was missing userGesture, both real CDP fields src/cdp/ already sends.
tsc is red project-wide (6 errors, all in src/domains/) — the sanctioned
state between Tasks 4 and 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/cdp/commands.ts | 8 | ||||
| -rw-r--r-- | src/cdp/session.ts | 127 | ||||
| -rw-r--r-- | src/cdp/target-factory.ts | 17 |
3 files changed, 86 insertions, 66 deletions
diff --git a/src/cdp/commands.ts b/src/cdp/commands.ts index 123b9f6..d7faa34 100644 --- a/src/cdp/commands.ts +++ b/src/cdp/commands.ts @@ -57,6 +57,7 @@ export const COMMANDS = { newWindow: Type.Optional(Type.Boolean()), background: Type.Optional(Type.Boolean()), windowId: Type.Optional(Type.Number()), + openerId: Type.Optional(Type.String()), }), Type.Object({ targetId: Type.String() }), ), @@ -129,7 +130,12 @@ export const COMMANDS = { // --- Runtime --- "Runtime.evaluate": cmd( Type.Object( - { expression: Type.String(), returnByValue: Type.Optional(Type.Boolean()), awaitPromise: Type.Optional(Type.Boolean()) }, + { + expression: Type.String(), + returnByValue: Type.Optional(Type.Boolean()), + awaitPromise: Type.Optional(Type.Boolean()), + userGesture: Type.Optional(Type.Boolean()), + }, { additionalProperties: true }, ), Type.Object({ result: RemoteObject, exceptionDetails: Type.Optional(ExceptionDetails) }), diff --git a/src/cdp/session.ts b/src/cdp/session.ts index eeb3802..96b980f 100644 --- a/src/cdp/session.ts +++ b/src/cdp/session.ts @@ -3,6 +3,7 @@ import type { CdpError } from "./errors"; import type { DialogInfo } from "./types"; import type { OwnershipRegistry } from "./ownership"; 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"; @@ -36,9 +37,9 @@ export type CdpSession = { resolveRef(ref: string): number | undefined; /** The active tab's prior ref signatures — baseline for the post-mutation diff. */ refSignatures(): ReadonlyMap<string, string>; - call(method: string, params?: Record<string, unknown>, opts?: { timeoutMs?: number }): Promise<Result<unknown, CdpError>>; - callOnTarget(method: string, params: Record<string, unknown>, sessionId: string, opts?: { timeoutMs?: number }): Promise<Result<unknown, CdpError>>; - callBrowser(method: string, params?: Record<string, unknown>, opts?: { timeoutMs?: number }): Promise<Result<unknown, CdpError>>; + call<M extends CdpMethod>(method: M, params?: ParamsOf<M>, opts?: { timeoutMs?: number }): Promise<Result<ResultOf<M>, CdpError>>; + callOnTarget<M extends CdpMethod>(method: M, params: ParamsOf<M>, sessionId: string, opts?: { timeoutMs?: number }): Promise<Result<ResultOf<M>, CdpError>>; + callBrowser<M extends CdpMethod>(method: M, params?: ParamsOf<M>, opts?: { timeoutMs?: number }): Promise<Result<ResultOf<M>, CdpError>>; takeDialog(): DialogInfo | null; drainPageInfoInvalidations(): boolean; drainNetworkBuffer(filter: NetworkFilter): DrainResult; @@ -179,22 +180,29 @@ export const createCdpSession = ( } }; - // CDP response shapes are documented in chromedevtools.github.io but not - // available as TypeScript types. We cast `as` from `unknown` only in this - // file (the CDP boundary). Each cast is paired with the CDP method that - // produced the response. Adding runtime guards for every shape would be - // noise — Chrome's protocol is stable enough that a wrong cast surfaces - // as a clear Error in normal use. + // Internal helper: the session's own CDP calls (attach/switch bookkeeping) + // go through the same decode-on-call path as the public call/callOnTarget/ + // callBrowser — the session is where `unknown` ends, including for its own + // requests, not just the ones exposed to callers. + const req = async <M extends CdpMethod>( + method: M, + params: ParamsOf<M>, + sid: string | null, + ): Promise<Result<ResultOf<M>, CdpError>> => { + const raw = await transport.request(method, { ...params }, { sessionId: sid }); + if (!raw.success) return raw; + return decodeResult(method, raw.data); + }; + return { async attachFirstPage() { // Subscribe to Target.* events so we can react to targetDestroyed. // Best-effort: failing to enable discovery is not fatal for attach. - await transport.request("Target.setDiscoverTargets", { discover: true }, { sessionId: null }); + await req("Target.setDiscoverTargets", { discover: true }, null); - const targets = await transport.request("Target.getTargets", {}, { sessionId: null }); + const targets = await req("Target.getTargets", {}, null); if (!targets.success) return targets; - const data = targets.data as { targetInfos: ReadonlyArray<{ targetId: string; type: string; url: string }> }; - const allPages = data.targetInfos.filter((t) => t.type === "page"); + 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)); @@ -217,25 +225,23 @@ 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 transport.request("Browser.getWindowForTarget", { targetId: anchorId }, { sessionId: null }); + const anchorWin = await req("Browser.getWindowForTarget", { targetId: anchorId }, null); if (anchorWin.success) { - const anchorWindowId = (anchorWin.data as { windowId?: number }).windowId; - if (typeof anchorWindowId === "number") { - ownership.setHarnessWindowId(anchorWindowId); // refresh to the live id - const windows = await Promise.all( - allPages.map((p) => - p.targetId === anchorId - ? Promise.resolve(anchorWin) - : transport.request("Browser.getWindowForTarget", { targetId: p.targetId }, { sessionId: null }), - ), - ); - allPages.forEach((p, i) => { - const r = windows[i]; - if (r?.success && (r.data as { windowId?: number }).windowId === anchorWindowId) { - ownership.add(p.targetId); - } - }); - } + const anchorWindowId = anchorWin.data.windowId; + 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), + ), + ); + allPages.forEach((p, i) => { + const r = windows[i]; + if (r?.success && r.data.windowId === anchorWindowId) { + ownership.add(p.targetId); + } + }); } if (!ownership.harnessWindow()) ownership.setHarnessWindow(anchorId); } @@ -266,28 +272,26 @@ export const createCdpSession = ( pickTargetId = seeded.data; } if (!pickTargetId) { - const createParams: Record<string, unknown> = { url: "about:blank" }; - if (ownership) createParams["newWindow"] = true; - const created = await transport.request("Target.createTarget", createParams, { sessionId: null }); + const created = await req( + "Target.createTarget", + { url: "about:blank", ...(ownership ? { newWindow: true } : {}) }, + null, + ); if (!created.success) return created; - const c = created.data as { targetId: string }; - pickTargetId = c.targetId; + pickTargetId = created.data.targetId; if (ownership) { - ownership.setHarnessWindow(c.targetId); - ownership.add(c.targetId); + 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 transport.request("Browser.getWindowForTarget", { targetId: c.targetId }, { sessionId: null }); - if (win.success) { - const wid = (win.data as { windowId?: number }).windowId; - if (typeof wid === "number") ownership.setHarnessWindowId(wid); - } + const win = await req("Browser.getWindowForTarget", { targetId: created.data.targetId }, null); + if (win.success) ownership.setHarnessWindowId(win.data.windowId); } } - const attached = await transport.request("Target.attachToTarget", { targetId: pickTargetId, flatten: true }, { sessionId: null }); + const attached = await req("Target.attachToTarget", { targetId: pickTargetId, flatten: true }, null); if (!attached.success) return attached; - const a = attached.data as { sessionId: string }; + const a = attached.data; sessionId = a.sessionId; targetId = pickTargetId; await enableDomains(a.sessionId); @@ -305,11 +309,11 @@ export const createCdpSession = ( return ok({ targetId: pickTargetId, sessionId: a.sessionId }); }, async switchTo(tid) { - const activated = await transport.request("Target.activateTarget", { targetId: tid }, { sessionId: null }); + const activated = await req("Target.activateTarget", { targetId: tid }, null); if (!activated.success) return activated; - const attached = await transport.request("Target.attachToTarget", { targetId: tid, flatten: true }, { sessionId: null }); + const attached = await req("Target.attachToTarget", { targetId: tid, flatten: true }, null); if (!attached.success) return attached; - const a = attached.data as { sessionId: string }; + const a = attached.data; // Reuse existing TabSession or create one on first visit const existing = tabs.get(tid); const tab: TabSession = existing ?? { @@ -354,14 +358,29 @@ export const createCdpSession = ( const tab = targetId ? tabs.get(targetId) : undefined; return tab?.refSig ?? new Map(); }, - call(method, params = {}, opts = {}) { - return transport.request(method, params, { sessionId, ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) }); + async call(method, params, opts = {}) { + const raw = await transport.request(method, { ...(params ?? {}) }, { + sessionId, + ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}), + }); + if (!raw.success) return raw; + return decodeResult(method, raw.data); }, - callOnTarget(method, params, sid, opts = {}) { - return transport.request(method, params, { sessionId: sid, ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) }); + async callOnTarget(method, params, sid, opts = {}) { + const raw = await transport.request(method, { ...params }, { + sessionId: sid, + ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}), + }); + if (!raw.success) return raw; + return decodeResult(method, raw.data); }, - callBrowser(method, params = {}, opts = {}) { - return transport.request(method, params, { sessionId: null, ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}) }); + async callBrowser(method, params, opts = {}) { + const raw = await transport.request(method, { ...(params ?? {}) }, { + sessionId: null, + ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}), + }); + if (!raw.success) return raw; + return decodeResult(method, raw.data); }, takeDialog() { const tab = targetId ? tabs.get(targetId) : undefined; diff --git a/src/cdp/target-factory.ts b/src/cdp/target-factory.ts index 25c43a6..fb22339 100644 --- a/src/cdp/target-factory.ts +++ b/src/cdp/target-factory.ts @@ -56,8 +56,7 @@ type PageTarget = { const listPageTargets = async (client: BrowserClient): Promise<Result<ReadonlyArray<PageTarget>, CdpError>> => { const r = await client.session().callBrowser("Target.getTargets"); if (!r.success) return r; - const data = r.data as { targetInfos: ReadonlyArray<PageTarget> }; - return ok(data.targetInfos.filter((t) => t.type === "page")); + return ok(r.data.targetInfos.filter((t) => t.type === "page")); }; /** A CDP session id for driving `targetId`, reusing the active one when possible. */ @@ -66,7 +65,7 @@ const sessionForTarget = async (client: BrowserClient, targetId: string): Promis 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 as { sessionId: string }).sessionId); + return ok(attached.data.sessionId); }; /** @@ -92,8 +91,7 @@ const spawnTabViaOpener = async ( session.data, ); if (!opened.success) return opened; - const result = opened.data as { result?: { value?: unknown } }; - if (result.result?.value !== true) { + if (opened.data.result.value !== true) { return err(cdpError( "invalid_response", "the browser blocked window.open in the pinned profile's window — the page may have navigated away; retry, or run /browser-profile to re-seed", @@ -187,15 +185,12 @@ const ensureHarnessWindowUncoordinated = async ( const createDedicatedWindow = async (client: BrowserClient): Promise<Result<string, CdpError>> => { const created = await client.session().callBrowser("Target.createTarget", { url: "about:blank", newWindow: true }); if (!created.success) return created; - const { targetId } = created.data as { targetId: string }; + const { targetId } = created.data; const ownership = client.ownership(); ownership.setHarnessWindow(targetId); ownership.add(targetId); const win = await client.session().callBrowser("Browser.getWindowForTarget", { targetId }); - if (win.success) { - const windowId = (win.data as { windowId?: number }).windowId; - if (typeof windowId === "number") ownership.setHarnessWindowId(windowId); - } + if (win.success) ownership.setHarnessWindowId(win.data.windowId); return ok(targetId); }; @@ -215,7 +210,7 @@ export const openHarnessTab = async ( openerId: openerTargetId, }); if (!created.success) return created; - return ok((created.data as { targetId: string }).targetId); + return ok(created.data.targetId); })(); if (!spawned.success) return spawned; client.ownership().add(spawned.data); |
