diff --git a/desktop/flipper-frontend-core/package.json b/desktop/flipper-frontend-core/package.json index 35a82cb66..5ce9753a5 100644 --- a/desktop/flipper-frontend-core/package.json +++ b/desktop/flipper-frontend-core/package.json @@ -16,7 +16,8 @@ "immer": "^9.0.12", "js-base64": "^3.7.2", "p-map": "^5.3.0", - "semver": "^7.3.7" + "semver": "^7.3.7", + "reconnecting-websocket": "^4.4.0" }, "devDependencies": { "flipper-test-utils": "0.0.0" diff --git a/desktop/flipper-frontend-core/src/client/FlipperServerClient.tsx b/desktop/flipper-frontend-core/src/client/FlipperServerClient.tsx new file mode 100644 index 000000000..4b1d43f76 --- /dev/null +++ b/desktop/flipper-frontend-core/src/client/FlipperServerClient.tsx @@ -0,0 +1,163 @@ +/** + * 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 enum FlipperServerState { + CONNECTING, + CONNECTED, + DISCONNECTED, +} + +export function createFlipperServer( + onStateChange: (state: FlipperServerState) => void, +): Promise { + onStateChange(FlipperServerState.CONNECTING); + + return new Promise((resolve, reject) => { + let initialConnectionTimeout: number | undefined = window.setTimeout(() => { + reject( + new Error('Failed to connect to flipper-server in a timely manner'), + ); + }, CONNECTION_TIMEOUT); + + const eventEmitter = new EventEmitter(); + + const socket = new ReconnectingWebSocket(`ws://${location.host}`); + const pendingRequests: Map< + number, + { + resolve: (data: any) => void; + reject: (data: any) => void; + timeout: ReturnType; + } + > = new Map(); + let requestId = 0; + let connected = false; + + socket.addEventListener('open', () => { + if (initialConnectionTimeout) { + resolve(flipperServer); + clearTimeout(initialConnectionTimeout); + initialConnectionTimeout = undefined; + } + + onStateChange(FlipperServerState.CONNECTED); + connected = true; + }); + + socket.addEventListener('close', () => { + onStateChange(FlipperServerState.DISCONNECTED); + connected = false; + pendingRequests.forEach((r) => + r.reject(new Error('flipper-server disconnected')), + ); + pendingRequests.clear(); + }); + + socket.addEventListener('message', ({data}) => { + const {event, payload} = JSON.parse( + data.toString(), + ) as ServerWebSocketMessage; + + switch (event) { + case 'exec-response': { + console.debug('flipper-server: 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( + 'flipper-server: exec <<< [SERVER ERROR]', + payload.id, + payload.data, + ); + const entry = pendingRequests.get(payload.id); + if (!entry) { + console.warn(`flipper-server: 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( + 'flipper-server: received unknown message type', + data.toString(), + ); + } + } + }); + + const flipperServer: FlipperServer = { + async connect() {}, + close() {}, + exec(command, ...args): any { + if (connected) { + const id = ++requestId; + return new Promise((resolve, reject) => { + console.debug('flipper-server: exec >>>', id, command, args); + + pendingRequests.set(id, { + resolve, + reject, + timeout: setInterval(() => { + pendingRequests.delete(id); + reject( + new Error(`flipper-server: timeout for command '${command}'`), + ); + }, EXEC_TIMOUT), + }); + + const execMessage = { + event: 'exec', + payload: { + id, + command, + args, + }, + } as ExecWebSocketMessage; + socket.send(JSON.stringify(execMessage)); + }); + } else { + throw new Error('Not connected to Flipper server'); + } + }, + on(event, callback) { + eventEmitter.on(event, callback); + }, + off(event, callback) { + eventEmitter.off(event, callback); + }, + }; + }); +} diff --git a/desktop/flipper-frontend-core/src/index.tsx b/desktop/flipper-frontend-core/src/index.tsx index 27483055e..2beca1acb 100644 --- a/desktop/flipper-frontend-core/src/index.tsx +++ b/desktop/flipper-frontend-core/src/index.tsx @@ -14,3 +14,4 @@ export {default as BaseDevice} from './devices/BaseDevice'; export * from './globalObject'; export * from './plugins'; export * from './flipperLibImplementation'; +export * from './client/FlipperServerClient'; diff --git a/desktop/flipper-ui-browser/package.json b/desktop/flipper-ui-browser/package.json index 47a7c2f58..586684606 100644 --- a/desktop/flipper-ui-browser/package.json +++ b/desktop/flipper-ui-browser/package.json @@ -15,6 +15,7 @@ "devDependencies": { "eventemitter3": "^4.0.7", "flipper-common": "0.0.0", + "flipper-frontend-core": "0.0.0", "flipper-ui-core": "0.0.0", "invariant": "^2.2.4", "metro-runtime": "^0.70.2", diff --git a/desktop/flipper-ui-browser/src/index.tsx b/desktop/flipper-ui-browser/src/index.tsx index 4c1f86c6f..4001f8fff 100644 --- a/desktop/flipper-ui-browser/src/index.tsx +++ b/desktop/flipper-ui-browser/src/index.tsx @@ -9,7 +9,7 @@ import {getLogger, Logger, setLoggerInstance} from 'flipper-common'; import {initializeRenderHost} from './initializeRenderHost'; -import {createFlipperServer} from './flipperServerConnection'; +import {createFlipperServer, FlipperServerState} from 'flipper-frontend-core'; document.getElementById('root')!.innerText = 'flipper-ui-browser started'; @@ -27,7 +27,21 @@ async function start() { const logger = createDelegatedLogger(); setLoggerInstance(logger); - const flipperServer = await createFlipperServer(); + const flipperServer = await createFlipperServer( + (state: FlipperServerState) => { + switch (state) { + case FlipperServerState.CONNECTING: + window.flipperShowError?.('Connecting to flipper-server...'); + break; + case FlipperServerState.CONNECTED: + window?.flipperHideError?.(); + break; + case FlipperServerState.DISCONNECTED: + window?.flipperShowError?.('Lost connection to flipper-server'); + break; + } + }, + ); await flipperServer.connect(); const flipperServerConfig = await flipperServer.exec('get-config'); diff --git a/desktop/flipper-ui-browser/tsconfig.json b/desktop/flipper-ui-browser/tsconfig.json index b067590bd..c0f928036 100644 --- a/desktop/flipper-ui-browser/tsconfig.json +++ b/desktop/flipper-ui-browser/tsconfig.json @@ -10,6 +10,9 @@ { "path": "../flipper-common" }, + { + "path": "../flipper-frontend-core" + }, { "path": "../flipper-ui-core" }