ssap_app/node_modules/@expo/devtools/build/DevToolsPluginClient.js

212 lines
7.4 KiB
JavaScript

import { MessageFramePacker } from './MessageFramePacker';
import { WebSocketBackingStore } from './WebSocketBackingStore';
import { WebSocketWithReconnect, } from './WebSocketWithReconnect';
import * as logger from './logger';
import { blobToArrayBufferAsync } from './utils/blobUtils';
/**
* 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;
listeners;
static defaultWSStore = new WebSocketBackingStore();
wsStore = DevToolsPluginClient.defaultWSStore;
isClosed = false;
retries = 0;
messageFramePacker = new MessageFramePacker();
constructor(connectionInfo, options) {
this.connectionInfo = connectionInfo;
this.options = options;
this.wsStore = connectionInfo.wsStore || DevToolsPluginClient.defaultWSStore;
this.listeners = Object.create(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.listeners = Object.create(null);
}
/**
* 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;
}
const messageKey = {
pluginName: this.connectionInfo.pluginName,
method,
};
const packedData = this.messageFramePacker.pack({ messageKey, payload: params });
if (!(packedData instanceof Promise)) {
this.wsStore.ws?.send(packedData);
return;
}
packedData.then((data) => {
this.wsStore.ws?.send(data);
});
}
/**
* 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) {
const listenersForMethod = this.listeners[method] || (this.listeners[method] = new Set());
listenersForMethod.add(listener);
return {
remove: () => {
this.listeners[method]?.delete(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) {
const wrappedListenerOnce = (params) => {
listener(params);
this.listeners[method]?.delete(wrappedListenerOnce);
};
this.addMessageListener(method, wrappedListenerOnce);
}
/**
* Internal handshake message sender.
* @hidden
*/
sendHandshakeMessage(params) {
if (this.wsStore.ws?.readyState === WebSocket.CLOSED) {
logger.warn('Unable to send message in a disconnected state.');
return;
}
this.wsStore.ws?.send(JSON.stringify({ ...params, __isHandshakeMessages: true }));
}
/**
* Internal handshake message listener.
* @hidden
*/
addHandskakeMessageListener(listener) {
const messageListener = (event) => {
if (typeof event.data !== 'string') {
// binary data is not coming from the handshake messages.
return;
}
const data = JSON.parse(event.data);
if (!data.__isHandshakeMessages) {
return;
}
delete data.__isHandshakeMessages;
const params = data;
if (params.pluginName && params.pluginName !== this.connectionInfo.pluginName) {
return;
}
listener(params);
};
this.wsStore.ws?.addEventListener('message', messageListener);
return {
remove: () => {
this.wsStore.ws?.removeEventListener('message', messageListener);
},
};
}
/**
* 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 = 'expo-dev-plugins/broadcast';
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 = async (event) => {
let data;
if (typeof event.data === 'string') {
data = event.data;
}
else if (event.data instanceof ArrayBuffer) {
data = event.data;
}
else if (ArrayBuffer.isView(event.data)) {
data = event.data.buffer;
}
else if (event.data instanceof Blob) {
data = await blobToArrayBufferAsync(event.data);
}
else {
logger.warn('Unsupported received data type in handleMessageImpl');
return;
}
const { messageKey, payload, ...rest } = this.messageFramePacker.unpack(data);
// @ts-expect-error: `__isHandshakeMessages` is a private field that is not part of the MessageFramePacker type.
if (rest?.__isHandshakeMessages === true) {
return;
}
if (messageKey.pluginName && messageKey.pluginName !== this.connectionInfo.pluginName) {
return;
}
const listenersForMethod = this.listeners[messageKey.method];
if (listenersForMethod) {
for (const listener of listenersForMethod) {
listener(payload);
}
}
};
/**
* Get the WebSocket backing store. Exposed for testing.
* @hidden
*/
getWebSocketBackingStore() {
return this.wsStore;
}
}
//# sourceMappingURL=DevToolsPluginClient.js.map