Expose current connection status to Sandy plugins

Summary:
Introduced `isConnected` flag on device and plugin client to reflect whether a connection is still available for the plugins, or that they have been disconnected.

Potentially we could expose the (readonly) `connected` state atom for this as well, or an `onDisconnect` event for device pugins, to create a responsive UI, but there might be no need for that, in which case this suffices.

Reviewed By: nikoant

Differential Revision: D26249346

fbshipit-source-id: b8486713fdf2fcd520488ce54f771bd038fd13f8
This commit is contained in:
Michel Weststrate
2021-02-09 04:12:09 -08:00
committed by Facebook GitHub Bot
parent 7e1bf0f58b
commit bb529411b5
6 changed files with 62 additions and 16 deletions

View File

@@ -29,6 +29,9 @@ test('Devices can disconnect', async () => {
return { return {
counter, counter,
destroy, destroy,
get isConnected() {
return client.device.isConnected;
},
}; };
}, },
supportsDevice() { supportsDevice() {
@@ -42,6 +45,9 @@ test('Devices can disconnect', async () => {
const {device} = await createMockFlipperWithPlugin(deviceplugin); const {device} = await createMockFlipperWithPlugin(deviceplugin);
device.sandyPluginStates.get(deviceplugin.id)!.instanceApi.counter.set(1); device.sandyPluginStates.get(deviceplugin.id)!.instanceApi.counter.set(1);
expect(
device.sandyPluginStates.get(deviceplugin.id)!.instanceApi.isConnected,
).toBe(true);
expect(device.isArchived).toBe(false); expect(device.isArchived).toBe(false);
@@ -49,6 +55,7 @@ test('Devices can disconnect', async () => {
expect(device.isArchived).toBe(true); expect(device.isArchived).toBe(true);
const instance = device.sandyPluginStates.get(deviceplugin.id)!; const instance = device.sandyPluginStates.get(deviceplugin.id)!;
expect(instance.instanceApi.isConnected).toBe(false);
expect(instance).toBeTruthy(); expect(instance).toBeTruthy();
expect(instance.instanceApi.counter.get()).toBe(1); // state preserved expect(instance.instanceApi.counter.get()).toBe(1); // state preserved
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
@@ -126,6 +133,9 @@ test('clients can disconnect but preserve state', async () => {
disconnect, disconnect,
counter, counter,
destroy, destroy,
get isConnected() {
return client.isConnected;
},
}; };
}, },
Component() { Component() {
@@ -142,6 +152,7 @@ test('clients can disconnect but preserve state', async () => {
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
expect(instance.instanceApi.connect).toBeCalledTimes(1); expect(instance.instanceApi.connect).toBeCalledTimes(1);
expect(instance.instanceApi.disconnect).toBeCalledTimes(0); expect(instance.instanceApi.disconnect).toBeCalledTimes(0);
expect(instance.instanceApi.isConnected).toBe(true);
expect(client.connected.get()).toBe(true); expect(client.connected.get()).toBe(true);
client.disconnect(); client.disconnect();
@@ -150,6 +161,7 @@ test('clients can disconnect but preserve state', async () => {
instance = client.sandyPluginStates.get(plugin.id)!; instance = client.sandyPluginStates.get(plugin.id)!;
expect(instance).toBeTruthy(); expect(instance).toBeTruthy();
expect(instance.instanceApi.counter.get()).toBe(1); // state preserved expect(instance.instanceApi.counter.get()).toBe(1); // state preserved
expect(instance.instanceApi.isConnected).toBe(false);
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
expect(instance.instanceApi.connect).toBeCalledTimes(1); expect(instance.instanceApi.connect).toBeCalledTimes(1);
expect(instance.instanceApi.disconnect).toBeCalledTimes(1); expect(instance.instanceApi.disconnect).toBeCalledTimes(1);

View File

@@ -36,6 +36,7 @@ export type LogLevel =
export interface Device { export interface Device {
readonly realDevice: any; // TODO: temporarily, clean up T70688226 readonly realDevice: any; // TODO: temporarily, clean up T70688226
readonly isArchived: boolean; readonly isArchived: boolean;
readonly isConnected: boolean;
readonly os: string; readonly os: string;
readonly deviceType: DeviceType; readonly deviceType: DeviceType;
onLogEntry(cb: DeviceLogListener): () => void; onLogEntry(cb: DeviceLogListener): () => void;
@@ -79,7 +80,7 @@ export class SandyDevicePluginInstance extends BasePluginInstance {
} }
/** client that is bound to this instance */ /** client that is bound to this instance */
client: DevicePluginClient; readonly client: DevicePluginClient;
constructor( constructor(
flipperLib: FlipperLib, flipperLib: FlipperLib,

View File

@@ -12,6 +12,7 @@ import {BasePluginInstance, BasePluginClient} from './PluginBase';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {RealFlipperDevice} from './DevicePlugin'; import {RealFlipperDevice} from './DevicePlugin';
import {batched} from '../state/batch'; import {batched} from '../state/batch';
import {Atom, createState} from '../state/atom';
type EventsContract = Record<string, any>; type EventsContract = Record<string, any>;
type MethodsContract = Record<string, (params: any) => Promise<any>>; type MethodsContract = Record<string, (params: any) => Promise<any>>;
@@ -38,6 +39,8 @@ export interface PluginClient<
*/ */
readonly appName: string; readonly appName: string;
readonly isConnected: boolean;
/** /**
* the onConnect event is fired whenever the plugin is connected to it's counter part on the device. * 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 most plugins this event is fired if the user selects the plugin,
@@ -101,6 +104,7 @@ export interface PluginClient<
*/ */
export interface RealFlipperClient { export interface RealFlipperClient {
id: string; id: string;
connected: Atom<boolean>;
query: { query: {
app: string; app: string;
os: string; os: string;
@@ -134,11 +138,11 @@ export class SandyPluginInstance extends BasePluginInstance {
} }
/** base client provided by Flipper */ /** base client provided by Flipper */
realClient: RealFlipperClient; readonly realClient: RealFlipperClient;
/** client that is bound to this instance */ /** client that is bound to this instance */
client: PluginClient<any, any>; readonly client: PluginClient<any, any>;
/** connection alive? */ /** connection alive? */
connected = false; readonly connected = createState(false);
constructor( constructor(
flipperLib: FlipperLib, flipperLib: FlipperLib,
@@ -149,6 +153,7 @@ export class SandyPluginInstance extends BasePluginInstance {
super(flipperLib, definition, realClient.deviceSync, initialStates); super(flipperLib, definition, realClient.deviceSync, initialStates);
this.realClient = realClient; this.realClient = realClient;
this.definition = definition; this.definition = definition;
const self = this;
this.client = { this.client = {
...this.createBasePluginClient(), ...this.createBasePluginClient(),
get appId() { get appId() {
@@ -157,6 +162,9 @@ export class SandyPluginInstance extends BasePluginInstance {
get appName() { get appName() {
return realClient.query.app; return realClient.query.app;
}, },
get isConnected() {
return self.connected.get();
},
onConnect: (cb) => { onConnect: (cb) => {
this.events.on('connect', batched(cb)); this.events.on('connect', batched(cb));
}, },
@@ -212,7 +220,10 @@ export class SandyPluginInstance extends BasePluginInstance {
activate() { activate() {
super.activate(); super.activate();
const pluginId = this.definition.id; 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 this.realClient.initPlugin(pluginId); // will call connect() if needed
} }
} }
@@ -221,29 +232,29 @@ export class SandyPluginInstance extends BasePluginInstance {
deactivate() { deactivate() {
super.deactivate(); super.deactivate();
const pluginId = this.definition.id; const pluginId = this.definition.id;
if (this.connected && !this.realClient.isBackgroundPlugin(pluginId)) { if (this.connected.get() && !this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.deinitPlugin(pluginId); this.realClient.deinitPlugin(pluginId);
} }
} }
connect() { connect() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (!this.connected) { if (!this.connected.get()) {
this.connected = true; this.connected.set(true);
this.events.emit('connect'); this.events.emit('connect');
} }
} }
disconnect() { disconnect() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (this.connected) { if (this.connected.get()) {
this.connected = false; this.connected.set(false);
this.events.emit('disconnect'); this.events.emit('disconnect');
} }
} }
destroy() { destroy() {
if (this.connected) { if (this.connected.get()) {
this.realClient.deinitPlugin(this.definition.id); this.realClient.deinitPlugin(this.definition.id);
} }
super.destroy(); super.destroy();
@@ -265,7 +276,7 @@ export class SandyPluginInstance extends BasePluginInstance {
private assertConnected() { private assertConnected() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (!this.connected) { if (!this.connected.get()) {
throw new Error('Plugin is not connected'); throw new Error('Plugin is not connected');
} }
} }

View File

@@ -90,23 +90,23 @@ export function getCurrentPluginInstance(): typeof currentPluginInstance {
export abstract class BasePluginInstance { export abstract class BasePluginInstance {
/** generally available Flipper APIs */ /** generally available Flipper APIs */
flipperLib: FlipperLib; readonly flipperLib: FlipperLib;
/** the original plugin definition */ /** the original plugin definition */
definition: SandyPluginDefinition; definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */ /** the plugin instance api as used inside components and such */
instanceApi: any; instanceApi: any;
/** the device owning this plugin */ /** the device owning this plugin */
device: Device; readonly device: Device;
activated = false; activated = false;
destroyed = false; destroyed = false;
events = new EventEmitter(); readonly events = new EventEmitter();
// temporarily field that is used during deserialization // temporarily field that is used during deserialization
initialStates?: Record<string, any>; initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import // all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {}; readonly rootStates: Record<string, Atom<any>> = {};
// last seen deeplink // last seen deeplink
lastDeeplink?: any; lastDeeplink?: any;
// export handler // export handler
@@ -135,6 +135,10 @@ export abstract class BasePluginInstance {
get isArchived() { get isArchived() {
return realDevice.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, deviceType: realDevice.deviceType,
onLogEntry(cb) { onLogEntry(cb) {

View File

@@ -38,6 +38,7 @@ import {BasePluginInstance} from '../plugin/PluginBase';
import {FlipperLib} from '../plugin/FlipperLib'; import {FlipperLib} from '../plugin/FlipperLib';
import {stubLogger} from '../utils/Logger'; import {stubLogger} from '../utils/Logger';
import {Idler} from '../utils/Idler'; import {Idler} from '../utils/Idler';
import {createState} from '../state/atom';
type Renderer = RenderResult<typeof queries>; type Renderer = RenderResult<typeof queries>;
@@ -207,10 +208,13 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
isBackgroundPlugin(_pluginId: string) { isBackgroundPlugin(_pluginId: string) {
return !!options?.isBackgroundPlugin; return !!options?.isBackgroundPlugin;
}, },
connected: createState(false),
initPlugin() { initPlugin() {
this.connected.set(true);
pluginInstance.connect(); pluginInstance.connect();
}, },
deinitPlugin() { deinitPlugin() {
this.connected.set(false);
pluginInstance.disconnect(); pluginInstance.disconnect();
}, },
call( call(

View File

@@ -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. 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 ### Events listeners
#### `onMessage` #### `onMessage`
@@ -182,6 +189,9 @@ Usage: `client.send(method: string, params: object): Promise<object>`
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. 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: Example:
```typescript ```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. 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 ### Events
#### `onLogEntry` #### `onLogEntry`