Skip to content

Commit f242e0c

Browse files
authored
fix: make Transport.send() synchronous (#1177)
1 parent 5bd6e49 commit f242e0c

9 files changed

+82
-81
lines changed

src/chromium/crConnection.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ export class CRSession extends platform.EventEmitter {
140140
this.once = super.once;
141141
}
142142

143-
send<T extends keyof Protocol.CommandParameters>(
143+
async send<T extends keyof Protocol.CommandParameters>(
144144
method: T,
145145
params?: Protocol.CommandParameters[T]
146146
): Promise<Protocol.CommandReturnValues[T]> {
147147
if (!this._connection)
148-
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
148+
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
149149
const id = this._connection._rawSend(this._sessionId, { method, params });
150150
return new Promise((resolve, reject) => {
151151
this._callbacks.set(id, {resolve, reject, error: new Error(), method});

src/chromium/crExecutionContext.ts

+11-16
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,15 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
7272
throw new Error('Passed function is not well-serializable!');
7373
}
7474
}
75-
let callFunctionOnPromise;
76-
try {
77-
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
78-
functionDeclaration: functionText + '\n' + suffix + '\n',
79-
executionContextId: this._contextId,
80-
arguments: args.map(convertArgument.bind(this)),
81-
returnByValue,
82-
awaitPromise: true,
83-
userGesture: true
84-
});
85-
} catch (err) {
86-
if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON'))
87-
err.message += ' Are you passing a nested JSHandle?';
88-
throw err;
89-
}
90-
const { exceptionDetails, result: remoteObject } = await callFunctionOnPromise.catch(rewriteError);
75+
76+
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
77+
functionDeclaration: functionText + '\n' + suffix + '\n',
78+
executionContextId: this._contextId,
79+
arguments: args.map(convertArgument.bind(this)),
80+
returnByValue,
81+
awaitPromise: true,
82+
userGesture: true
83+
}).catch(rewriteError);
9184
if (exceptionDetails)
9285
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
9386
return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject);
@@ -127,6 +120,8 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
127120

128121
if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed') || error.message.endsWith('Execution context was destroyed.'))
129122
throw new Error('Execution context was destroyed, most likely because of a navigation.');
123+
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
124+
error.message += ' Are you passing a nested JSHandle?';
130125
throw error;
131126
}
132127
}

src/firefox/ffConnection.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class FFConnection extends platform.EventEmitter {
6868
return this._sessions.get(sessionId) || null;
6969
}
7070

