Introduce DevicePlugin APIs
Summary: This stack introduces Sandy device plugins, they are quite similar to normal plugins, but, a devicePlugin module is organized as ``` export function supportsDevice(device): boolean export function devicePlugin(devicePluginClient) export function Component ``` Device plugins get access to the device meta data and can subscribe to the `onLogEntry` callback and `onDestroy` lifecycle. They will be able to store state just as normal plugins, but can't send or receive methods, so devicePluginClient is a bit limited. This diff only sets up most of the new data structures, and makes sure everything still compiles and no existing tests fail. To prevent this diff from becoming to big, actually loading, rendering and testing device plugins will be done in next diffs Please take a critical look at the api proposed and the (especially) the public names used :) Reviewed By: passy, nikoant Differential Revision: D22691351 fbshipit-source-id: bdbbd7f86d14b646fc9a693ad19f33583a76f26d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6083534025
commit
91ed4e31c0
198
desktop/flipper-plugin/src/plugin/DevicePlugin.tsx
Normal file
198
desktop/flipper-plugin/src/plugin/DevicePlugin.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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';
|
||||
import {setCurrentPluginInstance} from './Plugin';
|
||||
|
||||
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
|
||||
|
||||
export type DeviceLogEntry = {
|
||||
readonly date: Date;
|
||||
readonly pid: number;
|
||||
readonly tid: number;
|
||||
readonly app?: string;
|
||||
readonly type: LogLevel;
|
||||
readonly tag: string;
|
||||
readonly message: string;
|
||||
};
|
||||
|
||||
export type LogLevel =
|
||||
| 'unknown'
|
||||
| 'verbose'
|
||||
| 'debug'
|
||||
| 'info'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'fatal';
|
||||
|
||||
export interface Device {
|
||||
isArchived: boolean;
|
||||
onLogEntry(cb: DeviceLogListener): () => void;
|
||||
}
|
||||
|
||||
export type DevicePluginPredicate = (device: Device) => boolean;
|
||||
|
||||
export type DevicePluginFactory = (client: DevicePluginClient) => object;
|
||||
|
||||
export interface DevicePluginClient {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface RealFlipperDevice {
|
||||
isArchived: boolean;
|
||||
addLogListener(callback: DeviceLogListener): Symbol;
|
||||
removeLogListener(id: Symbol): void;
|
||||
addLogEntry(entry: DeviceLogEntry): void;
|
||||
}
|
||||
|
||||
export class SandyDevicePluginInstance {
|
||||
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>> = {};
|
||||
|
||||
constructor(
|
||||
realDevice: RealFlipperDevice,
|
||||
definition: SandyPluginDefinition,
|
||||
initialStates?: Record<string, any>,
|
||||
) {
|
||||
this.definition = definition;
|
||||
const device: Device = {
|
||||
get isArchived() {
|
||||
return realDevice.isArchived;
|
||||
},
|
||||
onLogEntry(cb) {
|
||||
const handle = realDevice.addLogListener(cb);
|
||||
return () => {
|
||||
realDevice.removeLogListener(handle);
|
||||
};
|
||||
},
|
||||
};
|
||||
this.client = {
|
||||
device,
|
||||
onDestroy: (cb) => {
|
||||
this.events.on('destroy', cb);
|
||||
},
|
||||
onActivate: (cb) => {
|
||||
this.events.on('activate', cb);
|
||||
},
|
||||
onDeactivate: (cb) => {
|
||||
this.events.on('deactivate', cb);
|
||||
},
|
||||
};
|
||||
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');
|
||||
}
|
||||
// TODO:
|
||||
// const pluginId = this.definition.id;
|
||||
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
||||
// this.realClient.initPlugin(pluginId); // will call connect() if needed
|
||||
// }
|
||||
}
|
||||
|
||||
// the plugin is deselected in the UI
|
||||
deactivate() {
|
||||
// TODO:
|
||||
// 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;
|
||||
// }
|
||||
// const pluginId = this.definition.id;
|
||||
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
||||
// this.realClient.deinitPlugin(pluginId);
|
||||
// }
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.assertNotDestroyed();
|
||||
if (this.activated) {
|
||||
this.activated = false;
|
||||
this.events.emit('deactivate');
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.assertNotDestroyed();
|
||||
// TODO:
|
||||
// if (this.activated) {
|
||||
// this.realClient.deinitPlugin(this.definition.id);
|
||||
// }
|
||||
this.events.emit('destroy');
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return '[SandyDevicePluginInstance]';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user