mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			257 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			9.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 {
 | |
|   constructor(name, uid) {
 | |
|     this._name = name;
 | |
|     this._messageId = 0;
 | |
|     this._connectorId = 0;
 | |
|     this._pendingMessages = new Map();
 | |
|     this._handlers = new Map();
 | |
|     this._bufferedIncomingMessages = [];
 | |
|     this.transport = {
 | |
|       sendMessage: null,
 | |
|       dispose: () => {},
 | |
|     };
 | |
|     this._ready = false;
 | |
|     this._paused = false;
 | |
|     this._disposed = false;
 | |
| 
 | |
|     this._bufferedResponses = new Map();
 | |
|     // This is a "unique" identifier of this end of the channel. Two SimpleChannel instances
 | |
|     // on the same end of the channel (e.g. two content processes) must not have the same id.
 | |
|     // This way, the other end can distinguish between the old peer with a new transport and a new peer.
 | |
|     this._uid = uid;
 | |
|     this._connectedToUID = undefined;
 | |
|   }
 | |
| 
 | |
|   bindToActor(actor) {
 | |
|     this.resetTransport();
 | |
|     this._name = actor.actorName;
 | |
|     const oldReceiveMessage = actor.receiveMessage;
 | |
|     actor.receiveMessage = message => this._onMessage(message.data);
 | |
|     this.setTransport({
 | |
|       sendMessage: obj => actor.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj),
 | |
|       dispose: () => actor.receiveMessage = oldReceiveMessage,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   resetTransport() {
 | |
|     this.transport.dispose();
 | |
|     this.transport = {
 | |
|       sendMessage: null,
 | |
|       dispose: () => {},
 | |
|     };
 | |
|     this._ready = 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 { ack: `READY` } message to the other end.
 | |
|     // 4. Eventually, at least one of the ends receives { ack: `READY` } message and responds with
 | |
|     //    { ack: `READY_ACK` }. We assume at least one of the ends will receive { ack: "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 { ack: `READY` } or { ack: `READY_ACK` }, it transitions to `ready` state.
 | |
|     this.transport.sendMessage({ ack: 'READY', uid: this._uid });
 | |
|   }
 | |
| 
 | |
|   pause() {
 | |
|     this._paused = true;
 | |
|   }
 | |
| 
 | |
|   resumeSoon() {
 | |
|     if (!this._paused)
 | |
|       return;
 | |
|     this._paused = false;
 | |
|     this._setTimeout(() => this._deliverBufferedIncomingMessages(), 0);
 | |
|   }
 | |
| 
 | |
|   _setTimeout(cb, timeout) {
 | |
|     // Lazy load on first call.
 | |
|     this._setTimeout = ChromeUtils.import('resource://gre/modules/Timer.jsm').setTimeout;
 | |
|     this._setTimeout(cb, timeout);
 | |
|   }
 | |
| 
 | |
|   _markAsReady() {
 | |
|     if (this._ready)
 | |
|       return;
 | |
|     this._ready = true;
 | |
|     for (const { message } of this._pendingMessages.values())
 | |
|       this.transport.sendMessage(message);
 | |
|   }
 | |
| 
 | |
|   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);
 | |
|     this._deliverBufferedIncomingMessages();
 | |
|     return () => this.unregister(namespace);
 | |
|   }
 | |
| 
 | |
|   _deliverBufferedIncomingMessages() {
 | |
|     const bufferedRequests = this._bufferedIncomingMessages;
 | |
|     this._bufferedIncomingMessages = [];
 | |
|     for (const data of bufferedRequests) {
 | |
|       this._onMessage(data);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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 message = {requestId: id, methodName, params, namespace};
 | |
|     const promise = new Promise((resolve, reject) => {
 | |
|       this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace, message});
 | |
|     });
 | |
|     if (this._ready)
 | |
|       this.transport.sendMessage(message);
 | |
|     return promise;
 | |
|   }
 | |
