/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type * as puppeteer from './third_party/index.js';
import type {DevTools} from './third_party/index.js';
import {CDPSessionEvent} from './third_party/index.js';
/**
* This class makes a puppeteer connection look like DevTools CDPConnection.
*
* Since we connect "root" DevTools targets to specific pages, we scope everything to a puppeteer CDP session.
*
* We don't have to recursively listen for 'sessionattached' as the "root" CDP session sees all child session attached
* events, regardless how deeply nested they are.
*/
export class PuppeteerDevToolsConnection
implements DevTools.CDPConnection.CDPConnection
{
readonly #connection: puppeteer.Connection;
readonly #observers = new Set<DevTools.CDPConnection.CDPConnectionObserver>();
readonly #sessionEventHandlers = new Map<
string,
puppeteer.Handler<unknown>
>();
constructor(session: puppeteer.CDPSession) {
this.#connection = session.connection()!;
session.on(
CDPSessionEvent.SessionAttached,
this.#startForwardingCdpEvents.bind(this),
);
session.on(
CDPSessionEvent.SessionDetached,
this.#stopForwardingCdpEvents.bind(this),
);
this.#startForwardingCdpEvents(session);
}
send<T extends DevTools.CDPConnection.Command>(
method: T,
params: DevTools.CDPConnection.CommandParams<T>,
sessionId: string | undefined,
): Promise<
| {result: DevTools.CDPConnection.CommandResult<T>}
| {error: DevTools.CDPConnection.CDPError}
> {
if (sessionId === undefined) {
throw new Error(
'Attempting to send on the root session. This must not happen',
);
}
const session = this.#connection.session(sessionId);
if (!session) {
throw new Error('Unknown session ' + sessionId);
}
// Rolled protocol version between puppeteer and DevTools doesn't necessarily match
/* eslint-disable @typescript-eslint/no-explicit-any */
return session
.send(method as any, params)
.then(result => ({result}))
.catch(error => ({error})) as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
}
observe(observer: DevTools.CDPConnection.CDPConnectionObserver): void {
this.#observers.add(observer);
}
unobserve(observer: DevTools.CDPConnection.CDPConnectionObserver): void {
this.#observers.delete(observer);
}
#startForwardingCdpEvents(session: puppeteer.CDPSession): void {
const handler = this.#handleEvent.bind(
this,
session.id(),
) as puppeteer.Handler<unknown>;
this.#sessionEventHandlers.set(session.id(), handler);
session.on('*', handler);
}
#stopForwardingCdpEvents(session: puppeteer.CDPSession): void {
const handler = this.#sessionEventHandlers.get(session.id());
if (handler) {
session.off('*', handler);
}
}
#handleEvent(
sessionId: string,
type: string | symbol | number,
event: any, // eslint-disable-line @typescript-eslint/no-explicit-any
): void {
if (
typeof type === 'string' &&
type !== CDPSessionEvent.SessionAttached &&
type !== CDPSessionEvent.SessionDetached
) {
this.#observers.forEach(observer =>
observer.onEvent({
method: type as DevTools.CDPConnection.Event,
sessionId,
params: event,
}),
);
}
}
}