1import assert from 'assert';
2import { EventEmitter } from 'events';
3import WebSocket from 'ws';
4
5let serverInstance: WebSocket.WebSocketServer | null = null;
6
7const eventEmitter = new EventEmitter();
8
9/**
10 * Private command to support DevTools frontend reload.
11 *
12 * The react-devtools maintains state between frontend(webpage) and backend(app).
13 * If we reload the frontend without reloading the app, the react-devtools will stuck on incorrect state.
14 * We introduce this special reload command.
15 * As long as the frontend reload, we will close app's WebSocket connection and tell app to reconnect again.
16 */
17const RELOAD_COMMAND = 'Expo::RELOAD';
18
19/**
20 * Start the react-devtools WebSocket proxy server
21 */
22export async function startReactDevToolsProxyAsync(options?: { port: number }) {
23  if (serverInstance != null) {
24    return;
25  }
26
27  serverInstance = new WebSocket.WebSocketServer({ port: options?.port ?? 8097 });
28
29  serverInstance.on('connection', function connection(ws) {
30    ws.on('message', function message(rawData, isBinary) {
31      assert(!isBinary);
32      const data = rawData.toString();
33
34      if (data === RELOAD_COMMAND) {
35        closeAllOtherClients(ws);
36        eventEmitter.emit(RELOAD_COMMAND);
37        return;
38      }
39
40      serverInstance?.clients.forEach(function each(client) {
41        if (client !== ws && client.readyState === WebSocket.OPEN) {
42          client.send(data, { binary: isBinary });
43        }
44      });
45    });
46  });
47
48  serverInstance.on('close', function () {
49    serverInstance = null;
50  });
51}
52
53/**
54 * Close the WebSocket server
55 */
56export function closeReactDevToolsProxy() {
57  serverInstance?.close();
58  serverInstance = null;
59}
60
61/**
62 * add event listener from react-devtools frontend reload
63 */
64export function addReactDevToolsReloadListener(listener: (...args: any[]) => void) {
65  eventEmitter.addListener(RELOAD_COMMAND, listener);
66}
67
68/**
69 * Close all other WebSocket clients other than the current `self` client
70 */
71function closeAllOtherClients(self: WebSocket.WebSocket) {
72  serverInstance?.clients.forEach(function each(client) {
73    if (client !== self && client.readyState === WebSocket.OPEN) {
74      client.close();
75    }
76  });
77}
78