Draft communication with server add-ons from the client side

Reviewed By: nikoant

Differential Revision: D34075379

fbshipit-source-id: 09f575f5cced866ad7b9290d7739ce60f38edeee
This commit is contained in:
Andrey Goncharov
2022-02-28 03:50:34 -08:00
committed by Facebook GitHub Bot
parent db976d5113
commit b80755721c
6 changed files with 140 additions and 68 deletions

View File

@@ -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<ServerAddOnEvents, ServerAddOnMethods> {
/**
* 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<boolean>;
}
@@ -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<string, any>,
) {
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;
}
}

View File

@@ -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<string, any>;
type MethodsContract = Record<string, (params: any) => Promise<any>>;
type PreventIntersectionWith<Contract extends Record<string, any>> = {
[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<Events> = {},
ServerAddOnMethods extends MethodsContract &
PreventIntersectionWith<Methods> = {},
> extends BasePluginClient<ServerAddOnEvents, ServerAddOnMethods> {
/**
* 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<Events, Methods>) => object;
ServerAddOnEvents extends EventsContract & PreventIntersectionWith<Events>,
ServerAddOnMethods extends MethodsContract & PreventIntersectionWith<Methods>,
> = (
client: PluginClient<Events, Methods, ServerAddOnEvents, ServerAddOnMethods>,
) => 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<any, any>;
readonly client: PluginClient<any, any, any, any>;
/** connection alive? */
readonly connected = createState(false);
constructor(
private readonly serverAddOnControls: ServerAddOnControls,
serverAddOnControls: ServerAddOnControls,
flipperLib: FlipperLib,
definition: SandyPluginDefinition,
realClient: RealFlipperClient,
pluginKey: string,
initialStates?: Record<string, any>,
) {
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 (

View File

@@ -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<string, any>;
export type MethodsContract = Record<string, (params: any) => Promise<any>>;
type StateExportHandler<T = any> = (
idler: Idler,
@@ -25,7 +29,10 @@ type StateExportHandler<T = any> = (
) => Promise<T | undefined | void>;
type StateImportHandler<T = any> = (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 extends keyof ServerAddOnEvents>(
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 extends keyof ServerAddOnMethods>(
method: Method,
...params: Parameters<ServerAddOnMethods[Method]> extends []
? []
: [Parameters<ServerAddOnMethods[Method]>[0]]
): ReturnType<ServerAddOnMethods[Method]>;
}
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<any, any> {
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,
);
});
}
}
}

View File

@@ -33,7 +33,7 @@ export function usePluginInstanceMaybe():
}
export function usePlugin<
Factory extends PluginFactory<any, any> | DevicePluginFactory,
Factory extends PluginFactory<any, any, any, any> | DevicePluginFactory,
>(plugin: Factory): ReturnType<Factory> {
const pluginInstance = usePluginInstance();
// In principle we don't *need* the plugin, but having it passed it makes sure the

View File

@@ -26,7 +26,9 @@ export type FlipperDevicePluginModule = {
/**
* FlipperPluginModule describe the exports that are provided by a typical Flipper Desktop plugin
*/
export type FlipperPluginModule<Factory extends PluginFactory<any, any>> = {
export type FlipperPluginModule<
Factory extends PluginFactory<any, any, any, any>,
> = {
/** the factory function that initializes a plugin instance */
plugin: Factory;
/** the component type that can render this plugin */

View File

@@ -505,7 +505,7 @@ export function createMockPluginDetails(
};
}
export function createTestPlugin<T extends PluginFactory<any, any>>(
export function createTestPlugin<T extends PluginFactory<any, any, any, any>>(
implementation: Pick<FlipperPluginModule<T>, 'plugin'> &
Partial<FlipperPluginModule<T>>,
details?: Partial<InstalledPluginDetails>,