Source code
Revision control
Copy as Markdown
Other Tools
/**
* @license
* Copyright 2023 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {debug} from '../common/Debug.js';
import {TargetCloseError} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {BidiConnection} from './Connection.js';
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
debug(`bidi:${prefix}`)(args);
};
/**
* @internal
*/
export async function connectBidiOverCdp(
cdp: CdpConnection
): Promise<BidiConnection> {
const transportBiDi = new NoOpTransport();
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
const pptrTransport = {
send(message: string): void {
// Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
transportBiDi.emitMessage(JSON.parse(message));
},
close(): void {
bidiServer.close();
cdpConnectionAdapter.close();
cdp.dispose();
},
onmessage(_message: string): void {
// The method is overridden by the Connection.
},
};
transportBiDi.on('bidiResponse', (message: object) => {
// Forwards a BiDi event sent by BidiServer to Puppeteer.
pptrTransport.onmessage(JSON.stringify(message));
});
const pptrBiDiConnection = new BidiConnection(
cdp.url(),
pptrTransport,
cdp.delay,
cdp.timeout
);
const bidiServer = await BidiMapper.BidiServer.createAndStart(
transportBiDi,
cdpConnectionAdapter,
cdpConnectionAdapter.browserClient(),
/* selfTargetId= */ '',
undefined,
bidiServerLogger
);
return pptrBiDiConnection;
}
/**
* Manages CDPSessions for BidiServer.
* @internal
*/
class CdpConnectionAdapter {
#cdp: CdpConnection;
#adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
#browserCdpConnection: CDPClientAdapter<CdpConnection>;
constructor(cdp: CdpConnection) {
this.#cdp = cdp;
this.#browserCdpConnection = new CDPClientAdapter(cdp);
}
browserClient(): CDPClientAdapter<CdpConnection> {
return this.#browserCdpConnection;
}
getCdpClient(id: string) {
const session = this.#cdp.session(id);
if (!session) {
throw new Error(`Unknown CDP session with id ${id}`);
}
if (!this.#adapters.has(session)) {
const adapter = new CDPClientAdapter(
session,
id,
this.#browserCdpConnection
);
this.#adapters.set(session, adapter);
return adapter;
}
return this.#adapters.get(session)!;
}
close() {
this.#browserCdpConnection.close();
for (const adapter of this.#adapters.values()) {
adapter.close();
}
}
}
/**
* Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
* BidiServer needs.
*
* @internal
*/
class CDPClientAdapter<T extends CDPSession | CdpConnection>
extends BidiMapper.EventEmitter<CDPEvents>
implements BidiMapper.CdpClient
{
#closed = false;
#client: T;
sessionId: string | undefined = undefined;
#browserClient?: BidiMapper.CdpClient;
constructor(
client: T,
sessionId?: string,
browserClient?: BidiMapper.CdpClient
) {
super();
this.#client = client;
this.sessionId = sessionId;
this.#browserClient = browserClient;
this.#client.on('*', this.#forwardMessage as Handler<any>);
}
browserClient(): BidiMapper.CdpClient {
return this.#browserClient!;
}
#forwardMessage = <T extends keyof CDPEvents>(
method: T,
event: CDPEvents[T]
) => {
this.emit(method, event);
};
async sendCommand<T extends keyof ProtocolMapping.Commands>(
method: T,
...params: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#closed) {
return;
}
try {
return await this.#client.send(method, ...params);
} catch (err) {
if (this.#closed) {
return;
}
throw err;
}
}
close() {
this.#client.off('*', this.#forwardMessage as Handler<any>);
this.#closed = true;
}
isCloseError(error: unknown): boolean {
return error instanceof TargetCloseError;
}
}
/**
* This transport is given to the BiDi server instance and allows Puppeteer
* to send and receive commands to the BiDiServer.
* @internal
*/
class NoOpTransport
extends BidiMapper.EventEmitter<{
bidiResponse: Bidi.ChromiumBidi.Message;
}>
implements BidiMapper.BidiTransport
{
#onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void =
async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
return;
};
emitMessage(message: Bidi.ChromiumBidi.Command) {
void this.#onMessage(message);
}
setOnMessage(
onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void
): void {
this.#onMessage = onMessage;
}
async sendMessage(message: Bidi.ChromiumBidi.Message): Promise<void> {
this.emit('bidiResponse', message);
}
close() {
this.#onMessage = async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
return;
};
}
}