mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
	
	
		
			181 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			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; |