71-
send<T extends keyof Protocol.CommandParameters>(
71+
async send<T extends keyof Protocol.CommandParameters>(
7272
method: T,
7373
params?: Protocol.CommandParameters[T]
7474
): Promise<Protocol.CommandReturnValues[T]> {
@@ -179,12 +179,12 @@ export class FFSession extends platform.EventEmitter {
179179
this.once = super.once;
180180
}
181181

182-
send<T extends keyof Protocol.CommandParameters>(
182+
async send<T extends keyof Protocol.CommandParameters>(
183183
method: T,
184184
params?: Protocol.CommandParameters[T]
185185
): Promise<Protocol.CommandReturnValues[T]> {
186186
if (this._disposed)
187-
return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`));
187+
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
188188
const id = this._connection.nextMessageId();
189189
this._rawSend({method, params, id});
190190
return new Promise((resolve, reject) => {

src/firefox/ffExecutionContext.ts

+9-14
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,13 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
7979
return {unserializableValue: 'NaN'};
8080
return {value: arg};
8181
});
82-
let callFunctionPromise;
83-
try {
84-
callFunctionPromise = this._session.send('Runtime.callFunction', {
85-
functionDeclaration: functionText,
86-
args: protocolArgs,
87-
returnByValue,
88-
executionContextId: this._executionContextId
89-
});
90-
} catch (err) {
91-
if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON'))
92-
err.message += ' Are you passing a nested JSHandle?';
93-
throw err;
94-
}
95-
const payload = await callFunctionPromise.catch(rewriteError);
82+
83+
const payload = await this._session.send('Runtime.callFunction', {
84+
functionDeclaration: functionText,
85+
args: protocolArgs,
86+
returnByValue,
87+
executionContextId: this._executionContextId
88+
}).catch(rewriteError);
9689
checkException(payload.exceptionDetails);
9790
if (returnByValue)
9891
return deserializeValue(payload.result!);
@@ -103,6 +96,8 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
10396
return {result: {type: 'undefined', value: undefined}};
10497
if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!'))
10598
throw new Error('Execution context was destroyed, most likely because of a navigation.');
99+
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
100+
error.message += ' Are you passing a nested JSHandle?';
106101
throw error;
107102
}
108103
}

src/platform.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -311,22 +311,27 @@ export function makeWaitForNextTask() {
311311
};
312312
}
313313

314-
export class WebSocketTransport implements ConnectionTransport {
315-
private _ws: WebSocket;
314+
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to
315+
// avoid missing incoming messages.
316+
export async function connectToWebsocket<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T>): Promise<T> {
317+
const transport = new WebSocketTransport(url);
318+
return new Promise<T>((fulfill, reject) => {
319+
transport._ws.addEventListener('open', async () => fulfill(await onopen(transport)));
320+
transport._ws.addEventListener('error', event => reject(new Error('WebSocket error: ' + (event as ErrorEvent).message)));
321+
});
322+
}
323+
324+
class WebSocketTransport implements ConnectionTransport {
325+
_ws: WebSocket;
316326

317327
onmessage?: (message: string) => void;
318328
onclose?: () => void;
319-
private _connectPromise: Promise<(Error|null)>;
320329

321330
constructor(url: string) {
322331
this._ws = (isNode ? new NodeWebSocket(url, [], {
323332
perMessageDeflate: false,
324333
maxPayload: 256 * 1024 * 1024, // 256Mb
325334
}) : new WebSocket(url)) as WebSocket;
326-
this._connectPromise = new Promise(fulfill => {
327-
this._ws.addEventListener('open', () => fulfill(null));
328-
this._ws.addEventListener('error', event => fulfill(new Error('WebSocket error: ' + (event as ErrorEvent).message)));
329-
});
330335
// The 'ws' module in node sometimes sends us multiple messages in a single task.
331336
// In Web, all IO callbacks (e.g. WebSocket callbacks)
332337
// are dispatched into separate tasks, so there's no need
@@ -348,10 +353,7 @@ export class WebSocketTransport implements ConnectionTransport {
348353
this._ws.addEventListener('error', () => {});
349354
}
350355

351-
async send(message: string) {
352-
const error = await this._connectPromise;
353-
if (error)
354-
throw error;
356+
send(message: string) {
355357
this._ws.send(message);
356358
}
357359

src/server/chromium.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class Chromium implements BrowserType {
128128
// We try to gracefully close to prevent crash reporting and core dumps.
129129
// Note that it's fine to reuse the pipe transport, since
130130
// our connection ignores kBrowserCloseMessageId.
131-
const t = transport || new platform.WebSocketTransport(browserWSEndpoint!);
131+
const t = transport || await platform.connectToWebsocket(browserWSEndpoint!, async transport => transport);
132132
const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
133133
await t.send(JSON.stringify(message));
134134
},
@@ -153,8 +153,9 @@ export class Chromium implements BrowserType {
153153
}
154154

155155
async connect(options: ConnectOptions): Promise<CRBrowser> {
156-
const transport = new platform.WebSocketTransport(options.wsEndpoint);
157-
return CRBrowser.connect(transport, options.slowMo);
156+
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
157+
return CRBrowser.connect(transport, options.slowMo);
158+
});
158159
}
159160

160161
executablePath(): string {

src/server/firefox.ts

+27-23
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,24 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { FFBrowser } from '../firefox/ffBrowser';
19-
import { BrowserFetcher, OnProgressCallback, BrowserFetcherOptions } from './browserFetcher';
20-
import { DeviceDescriptors } from '../deviceDescriptors';
21-
import { launchProcess, waitForLine } from './processLauncher';
22-
import * as types from '../types';
23-
import * as platform from '../platform';
24-
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
2518
import * as fs from 'fs';
2619
import * as os from 'os';
2720
import * as path from 'path';
2821
import * as util from 'util';
22+
import { ConnectOptions, LaunchType } from '../browser';
23+
import { BrowserContext } from '../browserContext';
24+
import { DeviceDescriptors } from '../deviceDescriptors';
2925
import { TimeoutError } from '../errors';
26+
import { Events } from '../events';
27+
import { FFBrowser } from '../firefox/ffBrowser';
28+
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
3029
import { assert, helper } from '../helper';
31-
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
32-
import { ConnectOptions, LaunchType } from '../browser';
30+
import * as platform from '../platform';
31+
import * as types from '../types';
32+
import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback } from './browserFetcher';
3333
import { BrowserServer } from './browserServer';
34-
import { Events } from '../events';
35-
import { ConnectionTransport } from '../transport';
36-
import { BrowserContext } from '../browserContext';
34+
import { BrowserArgOptions, BrowserType, LaunchOptions } from './browserType';
35+
import { launchProcess, waitForLine } from './processLauncher';
3736

3837
const mkdtempAsync = platform.promisify(fs.mkdtemp);
3938

@@ -64,30 +63,34 @@ export class Firefox implements BrowserType {
6463
async launch(options?: LaunchOptions & { slowMo?: number }): Promise<FFBrowser> {
6564
if (options && (options as any).userDataDir)
6665
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistent` instead');
67-
const { browserServer, transport } = await this._launchServer(options, 'local');
68-
const browser = await FFBrowser.connect(transport!, options && options.slowMo);
66+
const browserServer = await this._launchServer(options, 'local');
67+
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
68+
return FFBrowser.connect(transport, options && options.slowMo);
69+
});
6970
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
7071
browser.close = () => browserServer.close();
7172
(browser as any)['__server__'] = browserServer;
7273
return browser;
7374
}
7475

7576
async launchServer(options?: LaunchOptions & { port?: number }): Promise<BrowserServer> {
76-
return (await this._launchServer(options, 'server', undefined, options && options.port)).browserServer;
77+
return await this._launchServer(options, 'server', undefined, options && options.port);
7778
}
7879

7980
async launchPersistent(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext> {
8081
const { timeout = 30000 } = options || {};
81-
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
82-
const browser = await FFBrowser.connect(transport!);
82+
const browserServer = await this._launchServer(options, 'persistent', userDataDir);
83+
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
84+
return FFBrowser.connect(transport);
85+
});
8386
await helper.waitWithTimeout(browser._waitForTarget(t => t.type() === 'page'), 'first page', timeout);
8487
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
8588
const browserContext = browser._defaultContext;
8689
browserContext.close = () => browserServer.close();
8790
return browserContext;
8891
}
8992