| 
 | |
|   _onMessage(data) {
 | |
|     if (data?.ack === 'READY') {
 | |
|       // The "READY" and "READY_ACK" messages are a part of initialization sequence.
 | |
|       // This sequence happens when:
 | |
|       // 1. A new SimpleChannel instance is getting initialized on the other end.
 | |
|       //    In this case, it will have a different UID and we must clear
 | |
|       //    `this._bufferedResponses` since they are no longer relevant.
 | |
|       // 2. A new transport is assigned to communicate between 2 SimpleChannel instances.
 | |
|       //    In this case, we MUST NOT clear `this._bufferedResponses` since they are used
 | |
|       //    to address the double-dispatch issue.
 | |
|       if (this._connectedToUID !== data.uid)
 | |
|         this._bufferedResponses.clear();
 | |
|       this._connectedToUID = data.uid;
 | |
|       this.transport.sendMessage({ ack: 'READY_ACK', uid: this._uid });
 | |
|       this._markAsReady();
 | |
|       return;
 | |
|     }
 | |
|     if (data?.ack === 'READY_ACK') {
 | |
|       if (this._connectedToUID !== data.uid)
 | |
|         this._bufferedResponses.clear();
 | |
|       this._connectedToUID = data.uid;
 | |
|       this._markAsReady();
 | |
|       return;
 | |
|     }
 | |
|     if (data?.ack === 'RESPONSE_ACK') {
 | |
|       this._bufferedResponses.delete(data.responseId);
 | |
|       return;
 | |
|     }
 | |
|     if (this._paused)
 | |
|       this._bufferedIncomingMessages.push(data);
 | |
|     else
 | |
|       this._onMessageInternal(data);
 | |
|   }
 | |
| 
 | |
|   async _onMessageInternal(data) {
 | |
|     if (data.responseId) {
 | |
|       this.transport.sendMessage({ ack: 'RESPONSE_ACK', responseId: data.responseId });
 | |
|       const message = this._pendingMessages.get(data.responseId);
 | |
|       if (!message) {
 | |
|         // During cross-process navigation, we might receive a response for
 | |
|         // the message sent by another process.
 | |
|         return;
 | |
|       }
 | |
|       this._pendingMessages.delete(data.responseId);
 | |
|       if (data.error)
 | |
|         message.reject(new Error(data.error));
 | |
|       else
 | |
|         message.resolve(data.result);
 | |
|     } else if (data.requestId) {
 | |
|       // When the underlying transport gets replaced, some responses might
 | |
|       // not get delivered. As a result, sender will repeat the same request once
 | |
|       // a new transport gets set.
 | |
|       //
 | |
|       // If this request was already processed, we can fulfill it with the cached response
 | |
|       // and fast-return.
 | |
|       if (this._bufferedResponses.has(data.requestId)) {
 | |
|         this.transport.sendMessage(this._bufferedResponses.get(data.requestId));
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       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;
 | |
|       }
 | |
|       let response;
 | |
|       const connectedToUID = this._connectedToUID;
 | |
|       try {
 | |
|         const result = await method.call(handler, ...data.params);
 | |
|         response = {responseId: data.requestId, result};
 | |
|       } catch (error) {
 | |
|         response = {responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`};
 | |
|       }
 | |
|       // The connection might have changed during the ASYNCHRONOUS handler execution.
 | |
|       // We only need to buffer & send response if we are connected to the same
 | |
|       // end.
 | |
|       if (connectedToUID === this._connectedToUID) {
 | |
|         this._bufferedResponses.set(data.requestId, response);
 | |
|         this.transport.sendMessage(response);
 | |
|       }
 | |
|     } else {
 | |
|       dump(`WARNING: unknown message in channel "${this._name}": ${JSON.stringify(data)}\n`);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ['SimpleChannel'];
 | |
| this.SimpleChannel = SimpleChannel;
 | 
