diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index 481f7b1f8..c065c6a32 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -8,7 +8,12 @@ */ import {SandyPluginDefinition} from './SandyPluginDefinition'; -import {BasePluginInstance, BasePluginClient} from './PluginBase'; +import { + BasePluginInstance, + BasePluginClient, + EventsContract, + MethodsContract, +} from './PluginBase'; import {FlipperLib} from './FlipperLib'; import {Atom, ReadOnlyAtom} from '../state/atom'; import { @@ -46,12 +51,14 @@ export type DevicePluginPredicate = (device: Device) => boolean; export type DevicePluginFactory = (client: DevicePluginClient) => object; -export interface DevicePluginClient extends BasePluginClient { +export interface DevicePluginClient< + ServerAddOnEvents extends EventsContract = {}, + ServerAddOnMethods extends MethodsContract = {}, +> extends BasePluginClient { /** * opens a different plugin by id, optionally providing a deeplink to bring the plugin to a certain state */ selectPlugin(pluginId: string, deeplinkPayload?: unknown): void; - readonly isConnected: boolean; readonly connected: ReadOnlyAtom; } @@ -65,14 +72,21 @@ export class SandyDevicePluginInstance extends BasePluginInstance { readonly client: DevicePluginClient; constructor( - private readonly serverAddOnControls: ServerAddOnControls, + serverAddOnControls: ServerAddOnControls, flipperLib: FlipperLib, definition: SandyPluginDefinition, device: Device, pluginKey: string, initialStates?: Record, ) { - super(flipperLib, definition, device, pluginKey, initialStates); + super( + serverAddOnControls, + flipperLib, + definition, + device, + pluginKey, + initialStates, + ); this.client = { ...this.createBasePluginClient(), selectPlugin(pluginId: string, deeplink?: unknown) { @@ -98,31 +112,7 @@ export class SandyDevicePluginInstance extends BasePluginInstance { super.destroy(); } - private startServerAddOn() { - const {serverAddOn, name} = this.definition.details; - if (serverAddOn) { - this.serverAddOnControls.start(name, this.device.serial).catch((e) => { - console.warn( - 'Failed to start a server add on', - name, - this.device.serial, - e, - ); - }); - } - } - - private stopServerAddOn() { - const {serverAddOn, name} = this.definition.details; - if (serverAddOn) { - this.serverAddOnControls.stop(name, this.device.serial).catch((e) => { - console.warn( - 'Failed to start a server add on', - name, - this.device.serial, - e, - ); - }); - } + protected get serverAddOnOwner() { + return this.device.serial; } } diff --git a/desktop/flipper-plugin/src/plugin/Plugin.tsx b/desktop/flipper-plugin/src/plugin/Plugin.tsx index 553af3b0b..817822c08 100644 --- a/desktop/flipper-plugin/src/plugin/Plugin.tsx +++ b/desktop/flipper-plugin/src/plugin/Plugin.tsx @@ -8,15 +8,21 @@ */ import {SandyPluginDefinition} from './SandyPluginDefinition'; -import {BasePluginInstance, BasePluginClient} from './PluginBase'; +import { + BasePluginInstance, + BasePluginClient, + EventsContract, + MethodsContract, +} from './PluginBase'; import {FlipperLib} from './FlipperLib'; import {Device} from './DevicePlugin'; import {batched} from '../state/batch'; import {Atom, createState, ReadOnlyAtom} from '../state/atom'; import {ServerAddOnControls} from 'flipper-common'; -type EventsContract = Record; -type MethodsContract = Record Promise>; +type PreventIntersectionWith> = { + [Key in keyof Contract]?: never; +}; type Message = { method: string; @@ -29,7 +35,11 @@ type Message = { export interface PluginClient< Events extends EventsContract = {}, Methods extends MethodsContract = {}, -> extends BasePluginClient { + ServerAddOnEvents extends EventsContract & + PreventIntersectionWith = {}, + ServerAddOnMethods extends MethodsContract & + PreventIntersectionWith = {}, +> extends BasePluginClient { /** * Identifier that uniquely identifies the connected application */ @@ -125,7 +135,11 @@ export interface RealFlipperClient { export type PluginFactory< Events extends EventsContract, Methods extends MethodsContract, -> = (client: PluginClient) => object; + ServerAddOnEvents extends EventsContract & PreventIntersectionWith, + ServerAddOnMethods extends MethodsContract & PreventIntersectionWith, +> = ( + client: PluginClient, +) => object; export type FlipperPluginComponent = React.FC<{}>; @@ -137,19 +151,26 @@ export class SandyPluginInstance extends BasePluginInstance { /** base client provided by Flipper */ readonly realClient: RealFlipperClient; /** client that is bound to this instance */ - readonly client: PluginClient; + readonly client: PluginClient; /** connection alive? */ readonly connected = createState(false); constructor( - private readonly serverAddOnControls: ServerAddOnControls, + serverAddOnControls: ServerAddOnControls, flipperLib: FlipperLib, definition: SandyPluginDefinition, realClient: RealFlipperClient, pluginKey: string, initialStates?: Record, ) { - super(flipperLib, definition, realClient.device, pluginKey, initialStates); + super( + serverAddOnControls, + flipperLib, + definition, + realClient.device, + pluginKey, + initialStates, + ); this.realClient = realClient; this.definition = definition; const self = this; @@ -231,18 +252,7 @@ export class SandyPluginInstance extends BasePluginInstance { connect() { this.assertNotDestroyed(); if (!this.connected.get()) { - const {serverAddOn, name} = this.definition.details; - if (serverAddOn) { - this.serverAddOnControls.start(name, this.realClient.id).catch((e) => { - console.warn( - 'Failed to start a server add on', - name, - this.realClient.id, - e, - ); - }); - } - + this.startServerAddOn(); this.connected.set(true); this.events.emit('connect'); } @@ -251,18 +261,7 @@ export class SandyPluginInstance extends BasePluginInstance { disconnect() { this.assertNotDestroyed(); if (this.connected.get()) { - const {serverAddOn, name} = this.definition.details; - if (serverAddOn) { - this.serverAddOnControls.stop(name, this.realClient.id).catch((e) => { - console.warn( - 'Failed to stop a server add on', - name, - this.realClient.id, - e, - ); - }); - } - + this.stopServerAddOn(); this.connected.set(false); this.events.emit('disconnect'); } @@ -289,6 +288,10 @@ export class SandyPluginInstance extends BasePluginInstance { return '[SandyPluginInstance]'; } + protected get serverAddOnOwner() { + return this.realClient.id; + } + private assertConnected() { this.assertNotDestroyed(); if ( diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index eade7435a..e8b7babe3 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -18,6 +18,10 @@ import {Idler} from '../utils/Idler'; import {Notification} from './Notification'; import {Logger} from '../utils/Logger'; import {CreatePasteArgs, CreatePasteResult} from './Paste'; +import {ServerAddOnControls} from 'flipper-common'; + +export type EventsContract = Record; +export type MethodsContract = Record Promise>; type StateExportHandler = ( idler: Idler, @@ -25,7 +29,10 @@ type StateExportHandler = ( ) => Promise; type StateImportHandler = (data: T) => void; -export interface BasePluginClient { +export interface BasePluginClient< + ServerAddOnEvents extends EventsContract = {}, + ServerAddOnMethods extends MethodsContract = {}, +> { /** * A key that uniquely identifies this plugin instance, captures the current device/client/plugin combination. */ @@ -128,6 +135,36 @@ export interface BasePluginClient { * Logger instance that logs information to the console, but also to the internal logging (in FB only builds) and which can be used to track performance. */ logger: Logger; + + /** + * Subscribe to a specific event arriving from the server add-on. + * + * Messages can only arrive if the plugin is enabled and connected. + * For background plugins messages will be batched and arrive the next time the plugin is connected. + */ + onServerAddOnMessage( + event: Event, + callback: (params: ServerAddOnEvents[Event]) => void, + ): void; + + /** + * Subscribe to all messages arriving from the server add-ons not handled by another listener. + * + * This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront. + */ + onServerAddOnUnhandledMessage( + callback: (event: string, params: any) => void, + ): void; + + /** + * Send a message to the server add-on + */ + sendToServerAddOn( + method: Method, + ...params: Parameters extends [] + ? [] + : [Parameters[0]] + ): ReturnType; } let currentPluginInstance: BasePluginInstance | undefined = undefined; @@ -200,6 +237,7 @@ export abstract class BasePluginInstance { readonly instanceId = ++staticInstanceId; constructor( + private readonly serverAddOnControls: ServerAddOnControls, flipperLib: FlipperLib, definition: SandyPluginDefinition, device: Device, @@ -269,7 +307,7 @@ export abstract class BasePluginInstance { } } - protected createBasePluginClient(): BasePluginClient { + protected createBasePluginClient(): BasePluginClient { return { pluginKey: this.pluginKey, device: this.device, @@ -341,6 +379,15 @@ export abstract class BasePluginInstance { this.flipperLib.showNotification(this.pluginKey, notification); }, logger: this.flipperLib.logger, + sendToServerAddOn: (_method, _params): any => { + // TODO: Implement me + }, + onServerAddOnMessage: (_event, _cb) => { + // TODO: Implement me + }, + onServerAddOnUnhandledMessage: (_cb) => { + // TODO: Implement me + }, }; } @@ -447,4 +494,34 @@ export abstract class BasePluginInstance { } abstract toJSON(): string; + + protected abstract serverAddOnOwner: string; + + protected startServerAddOn() { + const {serverAddOn, name} = this.definition.details; + if (serverAddOn) { + this.serverAddOnControls.start(name, this.serverAddOnOwner).catch((e) => { + console.warn( + 'Failed to start a server add on', + name, + this.serverAddOnOwner, + e, + ); + }); + } + } + + protected stopServerAddOn() { + const {serverAddOn, name} = this.definition.details; + if (serverAddOn) { + this.serverAddOnControls.stop(name, this.serverAddOnOwner).catch((e) => { + console.warn( + 'Failed to start a server add on', + name, + this.serverAddOnOwner, + e, + ); + }); + } + } } diff --git a/desktop/flipper-plugin/src/plugin/PluginContext.tsx b/desktop/flipper-plugin/src/plugin/PluginContext.tsx index 5aa3ddd50..7850cf9f7 100644 --- a/desktop/flipper-plugin/src/plugin/PluginContext.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginContext.tsx @@ -33,7 +33,7 @@ export function usePluginInstanceMaybe(): } export function usePlugin< - Factory extends PluginFactory | DevicePluginFactory, + Factory extends PluginFactory | DevicePluginFactory, >(plugin: Factory): ReturnType { const pluginInstance = usePluginInstance(); // In principle we don't *need* the plugin, but having it passed it makes sure the diff --git a/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx b/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx index d13432681..610103886 100644 --- a/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx +++ b/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx @@ -26,7 +26,9 @@ export type FlipperDevicePluginModule = { /** * FlipperPluginModule describe the exports that are provided by a typical Flipper Desktop plugin */ -export type FlipperPluginModule> = { +export type FlipperPluginModule< + Factory extends PluginFactory, +> = { /** the factory function that initializes a plugin instance */ plugin: Factory; /** the component type that can render this plugin */ diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 9296afa80..1ed28766f 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -505,7 +505,7 @@ export function createMockPluginDetails( }; } -export function createTestPlugin>( +export function createTestPlugin>( implementation: Pick, 'plugin'> & Partial>, details?: Partial,