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
314 lines
8.6 KiB
TypeScript
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);
|
|
}
|
|
}
|