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 {SandyPluginDefinition} from './SandyPluginDefinition';
import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {
BasePluginInstance,
BasePluginClient,
EventsContract,
MethodsContract,
} from './PluginBase';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {Atom, ReadOnlyAtom} from '../state/atom'; import {Atom, ReadOnlyAtom} from '../state/atom';
import { import {
@@ -46,12 +51,14 @@ export type DevicePluginPredicate = (device: Device) => boolean;
export type DevicePluginFactory = (client: DevicePluginClient) => object; 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 * opens a different plugin by id, optionally providing a deeplink to bring the plugin to a certain state
*/ */
selectPlugin(pluginId: string, deeplinkPayload?: unknown): void; selectPlugin(pluginId: string, deeplinkPayload?: unknown): void;
readonly isConnected: boolean; readonly isConnected: boolean;
readonly connected: ReadOnlyAtom<boolean>; readonly connected: ReadOnlyAtom<boolean>;
} }
@@ -65,14 +72,21 @@ export class SandyDevicePluginInstance extends BasePluginInstance {
readonly client: DevicePluginClient; readonly client: DevicePluginClient;
constructor( constructor(
private readonly serverAddOnControls: ServerAddOnControls, serverAddOnControls: ServerAddOnControls,
flipperLib: FlipperLib, flipperLib: FlipperLib,
definition: SandyPluginDefinition, definition: SandyPluginDefinition,
device: Device, device: Device,
pluginKey: string, pluginKey: string,
initialStates?: Record<string, any>, initialStates?: Record<string, any>,
) { ) {
super(flipperLib, definition, device, pluginKey, initialStates); super(
serverAddOnControls,
flipperLib,
definition,
device,
pluginKey,
initialStates,
);
this.client = { this.client = {
...this.createBasePluginClient(), ...this.createBasePluginClient(),
selectPlugin(pluginId: string, deeplink?: unknown) { selectPlugin(pluginId: string, deeplink?: unknown) {
@@ -98,31 +112,7 @@ export class SandyDevicePluginInstance extends BasePluginInstance {
super.destroy(); super.destroy();
} }
private startServerAddOn() { protected get serverAddOnOwner() {
const {serverAddOn, name} = this.definition.details; return this.device.serial;
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,
);
});
}
} }
} }

View File

