Skip to content

Commit 9c74255

Browse files
committed
chore: change puppeteer/devtools connection adapter
1 parent cc0a351 commit 9c74255

File tree

1 file changed

+84
-21
lines changed

1 file changed

+84
-21
lines changed

src/DevToolsConnectionAdapter.ts

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,101 @@
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';
1010

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

25-
override setOnMessage(onMessage: (arg0: object | string) => void): void {
26-
this.onMessage = onMessage;
63+
unobserve(observer: devtools.CDPConnectionObserver): void {
64+
this.#observers.delete(observer);
2765
}
2866

29-
override setOnDisconnect(onDisconnect: (arg0: string) => void): void {
30-
this.#onDisconnect = onDisconnect;
67+
#startForwardingCdpEvents(session: puppeteer.CDPSession): void {
68+
const handler = this.#handleEvent.bind(
69+
this,
70+
session.id(),
71+
) as puppeteer.Handler<unknown>;
72+
this.#sessionEventHandlers.set(session.id(), handler);
73+
session.on('*', handler);
3174
}
3275

33-
override sendRawMessage(message: string): void {
34-
this.#transport?.send(message);
76+
#stopForwardingCdpEvents(session: puppeteer.CDPSession): void {
77+
const handler = this.#sessionEventHandlers.get(session.id());
78+
if (handler) {
79+
session.off('*', handler);
80+
}
3581
}
3682

37-
override async disconnect(): Promise<void> {
38-
this.#transport?.close();
39-
this.#transport = null;
83+
// puppeteer types around '*' event listeners are broken
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85+
#handleEvent(
86+
sessionId: string,
87+
type: string | symbol | number,
88+
event: any,
89+
): void {
90+
if (
91+
typeof type === 'string' &&
92+
type !== 'sessionattached' &&
93+
type !== 'sessiondetached'
94+
) {
95+
this.#observers.forEach(observer =>
96+
observer.onEvent({
97+
method: type as devtools.Event,
98+
sessionId,
99+
params: event,
100+
}),
101+
);
102+
}
40103
}
41104
}

0 commit comments

Comments
 (0)