diff --git a/desktop/app/src/__tests__/disconnect.node.tsx b/desktop/app/src/__tests__/disconnect.node.tsx index eac6a7e24..918252342 100644 --- a/desktop/app/src/__tests__/disconnect.node.tsx +++ b/desktop/app/src/__tests__/disconnect.node.tsx @@ -29,6 +29,9 @@ test('Devices can disconnect', async () => { return { counter, destroy, + get isConnected() { + return client.device.isConnected; + }, }; }, supportsDevice() { @@ -42,6 +45,9 @@ test('Devices can disconnect', async () => { const {device} = await createMockFlipperWithPlugin(deviceplugin); device.sandyPluginStates.get(deviceplugin.id)!.instanceApi.counter.set(1); + expect( + device.sandyPluginStates.get(deviceplugin.id)!.instanceApi.isConnected, + ).toBe(true); expect(device.isArchived).toBe(false); @@ -49,6 +55,7 @@ test('Devices can disconnect', async () => { expect(device.isArchived).toBe(true); const instance = device.sandyPluginStates.get(deviceplugin.id)!; + expect(instance.instanceApi.isConnected).toBe(false); expect(instance).toBeTruthy(); expect(instance.instanceApi.counter.get()).toBe(1); // state preserved expect(instance.instanceApi.destroy).toBeCalledTimes(0); @@ -126,6 +133,9 @@ test('clients can disconnect but preserve state', async () => { disconnect, counter, destroy, + get isConnected() { + return client.isConnected; + }, }; }, Component() { @@ -142,6 +152,7 @@ test('clients can disconnect but preserve state', async () => { expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.connect).toBeCalledTimes(1); expect(instance.instanceApi.disconnect).toBeCalledTimes(0); + expect(instance.instanceApi.isConnected).toBe(true); expect(client.connected.get()).toBe(true); client.disconnect(); @@ -150,6 +161,7 @@ test('clients can disconnect but preserve state', async () => { instance = client.sandyPluginStates.get(plugin.id)!; expect(instance).toBeTruthy(); expect(instance.instanceApi.counter.get()).toBe(1); // state preserved + expect(instance.instanceApi.isConnected).toBe(false); expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.connect).toBeCalledTimes(1); expect(instance.instanceApi.disconnect).toBeCalledTimes(1); diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index e2a57ec21..1a4cd23b1 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -36,6 +36,7 @@ export type LogLevel = export interface Device { readonly realDevice: any; // TODO: temporarily, clean up T70688226 readonly isArchived: boolean; + readonly isConnected: boolean; readonly os: string; readonly deviceType: DeviceType; onLogEntry(cb: DeviceLogListener): () => void; @@ -79,7 +80,7 @@ export class SandyDevicePluginInstance extends BasePluginInstance { } /** client that is bound to this instance */ - client: DevicePluginClient; + readonly client: DevicePluginClient; constructor( flipperLib: FlipperLib, diff --git a/desktop/flipper-plugin/src/plugin/Plugin.tsx b/desktop/flipper-plugin/src/plugin/Plugin.tsx index d47ee943a..cd716802f 100644 --- a/desktop/flipper-plugin/src/plugin/Plugin.tsx +++ b/desktop/flipper-plugin/src/plugin/Plugin.tsx @@ -12,6 +12,7 @@ import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {FlipperLib} from './FlipperLib'; import {RealFlipperDevice} from './DevicePlugin'; import {batched} from '../state/batch'; +import {Atom, createState} from '../state/atom'; type EventsContract = Record; type MethodsContract = Record Promise>; @@ -38,6 +39,8 @@ export interface PluginClient< */ readonly appName: string; + readonly isConnected: boolean; + /** * 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, @@ -101,6 +104,7 @@ export interface PluginClient< */ export interface RealFlipperClient { id: string; + connected: Atom; query: { app: string; os: string; @@ -134,11 +138,11 @@ export class SandyPluginInstance extends BasePluginInstance { } /** base client provided by Flipper */ - realClient: RealFlipperClient; + readonly realClient: RealFlipperClient; /** client that is bound to this instance */ - client: PluginClient; + readonly client: PluginClient; /** connection alive? */ - connected = false; + readonly connected = createState(false); constructor( flipperLib: FlipperLib, @@ -149,6 +153,7 @@ export class SandyPluginInstance extends BasePluginInstance { super(flipperLib, definition, realClient.deviceSync, initialStates); this.realClient = realClient; this.definition = definition; + const self = this; this.client = { ...this.createBasePluginClient(), get appId() { @@ -157,6 +162,9 @@ export class SandyPluginInstance extends BasePluginInstance { get appName() { return realClient.query.app; }, + get isConnected() { + return self.connected.get(); + }, onConnect: (cb) => { this.events.on('connect', batched(cb)); }, @@ -212,7 +220,10 @@ export class SandyPluginInstance extends BasePluginInstance { activate() { super.activate(); const pluginId = this.definition.id; - if (!this.connected && !this.realClient.isBackgroundPlugin(pluginId)) { + if ( + !this.connected.get() && + !this.realClient.isBackgroundPlugin(pluginId) + ) { this.realClient.initPlugin(pluginId); // will call connect() if needed } } @@ -221,29 +232,29 @@ export class SandyPluginInstance extends BasePluginInstance { deactivate() { super.deactivate(); const pluginId = this.definition.id; - if (this.connected && !this.realClient.isBackgroundPlugin(pluginId)) { + if (this.connected.get() && !this.realClient.isBackgroundPlugin(pluginId)) { this.realClient.deinitPlugin(pluginId); } } connect() { this.assertNotDestroyed(); - if (!this.connected) { - this.connected = true; + if (!this.connected.get()) { + this.connected.set(true); this.events.emit('connect'); } } disconnect() { this.assertNotDestroyed(); - if (this.connected) { - this.connected = false; + if (this.connected.get()) { + this.connected.set(false); this.events.emit('disconnect'); } } destroy() { - if (this.connected) { + if (this.connected.get()) { this.realClient.deinitPlugin(this.definition.id); } super.destroy(); @@ -265,7 +276,7 @@ export class SandyPluginInstance extends BasePluginInstance { private assertConnected() { this.assertNotDestroyed(); - if (!this.connected) { + if (!this.connected.get()) { throw new Error('Plugin is not connected'); } } diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index 60bc4494c..e0cdc9990 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -90,23 +90,23 @@ export function getCurrentPluginInstance(): typeof currentPluginInstance { export abstract class BasePluginInstance { /** generally available Flipper APIs */ - flipperLib: FlipperLib; + readonly flipperLib: FlipperLib; /** the original plugin definition */ definition: SandyPluginDefinition; /** the plugin instance api as used inside components and such */ instanceApi: any; /** the device owning this plugin */ - device: Device; + readonly device: Device; activated = false; destroyed = false; - events = new EventEmitter(); + readonly events = new EventEmitter(); // temporarily field that is used during deserialization initialStates?: Record; // all the atoms that should be serialized when making an export / import - rootStates: Record> = {}; + readonly rootStates: Record> = {}; // last seen deeplink lastDeeplink?: any; // export handler @@ -135,6 +135,10 @@ export abstract class BasePluginInstance { get isArchived() { return realDevice.isArchived; }, + get isConnected() { + // for now same as isArchived, in the future we might distinguish between archived/imported and disconnected/offline devices + return !realDevice.isArchived; + }, deviceType: realDevice.deviceType, onLogEntry(cb) { diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 2967c3790..885a2f00f 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -38,6 +38,7 @@ import {BasePluginInstance} from '../plugin/PluginBase'; import {FlipperLib} from '../plugin/FlipperLib'; import {stubLogger} from '../utils/Logger'; import {Idler} from '../utils/Idler'; +import {createState} from '../state/atom'; type Renderer = RenderResult; @@ -207,10 +208,13 @@ export function startPlugin>( isBackgroundPlugin(_pluginId: string) { return !!options?.isBackgroundPlugin; }, + connected: createState(false), initPlugin() { + this.connected.set(true); pluginInstance.connect(); }, deinitPlugin() { + this.connected.set(false); pluginInstance.disconnect(); }, call( diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index c8c6c12f9..0d26c5343 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -51,6 +51,13 @@ The name of the application, for example 'Facebook', 'Instagram' or 'Slack'. A string that uniquely identifies the current application, is based on a combination of the application name and device serial on which the application is running. +#### `isConnected` + +Returns whether there is currently an active connection. This is true if: +1. The device is still connected +2. The client is still connected +3. The plugin is currently selected by the user _or_ the plugin is running in the background. + ### Events listeners #### `onMessage` @@ -182,6 +189,9 @@ Usage: `client.send(method: string, params: object): Promise` If the plugin is connected, `send` can be used to invoke a [method](create-plugin#[background-plugins#using-flipperconnection) on the client implementation of the plugin. +Note that if `client.isConnected` returns `false`, calling `client.send` will throw an exception. This is the case if for example the connection with the device or application was lost. +Generally one should guard `client.send` calls with a check to `client.isConnected`. + Example: ```typescript @@ -362,6 +372,10 @@ A `string` that describes whether the device is a physical device or an emulator This `boolean` flag is `true` if the current device is coming from an import Flipper snapshot, and not an actually connected device. +#### isConnected + +This `boolean` flag is `true` if the connection to the device is still alive. + ### Events #### `onLogEntry`