desktop/flipper-ui-browser/src/flipperServerConnection.tsx (128 lines of code) (raw):

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import EventEmitter from 'eventemitter3'; import { ExecWebSocketMessage, FlipperServer, ServerWebSocketMessage, } from 'flipper-common'; import ReconnectingWebSocket from 'reconnecting-websocket'; const CONNECTION_TIMEOUT = 30 * 1000; const EXEC_TIMOUT = 30 * 1000; export function createFlipperServer(): Promise<FlipperServer> { // TODO: polish this all! window.flipperShowError?.('Connecting to server...'); return new Promise<FlipperServer>((resolve, reject) => { let initialConnectionTimeout: number | undefined = setTimeout(() => { reject( new Error('Failed to connect to Flipper server in a timely manner'), ); }, CONNECTION_TIMEOUT); const eventEmitter = new EventEmitter(); // TODO: recycle the socket that is created in index.web.dev.html? const socket = new ReconnectingWebSocket(`ws://${location.host}`); const pendingRequests: Map< number, { resolve: (data: any) => void; reject: (data: any) => void; timeout: ReturnType<typeof setTimeout>; } > = new Map(); let requestId = 0; let connected = false; socket.addEventListener('open', () => { if (initialConnectionTimeout) { // only relevant for the first connect resolve(flipperServer); clearTimeout(initialConnectionTimeout); initialConnectionTimeout = undefined; } window?.flipperHideError?.(); console.log('Socket to Flipper server connected'); connected = true; }); socket.addEventListener('close', () => { window?.flipperShowError?.('WebSocket connection lost'); console.warn('Socket to Flipper server disconnected'); connected = false; pendingRequests.forEach((r) => r.reject(new Error('FLIPPER_SERVER_SOCKET_CONNECT_LOST')), ); pendingRequests.clear(); }); socket.addEventListener('message', ({data}) => { const {event, payload} = JSON.parse( data.toString(), ) as ServerWebSocketMessage; switch (event) { case 'exec-response': { console.debug('exec <<<', payload); const entry = pendingRequests.get(payload.id); if (!entry) { console.warn(`Unknown request id `, payload.id); } else { pendingRequests.delete(payload.id); clearTimeout(entry.timeout); entry.resolve(payload.data); } break; } case 'exec-response-error': { // TODO: Deserialize error console.debug('exec <<< [SERVER ERROR]', payload.id, payload.data); const entry = pendingRequests.get(payload.id); if (!entry) { console.warn(`Unknown request id `, payload.id); } else { pendingRequests.delete(payload.id); clearTimeout(entry.timeout); entry.reject(payload.data); } break; } case 'server-event': { eventEmitter.emit(payload.event, payload.data); break; } default: { console.warn( 'createFlipperServer -> unknown message', data.toString(), ); } } }); const flipperServer: FlipperServer = { async connect() {}, close() {}, exec(command, ...args): any { if (connected) { const id = ++requestId; return new Promise<any>((resolve, reject) => { console.debug('exec >>>', id, command, args); pendingRequests.set(id, { resolve, reject, timeout: setInterval(() => { pendingRequests.delete(id); reject(new Error(`Timeout for command '${command}'`)); }, EXEC_TIMOUT), }); const execMessage = { event: 'exec', payload: { id, command, args, }, } as ExecWebSocketMessage; socket.send(JSON.stringify(execMessage)); }); // socket. } else { throw new Error('Not connected to Flipper Server'); } }, on(event, callback) { eventEmitter.on(event, callback); }, off(event, callback) { eventEmitter.off(event, callback); }, }; }); }