fix: implement protocol-level timeouts (#9877)
This commit is contained in:
parent
f1488b6c3a
commit
510b36c500
@ -74,7 +74,6 @@ sidebar_label: API
|
||||
| [ClickOptions](./puppeteer.clickoptions.md) | |
|
||||
| [CommonEventEmitter](./puppeteer.commoneventemitter.md) | |
|
||||
| [Configuration](./puppeteer.configuration.md) | <p>Defines options to configure Puppeteer's behavior during installation and runtime.</p><p>See individual properties for more information.</p> |
|
||||
| [ConnectionCallback](./puppeteer.connectioncallback.md) | |
|
||||
| [ConnectionTransport](./puppeteer.connectiontransport.md) | |
|
||||
| [ConnectOptions](./puppeteer.connectoptions.md) | |
|
||||
| [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) | |
|
||||
|
@ -18,5 +18,6 @@ export interface BrowserConnectOptions
|
||||
| ---------------------------------------------------------------------------- | --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| [defaultViewport?](./puppeteer.browserconnectoptions.defaultviewport.md) | | [Viewport](./puppeteer.viewport.md) \| null | _(Optional)_ Sets the viewport for each page. | |
|
||||
| [ignoreHTTPSErrors?](./puppeteer.browserconnectoptions.ignorehttpserrors.md) | | boolean | _(Optional)_ Whether to ignore HTTPS errors during navigation. | false |
|
||||
| [protocolTimeout?](./puppeteer.browserconnectoptions.protocoltimeout.md) | | number | _(Optional)_ Timeout setting for individual protocol (CDP) calls. | 30000 |
|
||||
| [slowMo?](./puppeteer.browserconnectoptions.slowmo.md) | | number | _(Optional)_ Slows down Puppeteer operations by the specified amount of milliseconds to aid debugging. | |
|
||||
| [targetFilter?](./puppeteer.browserconnectoptions.targetfilter.md) | | [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | _(Optional)_ Callback to decide if Puppeteer should connect to a given target or not. | |
|
||||
|
19
docs/api/puppeteer.browserconnectoptions.protocoltimeout.md
Normal file
19
docs/api/puppeteer.browserconnectoptions.protocoltimeout.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
sidebar_label: BrowserConnectOptions.protocolTimeout
|
||||
---
|
||||
|
||||
# BrowserConnectOptions.protocolTimeout property
|
||||
|
||||
Timeout setting for individual protocol (CDP) calls.
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
interface BrowserConnectOptions {
|
||||
protocolTimeout?: number;
|
||||
}
|
||||
```
|
||||
|
||||
#### Default value:
|
||||
|
||||
30000
|
@ -10,7 +10,12 @@ Constructs a new instance of the `Connection` class
|
||||
|
||||
```typescript
|
||||
class Connection {
|
||||
constructor(url: string, transport: ConnectionTransport, delay?: number);
|
||||
constructor(
|
||||
url: string,
|
||||
transport: ConnectionTransport,
|
||||
delay?: number,
|
||||
timeout?: number
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@ -21,3 +26,4 @@ class Connection {
|
||||
| url | string | |
|
||||
| transport | [ConnectionTransport](./puppeteer.connectiontransport.md) | |
|
||||
| delay | number | _(Optional)_ |
|
||||
| timeout | number | _(Optional)_ |
|
||||
|
@ -14,9 +14,15 @@ export declare class Connection extends EventEmitter
|
||||
|
||||
## Constructors
|
||||
|
||||
| Constructor | Modifiers | Description |
|
||||
| ------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------- |
|
||||
| [(constructor)(url, transport, delay)](./puppeteer.connection._constructor_.md) | | Constructs a new instance of the <code>Connection</code> class |
|
||||
| Constructor | Modifiers | Description |
|
||||
| ---------------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------- |
|
||||
| [(constructor)(url, transport, delay, timeout)](./puppeteer.connection._constructor_.md) | | Constructs a new instance of the <code>Connection</code> class |
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| -------------------------------------------- | --------------------- | ------ | ----------- |
|
||||
| [timeout](./puppeteer.connection.timeout.md) | <code>readonly</code> | number | |
|
||||
|
||||
## Methods
|
||||
|
||||
|
13
docs/api/puppeteer.connection.timeout.md
Normal file
13
docs/api/puppeteer.connection.timeout.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
sidebar_label: Connection.timeout
|
||||
---
|
||||
|
||||
# Connection.timeout property
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
class Connection {
|
||||
get timeout(): number;
|
||||
}
|
||||
```
|
@ -1,13 +0,0 @@
|
||||
---
|
||||
sidebar_label: ConnectionCallback.error
|
||||
---
|
||||
|
||||
# ConnectionCallback.error property
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
interface ConnectionCallback {
|
||||
error: ProtocolError;
|
||||
}
|
||||
```
|
@ -1,25 +0,0 @@
|
||||
---
|
||||
sidebar_label: ConnectionCallback
|
||||
---
|
||||
|
||||
# ConnectionCallback interface
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export interface ConnectionCallback
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description | Default |
|
||||
| -------------------------------------------------- | --------- | --------------------------------------------- | ----------- | ------- |
|
||||
| [error](./puppeteer.connectioncallback.error.md) | | [ProtocolError](./puppeteer.protocolerror.md) | | |
|
||||
| [method](./puppeteer.connectioncallback.method.md) | | string | | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
| ---------------------------------------------------------- | ----------- |
|
||||
| [reject(args)](./puppeteer.connectioncallback.reject.md) | |
|
||||
| [resolve(args)](./puppeteer.connectioncallback.resolve.md) | |
|
@ -1,13 +0,0 @@
|
||||
---
|
||||
sidebar_label: ConnectionCallback.method
|
||||
---
|
||||
|
||||
# ConnectionCallback.method property
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
interface ConnectionCallback {
|
||||
method: string;
|
||||
}
|
||||
```
|
@ -1,23 +0,0 @@
|
||||
---
|
||||
sidebar_label: ConnectionCallback.reject
|
||||
---
|
||||
|
||||
# ConnectionCallback.reject() method
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
interface ConnectionCallback {
|
||||
reject(args: unknown): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ----------- |
|
||||
| args | unknown | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
void
|
@ -1,23 +0,0 @@
|
||||
---
|
||||
sidebar_label: ConnectionCallback.resolve
|
||||
---
|
||||
|
||||
# ConnectionCallback.resolve() method
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
interface ConnectionCallback {
|
||||
resolve(args: unknown): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ----------- |
|
||||
| args | unknown | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
void
|
@ -59,6 +59,12 @@ export interface BrowserConnectOptions {
|
||||
* @internal
|
||||
*/
|
||||
protocol?: 'cdp' | 'webDriverBiDi';
|
||||
/**
|
||||
* Timeout setting for individual protocol (CDP) calls.
|
||||
*
|
||||
* @defaultValue 30000
|
||||
*/
|
||||
protocolTimeout?: number;
|
||||
}
|
||||
|
||||
const getWebSocketTransportClass = async () => {
|
||||
@ -87,6 +93,7 @@ export async function _connectToCDPBrowser(
|
||||
slowMo = 0,
|
||||
targetFilter,
|
||||
_isPageTarget: isPageTarget,
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
assert(
|
||||
@ -97,18 +104,28 @@ export async function _connectToCDPBrowser(
|
||||
|
||||
let connection!: Connection;
|
||||
if (transport) {
|
||||
connection = new Connection('', transport, slowMo);
|
||||
connection = new Connection('', transport, slowMo, protocolTimeout);
|
||||
} else if (browserWSEndpoint) {
|
||||
const WebSocketClass = await getWebSocketTransportClass();
|
||||
const connectionTransport: ConnectionTransport =
|
||||
await WebSocketClass.create(browserWSEndpoint, headers);
|
||||
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
|
||||
connection = new Connection(
|
||||
browserWSEndpoint,
|
||||
connectionTransport,
|
||||
slowMo,
|
||||
protocolTimeout
|
||||
);
|
||||
} else if (browserURL) {
|
||||
const connectionURL = await getWSEndpoint(browserURL);
|
||||
const WebSocketClass = await getWebSocketTransportClass();
|
||||
const connectionTransport: ConnectionTransport =
|
||||
await WebSocketClass.create(connectionURL);
|
||||
connection = new Connection(connectionURL, connectionTransport, slowMo);
|
||||
connection = new Connection(
|
||||
connectionURL,
|
||||
connectionTransport,
|
||||
slowMo,
|
||||
protocolTimeout
|
||||
);
|
||||
}
|
||||
const version = await connection.send('Browser.getVersion');
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
|
||||
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import {assert} from '../util/assert.js';
|
||||
import {createDeferredPromise} from '../util/util.js';
|
||||
|
||||
import {ConnectionTransport} from './ConnectionTransport.js';
|
||||
import {debug} from './Debug.js';
|
||||
@ -32,16 +33,6 @@ const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
|
||||
*/
|
||||
export {ConnectionTransport, ProtocolMapping};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ConnectionCallback {
|
||||
resolve(args: unknown): void;
|
||||
reject(args: unknown): void;
|
||||
error: ProtocolError;
|
||||
method: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal events that the Connection class emits.
|
||||
*
|
||||
@ -51,6 +42,140 @@ export const ConnectionEmittedEvents = {
|
||||
Disconnected: Symbol('Connection.Disconnected'),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type GetIdFn = () => number;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function createIncrementalIdGenerator(): GetIdFn {
|
||||
let id = 0;
|
||||
return (): number => {
|
||||
return ++id;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Callback {
|
||||
#id: number;
|
||||
#error = new ProtocolError();
|
||||
#promise = createDeferredPromise<unknown>();
|
||||
#timer?: ReturnType<typeof setTimeout>;
|
||||
#label: string;
|
||||
|
||||
constructor(id: number, label: string, timeout?: number) {
|
||||
this.#id = id;
|
||||
this.#label = label;
|
||||
if (timeout) {
|
||||
this.#timer = setTimeout(() => {
|
||||
this.#promise.reject(rewriteError(this.#error, `${label} timed out.`));
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(value: unknown): void {
|
||||
clearTimeout(this.#timer);
|
||||
this.#promise.resolve(value);
|
||||
}
|
||||
|
||||
reject(error: Error): void {
|
||||
clearTimeout(this.#timer);
|
||||
this.#promise.reject(error);
|
||||
}
|
||||
|
||||
get id(): number {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get promise(): Promise<unknown> {
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
get error(): ProtocolError {
|
||||
return this.#error;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this.#label;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages callbacks and their IDs for the protocol request/response communication.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class CallbackRegistry {
|
||||
#callbacks: Map<number, Callback> = new Map();
|
||||
#idGenerator = createIncrementalIdGenerator();
|
||||
|
||||
create(
|
||||
label: string,
|
||||
timeout: number | undefined,
|
||||
request: (id: number) => void
|
||||
): Promise<unknown> {
|
||||
const callback = new Callback(this.#idGenerator(), label, timeout);
|
||||
this.#callbacks.set(callback.id, callback);
|
||||
try {
|
||||
request(callback.id);
|
||||
} catch (error) {
|
||||
// We still throw sync errors synchronously and clean up the scheduled
|
||||
// callback.
|
||||
callback.promise.catch(() => {
|
||||
this.#callbacks.delete(callback.id);
|
||||
});
|
||||
callback.reject(error as Error);
|
||||
throw error;
|
||||
}
|
||||
// Must only have sync code up until here.
|
||||
return callback.promise.finally(() => {
|
||||
this.#callbacks.delete(callback.id);
|
||||
});
|
||||
}
|
||||
|
||||
getCallback(id: number): Callback | undefined {
|
||||
return this.#callbacks.get(id);
|
||||
}
|
||||
|
||||
reject(id: number, message: string, originalMessage?: string): void {
|
||||
const callback = this.#callbacks.get(id);
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
this._reject(callback, message, originalMessage);
|
||||
}
|
||||
|
||||
_reject(callback: Callback, message: string, originalMessage?: string): void {
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.label}): ${message}`,
|
||||
originalMessage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
resolve(id: number, value: unknown): void {
|
||||
const callback = this.#callbacks.get(id);
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
callback.resolve(value);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const callback of this.#callbacks.values()) {
|
||||
// TODO: probably we can accept error messages as params.
|
||||
this._reject(callback, 'Target closed');
|
||||
}
|
||||
this.#callbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -58,16 +183,22 @@ export class Connection extends EventEmitter {
|
||||
#url: string;
|
||||
#transport: ConnectionTransport;
|
||||
#delay: number;
|
||||
#lastId = 0;
|
||||
#timeout: number;
|
||||
#sessions: Map<string, CDPSessionImpl> = new Map();
|
||||
#closed = false;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#manuallyAttached = new Set<string>();
|
||||
#callbacks = new CallbackRegistry();
|
||||
|
||||
constructor(url: string, transport: ConnectionTransport, delay = 0) {
|
||||
constructor(
|
||||
url: string,
|
||||
transport: ConnectionTransport,
|
||||
delay = 0,
|
||||
timeout?: number
|
||||
) {
|
||||
super();
|
||||
this.#url = url;
|
||||
this.#delay = delay;
|
||||
this.#timeout = timeout ?? 30000;
|
||||
|
||||
this.#transport = transport;
|
||||
this.#transport.onmessage = this.onMessage.bind(this);
|
||||
@ -78,6 +209,10 @@ export class Connection extends EventEmitter {
|
||||
return session.connection();
|
||||
}
|
||||
|
||||
get timeout(): number {
|
||||
return this.#timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -115,26 +250,28 @@ export class Connection extends EventEmitter {
|
||||
// type-inference.
|
||||
// So now we check if there are any params or not and deal with them accordingly.
|
||||
const params = paramArgs.length ? paramArgs[0] : undefined;
|
||||
const id = this._rawSend({method, params});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
error: new ProtocolError(),
|
||||
method,
|
||||
});
|
||||
});
|
||||
return this._rawSend(this.#callbacks, method, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_rawSend(message: Record<string, unknown>): number {
|
||||
const id = ++this.#lastId;
|
||||
const stringifiedMessage = JSON.stringify(Object.assign({}, message, {id}));
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
return id;
|
||||
_rawSend<T extends keyof ProtocolMapping.Commands>(
|
||||
callbacks: CallbackRegistry,
|
||||
method: T,
|
||||
params: ProtocolMapping.Commands[T]['paramsType'][0],
|
||||
sessionId?: string
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
return callbacks.create(method, this.#timeout, id => {
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
method,
|
||||
params,
|
||||
id,
|
||||
sessionId,
|
||||
});
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
}) as Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,17 +316,14 @@ export class Connection extends EventEmitter {
|
||||
session._onMessage(object);
|
||||
}
|
||||
} else if (object.id) {
|
||||
const callback = this.#callbacks.get(object.id);
|
||||
// Callbacks could be all rejected if someone has called `.dispose()`.
|
||||
if (callback) {
|
||||
this.#callbacks.delete(object.id);
|
||||
if (object.error) {
|
||||
callback.reject(
|
||||
createProtocolError(callback.error, callback.method, object)
|
||||
);
|
||||
} else {
|
||||
callback.resolve(object.result);
|
||||
}
|
||||
if (object.error) {
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolErrorMessage(object),
|
||||
object.error.message
|
||||
);
|
||||
} else {
|
||||
this.#callbacks.resolve(object.id, object.result);
|
||||
}
|
||||
} else {
|
||||
this.emit(object.method, object.params);
|
||||
@ -203,14 +337,6 @@ export class Connection extends EventEmitter {
|
||||
this.#closed = true;
|
||||
this.#transport.onmessage = undefined;
|
||||
this.#transport.onclose = undefined;
|
||||
for (const callback of this.#callbacks.values()) {
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.method}): Target closed.`
|
||||
)
|
||||
);
|
||||
}
|
||||
this.#callbacks.clear();
|
||||
for (const session of this.#sessions.values()) {
|
||||
session._onClosed();
|
||||
@ -356,7 +482,7 @@ export class CDPSession extends EventEmitter {
|
||||
export class CDPSessionImpl extends CDPSession {
|
||||
#sessionId: string;
|
||||
#targetType: string;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#callbacks = new CallbackRegistry();
|
||||
#connection?: Connection;
|
||||
|
||||
/**
|
||||
@ -386,39 +512,29 @@ export class CDPSessionImpl extends CDPSession {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// See the comment in Connection#send explaining why we do this.
|
||||
const params = paramArgs.length ? paramArgs[0] : undefined;
|
||||
|
||||
const id = this.#connection._rawSend({
|
||||
sessionId: this.#sessionId,
|
||||
return this.#connection._rawSend(
|
||||
this.#callbacks,
|
||||
method,
|
||||
params,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
error: new ProtocolError(),
|
||||
method,
|
||||
});
|
||||
});
|
||||
this.#sessionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_onMessage(object: CDPSessionOnMessageObject): void {
|
||||
const callback = object.id ? this.#callbacks.get(object.id) : undefined;
|
||||
if (object.id && callback) {
|
||||
this.#callbacks.delete(object.id);
|
||||
if (object.id) {
|
||||
if (object.error) {
|
||||
callback.reject(
|
||||
createProtocolError(callback.error, callback.method, object)
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolErrorMessage(object),
|
||||
object.error.message
|
||||
);
|
||||
} else {
|
||||
callback.resolve(object.result);
|
||||
this.#callbacks.resolve(object.id, object.result);
|
||||
}
|
||||
} else {
|
||||
assert(!object.id);
|
||||
@ -447,14 +563,6 @@ export class CDPSessionImpl extends CDPSession {
|
||||
* @internal
|
||||
*/
|
||||
_onClosed(): void {
|
||||
for (const callback of this.#callbacks.values()) {
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.method}): Target closed.`
|
||||
)
|
||||
);
|
||||
}
|
||||
this.#callbacks.clear();
|
||||
this.#connection = undefined;
|
||||
this.emit(CDPSessionEmittedEvents.Disconnected);
|
||||
@ -468,16 +576,14 @@ export class CDPSessionImpl extends CDPSession {
|
||||
}
|
||||
}
|
||||
|
||||
function createProtocolError(
|
||||
error: ProtocolError,
|
||||
method: string,
|
||||
object: {error: {message: string; data: any; code: number}}
|
||||
): Error {
|
||||
let message = `Protocol error (${method}): ${object.error.message}`;
|
||||
function createProtocolErrorMessage(object: {
|
||||
error: {message: string; data: any; code: number};
|
||||
}): string {
|
||||
let message = `${object.error.message}`;
|
||||
if ('data' in object.error) {
|
||||
message += ` ${object.error.data}`;
|
||||
}
|
||||
return rewriteError(error, message, object.error.message);
|
||||
return message;
|
||||
}
|
||||
|
||||
function rewriteError(
|
||||
|
@ -16,10 +16,9 @@
|
||||
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {ConnectionCallback} from '../Connection.js';
|
||||
import {CallbackRegistry} from '../Connection.js';
|
||||
import {ConnectionTransport} from '../ConnectionTransport.js';
|
||||
import {debug} from '../Debug.js';
|
||||
import {ProtocolError} from '../Errors.js';
|
||||
import {EventEmitter} from '../EventEmitter.js';
|
||||
|
||||
import {Context} from './Context.js';
|
||||
@ -81,14 +80,15 @@ interface Commands {
|
||||
export class Connection extends EventEmitter {
|
||||
#transport: ConnectionTransport;
|
||||
#delay: number;
|
||||
#lastId = 0;
|
||||
#timeout? = 0;
|
||||
#closed = false;
|
||||
#callbacks: Map<number, ConnectionCallback> = new Map();
|
||||
#callbacks = new CallbackRegistry();
|
||||
#contexts: Map<string, Context> = new Map();
|
||||
|
||||
constructor(transport: ConnectionTransport, delay = 0) {
|
||||
constructor(transport: ConnectionTransport, delay = 0, timeout?: number) {
|
||||
super();
|
||||
this.#delay = delay;
|
||||
this.#timeout = timeout;
|
||||
|
||||
this.#transport = transport;
|
||||
this.#transport.onmessage = this.onMessage.bind(this);
|
||||
@ -107,22 +107,15 @@ export class Connection extends EventEmitter {
|
||||
method: T,
|
||||
params: Commands[T]['params']
|
||||
): Promise<Commands[T]['returnType']> {
|
||||
const id = ++this.#lastId;
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
} as Bidi.Message.CommandRequest);
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
error: new ProtocolError(),
|
||||
return this.#callbacks.create(method, this.#timeout, id => {
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
});
|
||||
});
|
||||
params,
|
||||
} as Bidi.Message.CommandRequest);
|
||||
debugProtocolSend(stringifiedMessage);
|
||||
this.#transport.send(stringifiedMessage);
|
||||
}) as Promise<Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,23 +133,23 @@ export class Connection extends EventEmitter {
|
||||
| Bidi.Message.EventMessage;
|
||||
|
||||
if ('id' in object) {
|
||||
const callback = this.#callbacks.get(object.id);
|
||||
// Callbacks could be all rejected if someone has called `.dispose()`.
|
||||
if (callback) {
|
||||
this.#callbacks.delete(object.id);
|
||||
if ('error' in object) {
|
||||
callback.reject(
|
||||
createProtocolError(callback.error, callback.method, object)
|
||||
if ('error' in object) {
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolError(object),
|
||||
object.message
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
this.#callbacks.getCallback(object.id)?.label ===
|
||||
'browsingContext.create'
|
||||
) {
|
||||
this.#contexts.set(
|
||||
object.result.context,
|
||||
new Context(this, object.result)
|
||||
);
|
||||
} else {
|
||||
if (callback.method === 'browsingContext.create') {
|
||||
this.#contexts.set(
|
||||
object.result.context,
|
||||
new Context(this, object.result)
|
||||
);
|
||||
}
|
||||
callback.resolve(object);
|
||||
}
|
||||
this.#callbacks.resolve(object.id, object);
|
||||
}
|
||||
} else {
|
||||
let context: Context | undefined;
|
||||
@ -178,14 +171,6 @@ export class Connection extends EventEmitter {
|
||||
this.#closed = true;
|
||||
this.#transport.onmessage = undefined;
|
||||
this.#transport.onclose = undefined;
|
||||
for (const callback of this.#callbacks.values()) {
|
||||
callback.reject(
|
||||
rewriteError(
|
||||
callback.error,
|
||||
`Protocol error (${callback.method}): Connection closed.`
|
||||
)
|
||||
);
|
||||
}
|
||||
this.#callbacks.clear();
|
||||
}
|
||||
|
||||
@ -195,27 +180,13 @@ export class Connection extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteError(
|
||||
error: ProtocolError,
|
||||
message: string,
|
||||
originalMessage?: string
|
||||
): Error {
|
||||
error.message = message;
|
||||
error.originalMessage = originalMessage ?? error.originalMessage;
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
function createProtocolError(
|
||||
error: ProtocolError,
|
||||
method: string,
|
||||
object: Bidi.Message.ErrorResult
|
||||
): Error {
|
||||
let message = `Protocol error (${method}): ${object.error} ${object.message}`;
|
||||
function createProtocolError(object: Bidi.Message.ErrorResult): string {
|
||||
let message = `${object.error} ${object.message}`;
|
||||
if (object.stacktrace) {
|
||||
message += ` ${object.stacktrace}`;
|
||||
}
|
||||
return rewriteError(error, message, object.message);
|
||||
return message;
|
||||
}
|
||||
|
@ -252,10 +252,11 @@ export class BrowserRunner {
|
||||
timeout: number;
|
||||
slowMo: number;
|
||||
preferredRevision: string;
|
||||
protocolTimeout?: number;
|
||||
}): Promise<BiDiConnection> {
|
||||
assert(this.proc, 'BrowserRunner not started.');
|
||||
|
||||
const {timeout, slowMo, preferredRevision} = options;
|
||||
const {timeout, slowMo, preferredRevision, protocolTimeout} = options;
|
||||
let browserWSEndpoint = await waitForWSEndpoint(
|
||||
this.proc,
|
||||
timeout,
|
||||
@ -267,7 +268,7 @@ export class BrowserRunner {
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
);
|
||||
return new BiDi.Connection(transport, slowMo);
|
||||
return new BiDi.Connection(transport, slowMo, protocolTimeout);
|
||||
}
|
||||
|
||||
async setupConnection(options: {
|
||||
@ -275,10 +276,12 @@ export class BrowserRunner {
|
||||
timeout: number;
|
||||
slowMo: number;
|
||||
preferredRevision: string;
|
||||
protocolTimeout?: number;
|
||||
}): Promise<Connection> {
|
||||
assert(this.proc, 'BrowserRunner not started.');
|
||||
|
||||
const {usePipe, timeout, slowMo, preferredRevision} = options;
|
||||
const {usePipe, timeout, slowMo, preferredRevision, protocolTimeout} =
|
||||
options;
|
||||
if (!usePipe) {
|
||||
const browserWSEndpoint = await waitForWSEndpoint(
|
||||
this.proc,
|
||||
@ -286,7 +289,12 @@ export class BrowserRunner {
|
||||
preferredRevision
|
||||
);
|
||||
const transport = await WebSocketTransport.create(browserWSEndpoint);
|
||||
this.connection = new Connection(browserWSEndpoint, transport, slowMo);
|
||||
this.connection = new Connection(
|
||||
browserWSEndpoint,
|
||||
transport,
|
||||
slowMo,
|
||||
protocolTimeout
|
||||
);
|
||||
} else {
|
||||
// stdio was assigned during start(), and the 'pipe' option there adds the
|
||||
// 4th and 5th items to stdio array
|
||||
@ -295,7 +303,7 @@ export class BrowserRunner {
|
||||
pipeWrite as NodeJS.WritableStream,
|
||||
pipeRead as NodeJS.ReadableStream
|
||||
);
|
||||
this.connection = new Connection('', transport, slowMo);
|
||||
this.connection = new Connection('', transport, slowMo, protocolTimeout);
|
||||
}
|
||||
return this.connection;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
waitForInitialPage = true,
|
||||
debuggingPort,
|
||||
protocol,
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
const chromeArguments = [];
|
||||
@ -127,6 +128,7 @@ export class ChromeLauncher extends ProductLauncher {
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
|
||||
if (protocol === 'webDriverBiDi') {
|
||||
|
@ -43,6 +43,7 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
waitForInitialPage = true,
|
||||
debuggingPort = null,
|
||||
protocol = 'cdp',
|
||||
protocolTimeout,
|
||||
} = options;
|
||||
|
||||
const firefoxArguments = [];
|
||||
@ -132,6 +133,7 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
const BiDi = await import(
|
||||
/* webpackIgnore: true */ '../common/bidi/bidi.js'
|
||||
@ -156,6 +158,7 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
timeout,
|
||||
slowMo,
|
||||
preferredRevision: this.puppeteer.browserRevision,
|
||||
protocolTimeout,
|
||||
});
|
||||
browser = await CDPBrowser._create(
|
||||
this.product,
|
||||
|
Loading…
Reference in New Issue
Block a user