Files
flipper/desktop/flipper-ui-core/src/Client.tsx
Andrey Goncharov ef5fa275a3 Use AbstractClient from flipper-frontend-core in fliper-ui-core
Summary: This stack attempts to start using flipper-frontend-core from flipper-ui-core. Currently, flipper-frontend-core contains lots of copy-pasted code from flipper-ui-core.

Reviewed By: lblasa

Differential Revision: D37139198

fbshipit-source-id: 042db7492c550e10ea72c32fd15001c141bf53f9
2022-06-20 12:18:40 -07:00

314 lines
8.6 KiB
TypeScript

/**
* 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
*/
// We're using `deviceSync` here on purpose which is triggering a lot of warnings.
/* eslint-disable node/no-sync */
import {
Logger,
FlipperServer,
ClientQuery,
ClientErrorType,
} from 'flipper-common';
import {Store} from './reducers/index';
import {NoLongerConnectedToClientError} from 'flipper-common';
import {defaultEnabledBackgroundPlugins} from './utils/pluginUtils';
import {processMessagesLater} from './utils/messageQueue';
import {emitBytesReceived} from './dispatcher/tracking';
import {debounce} from 'lodash';
import {batch} from 'react-redux';
import {_SandyPluginInstance, _SandyPluginDefinition} from 'flipper-plugin';
import {message} from 'antd';
import {
isFlipperMessageDebuggingEnabled,
registerFlipperDebugMessage,
} from './chrome/FlipperMessages';
import {waitFor} from './utils/waitFor';
import {
AbstractClient,
Params,
ClientConnection,
BaseDevice,
RequestMetadata,
getPluginKey,
} from 'flipper-frontend-core';
export type ClientExport = {
id: string;
query: ClientQuery;
};
const handleError = (
store: Store,
device: BaseDevice,
error: ClientErrorType,
) => {
if (store.getState().settingsState.suppressPluginErrors) {
return;
}
const crashReporterPlugin = device.sandyPluginStates.get('CrashReporter');
if (!crashReporterPlugin) {
return;
}
if (!crashReporterPlugin.instanceApi.reportCrash) {
console.error('CrashReporterPlugin persistedStateReducer broken');
return;
}
const isCrashReport: boolean = Boolean(error.name || error.message);
const payload = isCrashReport
? {
name: error.name,
reason: error.message,
callstack: error.stacktrace,
}
: {
name: 'Plugin Error',
reason: JSON.stringify(error),
};
crashReporterPlugin.instanceApi.reportCrash(payload);
};
export default class Client extends AbstractClient {
store: Store;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
messageBuffer: Record<
string /*pluginKey*/,
{
plugin: _SandyPluginInstance;
messages: Params[];
}
> = {};
constructor(
id: string,
query: ClientQuery,
conn: ClientConnection | null | undefined,
logger: Logger,
store: Store,
plugins: Set<string> | null | undefined,
device: BaseDevice,
flipperServer: FlipperServer,
) {
super(id, query, conn, logger, plugins, device, flipperServer);
this.store = store;
this.broadcastCallbacks = new Map();
this.on('flipper-debug-message', (message) => {
if (isFlipperMessageDebuggingEnabled()) {
registerFlipperDebugMessage(message);
}
});
this.on('bytes-received', (api, bytes) => emitBytesReceived(api, bytes));
this.on('error', (error) => handleError(this.store, this.device, error));
}
supportsPlugin(pluginId: string): boolean {
return this.plugins.has(pluginId);
}
isEnabledPlugin(pluginId: string) {
return this.store
.getState()
.connections.enabledPlugins[this.query.app]?.includes(pluginId);
}
shouldConnectAsBackgroundPlugin(pluginId: string) {
return (
defaultEnabledBackgroundPlugins.includes(pluginId) ||
this.isEnabledPlugin(pluginId)
);
}
async initFromImport(
initialStates: Record<string, Record<string, any>>,
): Promise<this> {
await Promise.all(
[...this.plugins].map(async (pluginId) => {
const plugin = await this.getPlugin(pluginId);
if (plugin) {
this.loadPlugin(plugin, initialStates[pluginId]);
}
}),
);
this.emit('plugins-change');
return this;
}
// get the supported plugins
async loadPlugins(phase: 'init' | 'refresh'): Promise<Set<string>> {
const plugins = await super.loadPlugins(phase);
if (phase === 'init') {
// if a client arrives before all plugins are loaded, we'll have to wait
await waitFor(this.store, (state) => state.plugins.initialized);
}
return plugins;
}
startPluginIfNeeded(
plugin: _SandyPluginDefinition | undefined,
isEnabled = plugin ? this.isEnabledPlugin(plugin.id) : false,
) {
// start a plugin on start if it is a SandyPlugin, which is enabled, and doesn't have persisted state yet
if (
plugin &&
(isEnabled || defaultEnabledBackgroundPlugins.includes(plugin.id))
) {
super.startPluginIfNeeded(plugin);
}
}
stopPluginIfNeeded(pluginId: string, force = false) {
if (defaultEnabledBackgroundPlugins.includes(pluginId) && !force) {
return;
}
const pluginKey = getPluginKey(
this.id,
{serial: this.query.device_id},
pluginId,
);
delete this.messageBuffer[pluginKey];
return super.stopPluginIfNeeded(pluginId, force);
}
// gets a plugin by pluginId
protected async getPlugin(
pluginId: string,
): Promise<_SandyPluginDefinition | undefined> {
const plugins = this.store.getState().plugins;
return (
plugins.clientPlugins.get(pluginId) || plugins.devicePlugins.get(pluginId)
);
}
onMessage(msg: string) {
batch(() => {
super.onMessage(msg);
});
}
protected handleExecuteMessage(params: Params): boolean {
const persistingPlugin: _SandyPluginDefinition | undefined =
this.store.getState().plugins.clientPlugins.get(params.api) ||
this.store.getState().plugins.devicePlugins.get(params.api);
let handled = false; // This is just for analysis
if (
persistingPlugin &&
((persistingPlugin as any).persistedStateReducer ||
// only send messages to enabled sandy plugins
this.sandyPluginStates.has(params.api))
) {
handled = true;
const pluginKey = getPluginKey(
this.id,
{serial: this.query.device_id},
params.api,
);
if (!this.messageBuffer[pluginKey]) {
this.messageBuffer[pluginKey] = {
plugin: (this.sandyPluginStates.get(params.api) ??
persistingPlugin) as any,
messages: [params],
};
} else {
this.messageBuffer[pluginKey].messages.push(params);
}
this.flushMessageBufferDebounced();
}
const apiCallbacks = this.broadcastCallbacks.get(params.api);
if (apiCallbacks) {
const methodCallbacks = apiCallbacks.get(params.method);
if (methodCallbacks) {
for (const callback of methodCallbacks) {
handled = true;
callback(params.params);
}
}
}
return handled;
}
toJSON(): ClientExport {
return {id: this.id, query: this.query};
}
subscribe(api: string, method: string, callback: (params: Object) => void) {
let apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
apiCallbacks = new Map();
this.broadcastCallbacks.set(api, apiCallbacks);
}
let methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
methodCallbacks = new Set();
apiCallbacks.set(method, methodCallbacks);
}
methodCallbacks.add(callback);
}
unsubscribe(api: string, method: string, callback: Function) {
const apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
return;
}
const methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
return;
}
methodCallbacks.delete(callback);
}
rawCall<T>(method: string, fromPlugin: boolean, params?: Params): Promise<T> {
return super.rawCall<T>(method, fromPlugin, params).catch((error) => {
if (error instanceof NoLongerConnectedToClientError) {
message.warn({
content: 'Not connected',
key: 'appnotconnectedwarning',
duration: 0.5,
});
}
throw error;
});
}
flushMessageBuffer = () => {
// batch to make sure that Redux collapsed the dispatches
batch(() => {
for (const pluginKey in this.messageBuffer) {
processMessagesLater(
this.store,
pluginKey,
this.messageBuffer[pluginKey].plugin,
this.messageBuffer[pluginKey].messages,
);
}
this.messageBuffer = {};
});
};
flushMessageBufferDebounced = debounce(this.flushMessageBuffer, 200, {
leading: true,
trailing: true,
});
startTimingRequestResponse(data: RequestMetadata) {
performance.mark(this.getPerformanceMark(data));
}
finishTimingRequestResponse(data: RequestMetadata) {
const mark = this.getPerformanceMark(data);
const logEventName = this.getLogEventName(data);
this.logger.trackTimeSince(mark, logEventName);
}
}