@@ -8,15 +8,21 @@
*/ */
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {
BasePluginInstance,
BasePluginClient,
EventsContract,
MethodsContract,
} from './PluginBase';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {Device} from './DevicePlugin'; import {Device} from './DevicePlugin';
import {batched} from '../state/batch'; import {batched} from '../state/batch';
import {Atom, createState, ReadOnlyAtom} from '../state/atom'; import {Atom, createState, ReadOnlyAtom} from '../state/atom';
import {ServerAddOnControls} from 'flipper-common'; import {ServerAddOnControls} from 'flipper-common';
type EventsContract = Record<string, any>; type PreventIntersectionWith<Contract extends Record<string, any>> = {
type MethodsContract = Record<string, (params: any) => Promise<any>>; [Key in keyof Contract]?: never;
};
type Message = { type Message = {
method: string; method: string;
@@ -29,7 +35,11 @@ type Message = {
export interface PluginClient< export interface PluginClient<
Events extends EventsContract = {}, Events extends EventsContract = {},
Methods extends MethodsContract = {}, 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 * Identifier that uniquely identifies the connected application
*/ */
@@ -125,7 +135,11 @@ export interface RealFlipperClient {
export type PluginFactory< export type PluginFactory<
Events extends EventsContract, Events extends EventsContract,
Methods extends MethodsContract, 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<{}>; export type FlipperPluginComponent = React.FC<{}>;
@@ -137,19 +151,26 @@ export class SandyPluginInstance extends BasePluginInstance {
/** base client provided by Flipper */ /** base client provided by Flipper */
readonly realClient: RealFlipperClient; readonly realClient: RealFlipperClient;
/** client that is bound to this instance */ /** client that is bound to this instance */
readonly client: PluginClient<any, any>; readonly client: PluginClient<any, any, any, any>;
/** connection alive? */ /** connection alive? */
readonly connected = createState(false); readonly connected = createState(false);
constructor( constructor(
private readonly serverAddOnControls: ServerAddOnControls, serverAddOnControls: ServerAddOnControls,
flipperLib: FlipperLib, flipperLib: FlipperLib,
definition: SandyPluginDefinition, definition: SandyPluginDefinition,
realClient: RealFlipperClient, realClient: RealFlipperClient,
pluginKey: string, pluginKey: string,
initialStates?: Record<string, any>, initialStates?: Record<string, any>,
) { ) {
super(flipperLib, definition, realClient.device, pluginKey, initialStates); super(
serverAddOnControls,
flipperLib,
definition,
realClient.device,
pluginKey,
initialStates,
);
this.realClient = realClient; this.realClient = realClient;
this.definition = definition; this.definition = definition;
const self = this; const self = this;
@@ -231,18 +252,7 @@ export class SandyPluginInstance extends BasePluginInstance {
connect() { connect() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (!this.connected.get()) { if (!this.connected.get()) {
const {serverAddOn, name} = this.definition.details; this.startServerAddOn();
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.connected.set(true); this.connected.set(true);
this.events.emit('connect'); this.events.emit('connect');
} }
@@ -251,18 +261,7 @@ export class SandyPluginInstance extends BasePluginInstance {
disconnect() { disconnect() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (this.connected.get()) { if (this.connected.get()) {
const {serverAddOn, name} = this.definition.details; this.stopServerAddOn();
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.connected.set(false); this.connected.set(false);
this.events.emit('disconnect'); this.events.emit('disconnect');
} }
@@ -289,6 +288,10 @@ export class SandyPluginInstance extends BasePluginInstance {
return '[SandyPluginInstance]'; return '[SandyPluginInstance]';
} }
protected get serverAddOnOwner() {
return this.realClient.id;
}
private assertConnected() { private assertConnected() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if ( if (

View File

@@ -18,6 +18,10 @@ import {Idler} from '../utils/Idler';
import {Notification} from './Notification'; import {Notification} from './Notification';
import {Logger} from '../utils/Logger'; import {Logger} from '../utils/Logger';
import {CreatePasteArgs, CreatePasteResult} from './Paste'; 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> = ( type StateExportHandler<T = any> = (
idler: Idler, idler: Idler,
@@ -25,7 +29,10 @@ type StateExportHandler<T = any> = (
) => Promise<T | undefined | void>; ) => Promise<T | undefined | void>;
type StateImportHandler<T = any> = (data: T) => 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. * 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 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; 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; let currentPluginInstance: BasePluginInstance | undefined = undefined;
@@ -200,6 +237,7 @@ export abstract class BasePluginInstance {
readonly instanceId = ++staticInstanceId; readonly instanceId = ++staticInstanceId;
constructor( constructor(
private readonly serverAddOnControls: ServerAddOnControls,
flipperLib: FlipperLib, flipperLib: FlipperLib,
definition: SandyPluginDefinition, definition: SandyPluginDefinition,
device: Device, device: Device,
@@ -269,7 +307,7 @@ export abstract class BasePluginInstance {
} }
} }
protected createBasePluginClient(): BasePluginClient { protected createBasePluginClient(): BasePluginClient<any, any> {
return { return {
pluginKey: this.pluginKey, pluginKey: this.pluginKey,
device: this.device, device: this.device,
@@ -341,6 +379,15 @@ export abstract class BasePluginInstance {
this.flipperLib.showNotification(this.pluginKey, notification); this.flipperLib.showNotification(this.pluginKey, notification);
}, },
logger: this.flipperLib.logger, 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; 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< export function usePlugin<
Factory extends PluginFactory<any, any> | DevicePluginFactory, Factory extends PluginFactory<any, any, any, any> | DevicePluginFactory,
>(plugin: Factory): ReturnType<Factory> { >(plugin: Factory): ReturnType<Factory> {
const pluginInstance = usePluginInstance(); const pluginInstance = usePluginInstance();
// In principle we don't *need* the plugin, but having it passed it makes sure the // 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 * 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 */ /** the factory function that initializes a plugin instance */
plugin: Factory; plugin: Factory;
/** the component type that can render this plugin */ /** 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'> & implementation: Pick<FlipperPluginModule<T>, 'plugin'> &
Partial<FlipperPluginModule<T>>, Partial<FlipperPluginModule<T>>,
details?: Partial<InstalledPluginDetails>, details?: Partial<InstalledPluginDetails>,