playwright/browser_patches/firefox/juggler/SimpleChannel.js
Andrey Lushnikov 2b495c9750
browser(firefox): fix SimpleChannel to await initialization (#4311)
As Joel noticed recently, MessageManager in firefox doesn't guarantee
message delivery if the opposite end hasn't been initialized yet. In
this case, message will be silently dropped on the ground.

To fix this, we establish a handshake in SimpleChannel to make sure that
both ends are initialized, end buffer outgoing messages until this
happens.

Drive-by: serialize dialog events to only deliver *after* the
`Page.ready` protocol event. Otherwise, we deliver dialog events to the
unreported page.
2020-11-02 16:21:34 -08:00

181 lines
6.2 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Note: this file should be loadabale with eval() into worker environment.
// Avoid Components.*, ChromeUtils and global const variables.
const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
class SimpleChannel {
static createForMessageManager(name, mm) {
const channel = new SimpleChannel(name);
const messageListener = {
receiveMessage: message => channel._onMessage(message.data)
};
mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
channel.setTransport({
sendMessage: obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj),
dispose: () => mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener),
});
return channel;
}
constructor(name) {
this._name = name;
this._messageId = 0;
this._connectorId = 0;
this._pendingMessages = new Map();
this._handlers = new Map();
this._bufferedIncomingMessages = [];
this._bufferedOutgoingMessages = [];
this.transport = {
sendMessage: null,
dispose: null,
};
this._ready = false;
this._disposed = false;
}
setTransport(transport) {
this.transport = transport;
// connection handshake:
// 1. There are two channel ends in different processes.
// 2. Both ends start in the `ready = false` state, meaning that they will
// not send any messages over transport.
// 3. Once channel end is created, it sends `READY` message to the other end.
// 4. Eventually, at least one of the ends receives `READY` message and responds with
// `READY_ACK`. We assume at least one of the ends will receive "READY" event from the other, since
// channel ends have a "parent-child" relation, i.e. one end is always created before the other one.
// 5. Once channel end receives either `READY` or `READY_ACK`, it transitions to `ready` state.
this.transport.sendMessage('READY');
}
_markAsReady() {
if (this._ready)
return;
this._ready = true;
for (const msg of this._bufferedOutgoingMessages)
this.transport.sendMessage(msg);
this._bufferedOutgoingMessages = [];
}
dispose() {
if (this._disposed)
return;
this._disposed = true;
for (const {resolve, reject, methodName} of this._pendingMessages.values())
reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
this._pendingMessages.clear();
this._handlers.clear();
this.transport.dispose();
}
_rejectCallbacksFromConnector(connectorId) {
for (const [messageId, callback] of this._pendingMessages) {
if (callback.connectorId === connectorId) {
callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
this._pendingMessages.delete(messageId);
}
}
}
connect(namespace) {
const connectorId = ++this._connectorId;
return {
send: (...args) => this._send(namespace, connectorId, ...args),
emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
dispose: () => this._rejectCallbacksFromConnector(connectorId),
};
}
register(namespace, handler) {
if (this._handlers.has(namespace))
throw new Error('ERROR: double-register for namespace ' + namespace);
this._handlers.set(namespace, handler);
// Try to re-deliver all pending messages.
const bufferedRequests = this._bufferedIncomingMessages;
this._bufferedIncomingMessages = [];
for (const data of bufferedRequests) {
this._onMessage(data);
}
return () => this.unregister(namespace);
}
unregister(namespace) {
this._handlers.delete(namespace);
}
/**
* @param {string} namespace
* @param {number} connectorId
* @param {string} methodName
* @param {...*} params
* @return {!Promise<*>}
*/
async _send(namespace, connectorId, methodName, ...params) {
if (this._disposed)
throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
const id = ++this._messageId;
const promise = new Promise((resolve, reject) => {
this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace});
});
const message = {requestId: id, methodName, params, namespace};
if (this._ready)
this.transport.sendMessage(message);
else
this._bufferedOutgoingMessages.push(message);
return promise;
}
async _onMessage(data) {
if (data === 'READY') {
this.transport.sendMessage('READY_ACK');
this._markAsReady();
return;
}
if (data === 'READY_ACK') {
this._markAsReady();
return;
}
if (data.responseId) {
const {resolve, reject} = this._pendingMessages.get(data.responseId);
this._pendingMessages.delete(data.responseId);
if (data.error)
reject(new Error(data.error));
else
resolve(data.result);
} else if (data.requestId) {
const namespace = data.namespace;
const handler = this._handlers.get(namespace);
if (!handler) {
this._bufferedIncomingMessages.push(data);
return;
}
const method = handler[data.methodName];
if (!method) {
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
return;
}
try {
const result = await method.call(handler, ...data.params);
this.transport.sendMessage({responseId: data.requestId, result});
} catch (error) {
this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
return;
}
} else {
dump(`
ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)}
`);
}
}
}
var EXPORTED_SYMBOLS = ['SimpleChannel'];
this.SimpleChannel = SimpleChannel;