Cleanup and some code reuse between Device- and normal Plugins

Summary: Introducing a base abstract class (blegh) to share some life cycle management between Device- and normal plugins. Cleaned up the test utils a bit as well + some old TODO's that now have been taken care of

Reviewed By: nikoant

Differential Revision: D22727089

fbshipit-source-id: 507816f1bfdbc6e7e71d4bd365b881b6710ca917
This commit is contained in:
Michel Weststrate
2020-08-04 07:05:57 -07:00
committed by Facebook GitHub Bot
parent b9c9e89b53
commit 642261c0d0
13 changed files with 275 additions and 353 deletions

View File

@@ -8,9 +8,7 @@
*/
import {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events';
import {Atom} from '../state/atom';
import {setCurrentPluginInstance} from './Plugin';
import {BasePluginInstance, BasePluginClient} from './PluginBase';
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
@@ -42,31 +40,13 @@ export type DevicePluginPredicate = (device: Device) => boolean;
export type DevicePluginFactory = (client: DevicePluginClient) => object;
// TODO: better name?
export interface DevicePluginClient {
export interface DevicePluginClient extends BasePluginClient {
readonly device: Device;
/**
* the onDestroy event is fired whenever a device is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
}
/**
* Wrapper interface around BaseDevice in Flipper
*/
export interface RealFlipperDevice {
isArchived: boolean;
addLogListener(callback: DeviceLogListener): Symbol;
@@ -74,35 +54,20 @@ export interface RealFlipperDevice {
addLogEntry(entry: DeviceLogEntry): void;
}
export class SandyDevicePluginInstance {
export class SandyDevicePluginInstance extends BasePluginInstance {
static is(thing: any): thing is SandyDevicePluginInstance {
return thing instanceof SandyDevicePluginInstance;
}
/** client that is bound to this instance */
client: DevicePluginClient;
/** the original plugin definition */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor(
realDevice: RealFlipperDevice,
definition: SandyPluginDefinition,
initialStates?: Record<string, any>,
) {
this.definition = definition;
super(definition, initialStates);
const device: Device = {
get isArchived() {
return realDevice.isArchived;
@@ -115,84 +80,15 @@ export class SandyDevicePluginInstance {
},
};
this.client = {
...this.createBasePluginClient(),
device,
onDestroy: (cb) => {
this.events.on('destroy', cb);
},
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
};
setCurrentPluginInstance(this);
this.initialStates = initialStates;
try {
this.instanceApi = definition
.asDevicePluginModule()
.devicePlugin(this.client);
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.activated = true;
this.events.emit('activate');
}
}
deactivate() {
if (this.destroyed) {
return;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
this.events.emit('destroy');
this.destroyed = true;
this.initializePlugin(() =>
definition.asDevicePluginModule().devicePlugin(this.client),
);
}
toJSON() {
return '[SandyDevicePluginInstance]';
}
triggerDeepLink(deepLink: unknown) {
this.assertNotDestroyed();
if (deepLink !== this.lastDeeplink) {
this.lastDeeplink = deepLink;
this.events.emit('deeplink', deepLink);
}
}
exportState() {
return Object.fromEntries(
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
private assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
}

View File

@@ -8,9 +8,7 @@
*/
import {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events';
import {Atom} from '../state/atom';
import {SandyDevicePluginInstance} from './DevicePlugin';
import {BasePluginInstance, BasePluginClient} from './PluginBase';
type EventsContract = Record<string, any>;
type MethodsContract = Record<string, (params: any) => Promise<any>>;
@@ -23,25 +21,10 @@ type Message = {
/**
* API available to a plugin factory
*/
export interface FlipperClient<
export interface PluginClient<
Events extends EventsContract = {},
Methods extends MethodsContract = {}
> {
/**
* the onDestroy event is fired whenever a client is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
> extends BasePluginClient {
/**
* 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,
@@ -57,11 +40,6 @@ export interface FlipperClient<
*/
onDisconnect(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
/**
* Send a message to the connected client
*/
@@ -101,26 +79,11 @@ export interface RealFlipperClient {
export type PluginFactory<
Events extends EventsContract,
Methods extends MethodsContract
> = (client: FlipperClient<Events, Methods>) => object;
> = (client: PluginClient<Events, Methods>) => object;
export type FlipperPluginComponent = React.FC<{}>;
let currentPluginInstance:
| SandyPluginInstance
| SandyDevicePluginInstance
| undefined = undefined;
export function setCurrentPluginInstance(
instance: typeof currentPluginInstance,
) {
currentPluginInstance = instance;
}
export function getCurrentPluginInstance(): typeof currentPluginInstance {
return currentPluginInstance;
}
export class SandyPluginInstance {
export class SandyPluginInstance extends BasePluginInstance {
static is(thing: any): thing is SandyPluginInstance {
return thing instanceof SandyPluginInstance;
}
@@ -128,41 +91,20 @@ export class SandyPluginInstance {
/** base client provided by Flipper */
realClient: RealFlipperClient;
/** client that is bound to this instance */
client: FlipperClient<any, any>;
/** the original plugin definition */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
client: PluginClient<any, any>;
/** connection alive? */
connected = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor(
realClient: RealFlipperClient,
definition: SandyPluginDefinition,
initialStates?: Record<string, any>,
) {
super(definition, initialStates);
this.realClient = realClient;
this.definition = definition;
this.client = {
onDestroy: (cb) => {
this.events.on('destroy', cb);
},
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
...this.createBasePluginClient(),
onConnect: (cb) => {
this.events.on('connect', cb);
},
@@ -181,45 +123,24 @@ export class SandyPluginInstance {
onMessage: (event, callback) => {
this.events.on('event-' + event, callback);
},
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
};
setCurrentPluginInstance(this);
this.initialStates = initialStates;
try {
this.instanceApi = definition.asPluginModule().plugin(this.client);
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
this.initializePlugin(() =>
definition.asPluginModule().plugin(this.client),
);
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.activated = true;
this.events.emit('activate');
const pluginId = this.definition.id;
if (!this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.initPlugin(pluginId); // will call connect() if needed
}
super.activate();
const pluginId = this.definition.id;
if (!this.connected && !this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.initPlugin(pluginId); // will call connect() if needed
}
}
// the plugin is deselected in the UI
deactivate() {
if (this.destroyed) {
// this can happen if the plugin is disabled while active in the UI.
// In that case deinit & destroy is already triggered from the STAR_PLUGIN action
return;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
super.deactivate();
const pluginId = this.definition.id;
if (this.connected && !this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.deinitPlugin(pluginId);
@@ -243,13 +164,10 @@ export class SandyPluginInstance {
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
if (this.connected) {
this.realClient.deinitPlugin(this.definition.id);
}
this.events.emit('destroy');
this.destroyed = true;
super.destroy();
}
receiveMessages(messages: Message[]) {
@@ -262,30 +180,6 @@ export class SandyPluginInstance {
return '[SandyPluginInstance]';
}
triggerDeepLink(deepLink: unknown) {
this.assertNotDestroyed();
if (deepLink !== this.lastDeeplink) {
this.lastDeeplink = deepLink;
this.events.emit('deeplink', deepLink);
}
}
exportState() {
return Object.fromEntries(
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
private assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
private assertConnected() {
this.assertNotDestroyed();
if (!this.connected) {

View File

@@ -0,0 +1,153 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events';
import {Atom} from '../state/atom';
export interface BasePluginClient {
/**
* the onDestroy event is fired whenever a device is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
}
let currentPluginInstance: BasePluginInstance | undefined = undefined;
export function setCurrentPluginInstance(
instance: typeof currentPluginInstance,
) {
currentPluginInstance = instance;
}
export function getCurrentPluginInstance(): typeof currentPluginInstance {
return currentPluginInstance;
}
export abstract class BasePluginInstance {
/** the original plugin definition */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor(
definition: SandyPluginDefinition,
initialStates?: Record<string, any>,
) {
this.definition = definition;
this.initialStates = initialStates;
}
protected initializePlugin(factory: () => any) {
// To be called from constructory
setCurrentPluginInstance(this);
try {
this.instanceApi = factory();
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
}
protected createBasePluginClient(): BasePluginClient {
return {
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
onDestroy: (cb) => {
this.events.on('destroy', cb);
},
};
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.activated = true;
this.events.emit('activate');
}
}
deactivate() {
if (this.destroyed) {
return;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
this.events.emit('destroy');
this.destroyed = true;
}
triggerDeepLink(deepLink: unknown) {
this.assertNotDestroyed();
if (deepLink !== this.lastDeeplink) {
this.lastDeeplink = deepLink;
this.events.emit('deeplink', deepLink);
}
}
exportState() {
return Object.fromEntries(
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
protected assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
abstract toJSON(): string;
}