Skip to content

Commit eea5b80

Browse files
authored
chore: change puppeteer/devtools connection adapter (#602)
Rather then adapting on the transport layer, we adapt the puppeteer `CDPSession`/`Connection` and make them look like a `CDPConnection`. The class assumes that callers create a dedicated `CDPSession` to be used. The `PuppeteerDevToolsConnection` installs a generic `'*'` event listener on all child sessions to funnel all CDP events into DevTools via `CDPConnectionObservers`. As pointed out in the code comment, we don't need to recursively listen to `'sessionattached'` events on child sessions: Nested `sessionattached` events are reported on all parent sessions. While not strictly necessary, `PuppeteerDevToolsConnection` also uninstalls the CDP event listener when a session gets detached (modulo the root puppeteer `CDPSession`).
1 parent 85ba2dd commit eea5b80

File tree

2 files changed

+94
-22
lines changed

2 files changed

+94
-22
lines changed

src/DevToolsConnectionAdapter.ts

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,106 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {ConnectionTransport as DevToolsConnectionTransport} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
7+
import type {CDPConnection as devtools} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
88

9-
import {type ConnectionTransport} from './third_party/index.js';
9+
import type * as puppeteer from './third_party/index.js';
10+
import {CDPSessionEvent} from './third_party/index.js';
1011

1112
/**
12-
* Allows a puppeteer {@link ConnectionTransport} to act like a DevTools {@link Connection}.
13+
* This class makes a puppeteer connection look like DevTools CDPConnection.
14+
*
15+
* Since we connect "root" DevTools targets to specific pages, we scope everything to a puppeteer CDP session.
16+
*
17+
* We don't have to recursively listen for 'sessionattached' as the "root" CDP session sees all child session attached
18+
* events, regardless how deeply nested they are.
1319
*/
14-
export class DevToolsConnectionAdapter extends DevToolsConnectionTransport {
15-
#transport: ConnectionTransport | null;
16-
#onDisconnect: ((arg0: string) => void) | null = null;
17-
18-
constructor(transport: ConnectionTransport) {
19-
super();
20-
this.#transport = transport;
21-
this.#transport.onclose = () => this.#onDisconnect?.('');
22-
this.#transport.onmessage = msg => this.onMessage?.(msg);
20+
export class PuppeteerDevToolsConnection implements devtools.CDPConnection {
21+
readonly #connection: puppeteer.Connection;
22+
readonly #observers = new Set<devtools.CDPConnectionObserver>();
23+
readonly #sessionEventHandlers = new Map<
24+
string,
25+
puppeteer.Handler<unknown>
26+
>();
27+
28+
constructor(session: puppeteer.CDPSession) {
29+
this.#connection = session.connection()!;
30+
31+
session.on(
32+
CDPSessionEvent.SessionAttached,
33+
this.#startForwardingCdpEvents.bind(this),
34+
);
35+
session.on(
36+
CDPSessionEvent.SessionDetached,
37+
this.#stopForwardingCdpEvents.bind(this),
38+
);
39+
40+
this.#startForwardingCdpEvents(session);
41+
}
42+
43+
send<T extends devtools.Command>(
44+
method: T,
45+
params: devtools.CommandParams<T>,
46+
sessionId: string | undefined,
47+
): Promise<{result: devtools.CommandResult<T>} | {error: devtools.CDPError}> {
48+
if (sessionId === undefined) {
49+
throw new Error(
50+
'Attempting to send on the root session. This must not happen',
51+
);
52+
}
53+
const session = this.#connection.session(sessionId);
54+
if (!session) {
55+
throw new Error('Unknown session ' + sessionId);
56+
}
57+
// Rolled protocol version between puppeteer and DevTools doesn't necessarily match
58+
/* eslint-disable @typescript-eslint/no-explicit-any */
59+
return session
60+
.send(method as any, params)
61+
.then(result => ({result}))
62+
.catch(error => ({error})) as any;
63+
/* eslint-enable @typescript-eslint/no-explicit-any */
64+
}
65+
66+
observe(observer: devtools.CDPConnectionObserver): void {
67+
this.#observers.add(observer);
2368
}
2469

25-
override setOnMessage(onMessage: (arg0: object | string) => void): void {
26-
this.onMessage = onMessage;
70+
unobserve(observer: devtools.CDPConnectionObserver): void {
71+
this.#observers.delete(observer);
2772
}
2873

29-
override setOnDisconnect(onDisconnect: (arg0: string) => void): void {
30-
this.#onDisconnect = onDisconnect;
74+
#startForwardingCdpEvents(session: puppeteer.CDPSession): void {
75+
const handler = this.#handleEvent.bind(
76+
this,
77+
session.id(),
78+
) as puppeteer.Handler<unknown>;
79+
this.#sessionEventHandlers.set(session.id(), handler);
80+
session.on('*', handler);
3181
}
3282

33-
override sendRawMessage(message: string): void {
34-
this.#transport?.send(message);
83+
#stopForwardingCdpEvents(session: puppeteer.CDPSession): void {
84+
const handler = this.#sessionEventHandlers.get(session.id());
85+
if (handler) {
86+
session.off('*', handler);
87+
}
3588
}
3689

37-
override async disconnect(): Promise<void> {
38-
this.#transport?.close();
39-
this.#transport = null;
90+
#handleEvent(
91+
sessionId: string,
92+
type: string | symbol | number,
93+
event: any, // eslint-disable-line @typescript-eslint/no-explicit-any
94+
): void {
95+
if (
96+
typeof type === 'string' &&
97+
type !== CDPSessionEvent.SessionAttached &&
98+
type !== CDPSessionEvent.SessionDetached
99+
) {
100+
this.#observers.forEach(observer =>
101+
observer.onEvent({
102+
method: type as devtools.Event,
103+
sessionId,
104+
params: event,
105+
}),
106+
);
107+
}
40108
}
41109
}

src/third_party/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ export {
2020
type TextContent,
2121
} from '@modelcontextprotocol/sdk/types.js';
2222
export {z as zod} from 'zod';
23-
export {Locator, PredefinedNetworkConditions} from 'puppeteer-core';
23+
export {
24+
Locator,
25+
PredefinedNetworkConditions,
26+
CDPSessionEvent,
27+
} from 'puppeteer-core';
2428
export {default as puppeteer} from 'puppeteer-core';
2529
export type * from 'puppeteer-core';
2630
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';

0 commit comments

Comments
 (0)