Source code
Revision control
Copy as Markdown
Other Tools
/**
* @license
* Copyright 2017 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {debug} from '../common/Debug.js';
import type {EventsWithWildcard} from '../common/EventEmitter.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {BidiCdpSession} from './CDPSession.js';
import type {
Commands as BidiCommands,
BidiEvents,
Connection,
} from './core/Connection.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
/**
* @internal
*/
export interface Commands extends BidiCommands {
'cdp.sendCommand': {
params: Bidi.Cdp.SendCommandParameters;
returnType: Bidi.Cdp.SendCommandResult;
};
'cdp.getSession': {
params: Bidi.Cdp.GetSessionParameters;
returnType: Bidi.Cdp.GetSessionResult;
};
'cdp.resolveRealm': {
params: Bidi.Cdp.ResolveRealmParameters;
returnType: Bidi.Cdp.ResolveRealmResult;
};
}
/**
* @internal
*/
export class BidiConnection
extends EventEmitter<BidiEvents>
implements Connection
{
#url: string;
#transport: ConnectionTransport;
#delay: number;
#timeout = 0;
#closed = false;
#callbacks = new CallbackRegistry();
#emitters: Array<EventEmitter<any>> = [];
constructor(
url: string,
transport: ConnectionTransport,
delay = 0,
timeout?: number
) {
super();
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180_000;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.unbind.bind(this);
}
get closed(): boolean {
return this.#closed;
}
get url(): string {
return this.#url;
}
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
this.#emitters.push(emitter);
}
override emit<Key extends keyof EventsWithWildcard<BidiEvents>>(
type: Key,
event: EventsWithWildcard<BidiEvents>[Key]
): boolean {
for (const emitter of this.#emitters) {
emitter.emit(type, event);
}
return super.emit(type, event);
}
send<T extends keyof Commands>(
method: T,
params: Commands[T]['params'],
timeout?: number
): Promise<{result: Commands[T]['returnType']}> {
assert(!this.#closed, 'Protocol error: Connection closed.');
return this.#callbacks.create(method, timeout ?? this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
id,
method,
params,
} as Bidi.Command);
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
}) as Promise<{result: Commands[T]['returnType']}>;
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object: Bidi.ChromiumBidi.Message = JSON.parse(message);
if ('type' in object) {
switch (object.type) {
case 'success':
this.#callbacks.resolve(object.id, object);
return;
case 'error':
if (object.id === null) {
break;
}
this.#callbacks.reject(
object.id,
createProtocolError(object),
`${object.error}: ${object.message}`
);
return;
case 'event':
if (isCdpEvent(object)) {
BidiCdpSession.sessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
// SAFETY: We know the method and parameter still match here.
this.emit(
object.method,
object.params as BidiEvents[keyof BidiEvents]
);
return;
}
}
// Even if the response in not in BiDi protocol format but `id` is provided, reject
// the callback. This can happen if the endpoint supports CDP instead of BiDi.
if ('id' in object) {
this.#callbacks.reject(
(object as {id: number}).id,
`Protocol Error. Message is not in BiDi protocol format: '${message}'`,
object.message
);
}
debugError(object);
}
/**
* Unbinds the connection, but keeps the transport open. Useful when the transport will
* be reused by other connection e.g. with different protocol.
* @internal
*/
unbind(): void {
if (this.#closed) {
return;
}
this.#closed = true;
// Both may still be invoked and produce errors
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#callbacks.clear();
}
/**
* Unbinds the connection and closes the transport.
*/
dispose(): void {
this.unbind();
this.#transport.close();
}
getPendingProtocolErrors(): Error[] {
return this.#callbacks.getPendingProtocolErrors();
}
}
/**
* @internal
*/
function createProtocolError(object: Bidi.ErrorResponse): string {
let message = `${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return message;
}
function isCdpEvent(event: Bidi.ChromiumBidi.Event): event is Bidi.Cdp.Event {
return event.method.startsWith('cdp.');
}