puppeteer-core
Version:
A high-level API to control headless Chrome over the DevTools Protocol
181 lines • 6.38 kB
JavaScript
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CallbackRegistry } from '../common/CallbackRegistry.js';
import { debug } from '../common/Debug.js';
import { EventEmitter } from '../common/EventEmitter.js';
import { debugError } from '../common/util.js';
import { assert } from '../util/assert.js';
import { cdpSessions } from './BrowsingContext.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
/**
* @internal
*/
export class BidiConnection extends EventEmitter {
#url;
#transport;
#delay;
#timeout = 0;
#closed = false;
#callbacks = new CallbackRegistry();
#browsingContexts = new Map();
constructor(url, transport, delay = 0, timeout) {
super();
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180000;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.unbind.bind(this);
}
get closed() {
return this.#closed;
}
get url() {
return this.#url;
}
send(method, params) {
assert(!this.#closed, 'Protocol error: Connection closed.');
return this.#callbacks.create(method, this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
id,
method,
params,
});
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
});
}
/**
* @internal
*/
async onMessage(message) {
if (this.#delay) {
await new Promise(f => {
return setTimeout(f, this.#delay);
});
}
debugProtocolReceive(message);
const object = 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.message);
return;
case 'event':
if (isCdpEvent(object)) {
cdpSessions
.get(object.params.session)
?.emit(object.params.event, object.params.params);
return;
}
this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here.
this.emit(object.method, object.params);
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.id, `Protocol Error. Message is not in BiDi protocol format: '${message}'`, object.message);
}
debugError(object);
}
#maybeEmitOnContext(event) {
let context;
// Context specific events
if ('context' in event.params && event.params.context !== null) {
context = this.#browsingContexts.get(event.params.context);
// `log.entryAdded` specific context
}
else if ('source' in event.params &&
event.params.source.context !== undefined) {
context = this.#browsingContexts.get(event.params.source.context);
}
context?.emit(event.method, event.params);
}
registerBrowsingContexts(context) {
this.#browsingContexts.set(context.id, context);
}
getBrowsingContext(contextId) {
const currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
return currentContext;
}
getTopLevelContext(contextId) {
let currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
while (currentContext.parent) {
contextId = currentContext.parent;
currentContext = this.#browsingContexts.get(contextId);
if (!currentContext) {
throw new Error(`BrowsingContext ${contextId} does not exist.`);
}
}
return currentContext;
}
unregisterBrowsingContexts(id) {
this.#browsingContexts.delete(id);
}
/**
* 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() {
if (this.#closed) {
return;
}
this.#closed = true;
// Both may still be invoked and produce errors
this.#transport.onmessage = () => { };
this.#transport.onclose = () => { };
this.#browsingContexts.clear();
this.#callbacks.clear();
}
/**
* Unbinds the connection and closes the transport.
*/
dispose() {
this.unbind();
this.#transport.close();
}
}
/**
* @internal
*/
function createProtocolError(object) {
let message = `${object.error} ${object.message}`;
if (object.stacktrace) {
message += ` ${object.stacktrace}`;
}
return message;
}
function isCdpEvent(event) {
return event.method.startsWith('cdp.');
}
//# sourceMappingURL=Connection.js.map