diff --git a/desktop/flipper-plugin-core/src/plugin/Plugin.tsx b/desktop/flipper-plugin-core/src/plugin/Plugin.tsx index 6a96dee5e..5061b2533 100644 --- a/desktop/flipper-plugin-core/src/plugin/Plugin.tsx +++ b/desktop/flipper-plugin-core/src/plugin/Plugin.tsx @@ -59,34 +59,44 @@ export interface PluginClient< * the onConnect event is fired whenever the plugin is connected to it's counter part on the device. * For most plugins this event is fired if the user selects the plugin, * for background plugins when the initial connection is made. + * + * @returns an unsubscribe callback */ - onConnect(cb: () => void): void; + onConnect(cb: () => void): () => void; /** * The counterpart of the `onConnect` handler. * Will also be fired before the plugin is cleaned up if the connection is currently active: * - when the client disconnects * - when the plugin is disabled + * + * @returns an unsubscribe callback */ - onDisconnect(cb: () => void): void; + onDisconnect(cb: () => void): () => void; /** * Subscribe to a specific event arriving from the device. * * 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. + * + * @returns an unsubscribe callback */ onMessage( event: Event, callback: (params: Events[Event]) => void, - ): void; + ): () => void; /** * Subscribe to all messages arriving from the devices not handled by another listener. * * This handler is untyped, and onMessage should be favored over using onUnhandledMessage if the event name is known upfront. + * + * @returns an unsubscribe callback */ - onUnhandledMessage(callback: (event: string, params: any) => void): void; + onUnhandledMessage( + callback: (event: string, params: any) => void, + ): () => void; /** * Send a message to the connected client @@ -192,10 +202,18 @@ export class SandyPluginInstance extends BasePluginInstance { return self.connected.get(); }, onConnect: (cb) => { - this.events.on('connect', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('connect', cbWrapped); + return () => { + this.events.off('connect', cbWrapped); + }; }, onDisconnect: (cb) => { - this.events.on('disconnect', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('disconnect', cbWrapped); + return () => { + this.events.off('disconnect', cbWrapped); + }; }, send: async (method, params) => { this.assertConnected(); @@ -207,10 +225,19 @@ export class SandyPluginInstance extends BasePluginInstance { ); }, onMessage: (event, cb) => { - this.events.on(`event-${event.toString()}`, batched(cb)); + const cbWrapped = batched(cb); + const eventName = `event-${event.toString()}`; + this.events.on(eventName, cbWrapped); + return () => { + this.events.off(eventName, cbWrapped); + }; }, onUnhandledMessage: (cb) => { - this.events.on('unhandled-event', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('unhandled-event', cbWrapped); + return () => { + this.events.off('unhandled-event', cbWrapped); + }; }, supportsMethod: async (method) => { return await realClient.supportsMethod( diff --git a/desktop/flipper-plugin-core/src/plugin/PluginBase.tsx b/desktop/flipper-plugin-core/src/plugin/PluginBase.tsx index 6c277b35b..1ef650975 100644 --- a/desktop/flipper-plugin-core/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin-core/src/plugin/PluginBase.tsx @@ -46,18 +46,24 @@ export interface BasePluginClient< /** * the onActivate event is fired whenever the plugin is actived in the UI + * + * @returns an unsubscribe callback */ - onActivate(cb: () => void): void; + onActivate(cb: () => void): () => void; /** * The counterpart of the `onActivate` handler. + * + * @returns an unsubscribe callback */ - onDeactivate(cb: () => void): void; + onDeactivate(cb: () => void): () => void; /** * Triggered when this plugin is opened through a deeplink + * + * @returns an unsubscribe callback */ - onDeepLink(cb: (deepLink: unknown) => void): void; + onDeepLink(cb: (deepLink: unknown) => void): () => void; /** * Triggered when the current plugin is being exported and should create a snapshot of the state exported. @@ -78,8 +84,10 @@ export interface BasePluginClient< * The `onReady` event is triggered immediately after a plugin has been initialized and any pending state was restored. * This event fires after `onImport` / the interpretation of any `persist` flags and indicates that the initialization process has finished. * This event does not signal that the plugin is loaded in the UI yet (see `onActivated`) and does fire before deeplinks (see `onDeepLink`) are handled. + * + * @returns an unsubscribe callback */ - onReady(handler: () => void): void; + onReady(handler: () => void): () => void; /** * Register menu entries in the Flipper toolbar @@ -141,14 +149,18 @@ export interface BasePluginClient< * You should send messages to the server add-on only after it connects. * Do not forget to stop all communication when the add-on stops. * See `onServerAddStop`. + * + * @returns an unsubscribe callback */ - onServerAddOnStart(callback: () => void): void; + onServerAddOnStart(callback: () => void): () => void; /** * Triggered when a server add-on stops. * You should stop all communication with the server add-on when the add-on stops. + * + * @returns an unsubscribe callback */ - onServerAddOnStop(callback: () => void): void; + onServerAddOnStop(callback: () => void): () => void; /** * Subscribe to a specific event arriving from the server add-on. @@ -333,13 +345,25 @@ export abstract class BasePluginInstance { pluginKey: this.pluginKey, device: this.device, onActivate: (cb) => { - this.events.on('activate', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('activate', cbWrapped); + return () => { + this.events.off('activate', cbWrapped); + }; }, onDeactivate: (cb) => { + const cbWrapped = batched(cb); this.events.on('deactivate', batched(cb)); + return () => { + this.events.off('deactivate', cbWrapped); + }; }, onDeepLink: (cb) => { - this.events.on('deeplink', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('deeplink', cbWrapped); + return () => { + this.events.off('deeplink', cbWrapped); + }; }, onDestroy: (cb) => { this.events.on('destroy', batched(cb)); @@ -357,7 +381,11 @@ export abstract class BasePluginInstance { this.importHandler = cb; }, onReady: (cb) => { - this.events.on('ready', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('ready', cbWrapped); + return () => { + this.events.off('ready', cbWrapped); + }; }, addMenuEntry: (...entries) => { for (const entry of entries) { @@ -401,16 +429,24 @@ export abstract class BasePluginInstance { }, logger: this.flipperLib.logger, onServerAddOnStart: (cb) => { - this.events.on('serverAddOnStart', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('serverAddOnStart', cbWrapped); if (this.serverAddOnStarted) { - batched(cb)(); + cbWrapped(); } + return () => { + this.events.off('serverAddOnStart', cbWrapped); + }; }, onServerAddOnStop: (cb) => { - this.events.on('serverAddOnStop', batched(cb)); + const cbWrapped = batched(cb); + this.events.on('serverAddOnStop', cbWrapped); if (this.serverAddOnStopped) { - batched(cb)(); + cbWrapped(); } + return () => { + this.events.off('serverAddOnStop', cbWrapped); + }; }, sendToServerAddOn: (method, params) => this.serverAddOnControls.sendMessage(