UNPKG

expo

Version:
201 lines 7.4 kB
import { EventEmitter } from 'fbemitter'; import { MessageFramePacker } from './MessageFramePacker'; import { WebSocketBackingStore } from './WebSocketBackingStore'; import { WebSocketWithReconnect } from './WebSocketWithReconnect'; import { blobToArrayBufferAsync } from './blobUtils'; import * as logger from './logger'; // This version should be synced with the one in the **createMessageSocketEndpoint.ts** in @react-native-community/cli-server-api export const MESSAGE_PROTOCOL_VERSION = 2; export const DevToolsPluginMethod = 'Expo:DevToolsPlugin'; /** * This client is for the Expo DevTools Plugins to communicate between the app and the DevTools webpage hosted in a browser. * All the code should be both compatible with browsers and React Native. */ export class DevToolsPluginClient { connectionInfo; options; eventEmitter = new EventEmitter(); static defaultWSStore = new WebSocketBackingStore(); wsStore = DevToolsPluginClient.defaultWSStore; isClosed = false; retries = 0; useTransportationNext; messageFramePacker; constructor(connectionInfo, options) { this.connectionInfo = connectionInfo; this.options = options; this.wsStore = connectionInfo.wsStore || DevToolsPluginClient.defaultWSStore; this.useTransportationNext = options?.useTransportationNext ?? false; this.messageFramePacker = this.useTransportationNext ? new MessageFramePacker() : null; } /** * Initialize the connection. * @hidden */ async initAsync() { if (this.wsStore.ws == null) { this.wsStore.ws = await this.connectAsync(); } this.wsStore.refCount += 1; this.wsStore.ws.addEventListener('message', this.handleMessage); } /** * Close the connection. */ async closeAsync() { this.isClosed = true; this.wsStore.ws?.removeEventListener('message', this.handleMessage); this.wsStore.refCount -= 1; if (this.wsStore.refCount < 1) { this.wsStore.ws?.close(); this.wsStore.ws = null; } this.eventEmitter.removeAllListeners(); } /** * Send a message to the other end of DevTools. * @param method A method name. * @param params any extra payload. */ sendMessage(method, params) { if (this.wsStore.ws?.readyState === WebSocket.CLOSED) { logger.warn('Unable to send message in a disconnected state.'); return; } if (this.useTransportationNext) { this.sendMessageImplTransportationNext(method, params); } else { this.sendMessageImplLegacy(method, params); } } sendMessageImplLegacy(method, params) { const payload = { version: MESSAGE_PROTOCOL_VERSION, pluginName: this.connectionInfo.pluginName, method: DevToolsPluginMethod, params: { method, params, }, }; this.wsStore.ws?.send(JSON.stringify(payload)); } async sendMessageImplTransportationNext(method, params) { if (this.messageFramePacker == null) { logger.warn('MessageFramePacker is not initialized'); return; } const messageKey = { pluginName: this.connectionInfo.pluginName, method, }; const packedData = await this.messageFramePacker.pack({ messageKey, payload: params }); this.wsStore.ws?.send(packedData); } /** * Subscribe to a message from the other end of DevTools. * @param method Subscribe to a message with a method name. * @param listener Listener to be called when a message is received. */ addMessageListener(method, listener) { return this.eventEmitter.addListener(method, listener); } /** * Subscribe to a message from the other end of DevTools just once. * @param method Subscribe to a message with a method name. * @param listener Listener to be called when a message is received. */ addMessageListenerOnce(method, listener) { this.eventEmitter.once(method, listener); } /** * Returns whether the client is connected to the server. */ isConnected() { return this.wsStore.ws?.readyState === WebSocket.OPEN; } /** * The method to create the WebSocket connection. */ connectAsync() { return new Promise((resolve, reject) => { const endpoint = this.useTransportationNext ? 'expo-dev-plugins/broadcast' : 'message'; const ws = new WebSocketWithReconnect(`ws://${this.connectionInfo.devServer}/${endpoint}`, { binaryType: this.options?.websocketBinaryType, onError: (e) => { if (e instanceof Error) { console.warn(`Error happened from the WebSocket connection: ${e.message}\n${e.stack}`); } else { console.warn(`Error happened from the WebSocket connection: ${JSON.stringify(e)}`); } }, }); ws.addEventListener('open', () => { resolve(ws); }); ws.addEventListener('error', (e) => { reject(e); }); ws.addEventListener('close', (e) => { logger.info('WebSocket closed', e.code, e.reason); }); }); } handleMessage = (event) => { if (this.useTransportationNext) { this.handleMessageImplTransportationNext(event); } else { this.handleMessageImplLegacy(event); } }; handleMessageImplLegacy = (event) => { let payload; try { payload = JSON.parse(event.data); } catch (e) { logger.info('Failed to parse JSON', e); return; } if (payload.version !== MESSAGE_PROTOCOL_VERSION || payload.method !== DevToolsPluginMethod) { return; } if (payload.pluginName && payload.pluginName !== this.connectionInfo.pluginName) { return; } this.eventEmitter.emit(payload.params.method, payload.params.params); }; handleMessageImplTransportationNext = async (event) => { if (this.messageFramePacker == null) { logger.warn('MessageFramePacker is not initialized'); return; } let buffer; if (event.data instanceof ArrayBuffer) { buffer = event.data; } else if (event.data instanceof Blob) { buffer = await blobToArrayBufferAsync(event.data); } else { logger.warn('Unsupported received data type in handleMessageImplTransportationNext'); return; } const { messageKey, payload } = await this.messageFramePacker.unpack(buffer); if (messageKey.pluginName && messageKey.pluginName !== this.connectionInfo.pluginName) { return; } this.eventEmitter.emit(messageKey.method, payload); }; /** * Get the WebSocket backing store. Exposed for testing. * @hidden */ getWebSocketBackingStore() { return this.wsStore; } } //# sourceMappingURL=DevToolsPluginClient.js.map