90-
private async _launchServer(options: LaunchOptions = {}, launchType: LaunchType, userDataDir?: string, port?: number): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> {
93+
private async _launchServer(options: LaunchOptions = {}, launchType: LaunchType, userDataDir?: string, port?: number): Promise<BrowserServer> {
9194
const {
9295
ignoreDefaultArgs = false,
9396
args = [],
@@ -144,7 +147,7 @@ export class Firefox implements BrowserType {
144147
// We try to gracefully close to prevent crash reporting and core dumps.
145148
// Note that it's fine to reuse the pipe transport, since
146149
// our connection ignores kBrowserCloseMessageId.
147-
const transport = new platform.WebSocketTransport(browserWSEndpoint);
150+
const transport = await platform.connectToWebsocket(browserWSEndpoint, async transport => transport);
148151
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
149152
await transport.send(JSON.stringify(message));
150153
},
@@ -157,13 +160,14 @@ export class Firefox implements BrowserType {
157160
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
158161
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
159162
const browserWSEndpoint = match[1];
160-
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? browserWSEndpoint : null);
161-
return { browserServer, transport: launchType === 'server' ? undefined : new platform.WebSocketTransport(browserWSEndpoint) };
163+
browserServer = new BrowserServer(launchedProcess, gracefullyClose, browserWSEndpoint);
164+
return browserServer;
162165
}
163166

164167
async connect(options: ConnectOptions): Promise<FFBrowser> {
165-
const transport = new platform.WebSocketTransport(options.wsEndpoint);
166-
return FFBrowser.connect(transport, options.slowMo);
168+
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
169+
return FFBrowser.connect(transport, options.slowMo);
170+
});
167171
}
168172

169173
executablePath(): string {

src/server/webkit.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,9 @@ export class WebKit implements BrowserType {
156156
}
157157

158158
async connect(options: ConnectOptions): Promise<WKBrowser> {
159-
const transport = new platform.WebSocketTransport(options.wsEndpoint);
160-
return WKBrowser.connect(transport, options.slowMo);
159+
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
160+
return WKBrowser.connect(transport, options.slowMo);
161+
});
161162
}
162163

163164
executablePath(): string {
@@ -273,7 +274,7 @@ class SequenceNumberMixer<V> {
273274
}
274275
}
275276

276-
async function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
277+
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
277278
const server = new ws.Server({ port });
278279
const guid = uuidv4();
279280
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();

src/web.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,23 @@ import * as platform from './platform';
2222
const connect = {
2323
chromium: {
2424
connect: async (url: string) => {
25-
const transport = new platform.WebSocketTransport(url);
26-
return ChromiumBrowser.connect(transport);
25+
return await platform.connectToWebsocket(url, transport => {
26+
return ChromiumBrowser.connect(transport);
27+
});
2728
}
2829
},
2930
webkit: {
3031
connect: async (url: string) => {
31-
const transport = new platform.WebSocketTransport(url);
32-
return WebKitBrowser.connect(transport);
32+
return await platform.connectToWebsocket(url, transport => {
33+
return WebKitBrowser.connect(transport);
34+
});
3335
}
3436
},
3537
firefox: {
3638
connect: async (url: string) => {
37-
const transport = new platform.WebSocketTransport(url);
38-
return FirefoxBrowser.connect(transport);
39+
return await platform.connectToWebsocket(url, transport => {
40+
return FirefoxBrowser.connect(transport);
41+
});
3942
}
4043
}
4144
};

0 commit comments

Comments
 (0)