chrome-devtools-frontend
Version:
Chrome DevTools UI
113 lines (99 loc) • 4.53 kB
text/typescript
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Puppeteer from '../../third_party/puppeteer/puppeteer.js';
import type * as CDPConnection from './CDPConnection.js';
// Hardcoded string literals corresponding to Puppeteer's CDPSessionEvent.SessionAttached ('sessionattached')
// and CDPSessionEvent.SessionDetached ('sessiondetached'). We redeclare these strings directly so we can use
// `import type * as Puppeteer` and avoid importing Puppeteer runtime JavaScript.
const SESSION_ATTACHED: Puppeteer.CDPSessionEventSessionAttached = 'sessionattached';
const SESSION_DETACHED: Puppeteer.CDPSessionEventSessionDetached = 'sessiondetached';
/**
* 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 CDPConnection.CDPConnection {
readonly #connection: Puppeteer.Connection;
readonly #observers = new Set<CDPConnection.CDPConnectionObserver>();
readonly #sessionEventHandlers = new Map<string, Puppeteer.Handler<unknown>>();
constructor(session: Puppeteer.CDPSession) {
const connection = session.connection();
if (!connection) {
throw new Error('CDPSession has no connection');
}
this.#connection = connection;
session.on(
SESSION_ATTACHED,
this.#startForwardingCdpEvents.bind(this) as Puppeteer.Handler<unknown>,
);
session.on(
SESSION_DETACHED,
this.#stopForwardingCdpEvents.bind(this) as Puppeteer.Handler<unknown>,
);
this.#startForwardingCdpEvents(session);
}
send<T extends CDPConnection.Command>(
method: T,
params: CDPConnection.CommandParams<T>,
sessionId: string|undefined,
): Promise<|{result: CDPConnection.CommandResult<T>}|{error: 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);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
return session.send(method as any, params).then(result => ({result})).catch(error => ({
error: {
code: (error as any).code ?? -32000,
message: (error as any).message ||
String(error),
},
})) as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
}
observe(observer: CDPConnection.CDPConnectionObserver): void {
this.#observers.add(observer);
}
unobserve(observer: 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);
this.#sessionEventHandlers.delete(session.id());
}
}
#handleEvent(
sessionId: string,
type: string|symbol|number,
event: unknown,
): void {
if (typeof type === 'string' && type !== SESSION_ATTACHED && type !== SESSION_DETACHED) {
this.#observers.forEach(
observer => observer.onEvent({
method: type as CDPConnection.Event,
sessionId,
params: event as CDPConnection.EventParams<CDPConnection.Event>,
}),
);
}
}
}