update js-client api (migrate to TS)

Summary:
JS/TS api:
- migrate to TS
- some refactoring (get rid of bridge, make client abstract)

Implementation isn't full yet, things to be implemented:
- let plugins connect on init command from Flipper
- implement Responder

Further plans:
- make fully compatible with react-native api without breaking changes

Reviewed By: mweststrate

Differential Revision: D21839377

fbshipit-source-id: 9e9fe4ad01632f958b59eb255c703c6cbc5fafe2
This commit is contained in:
Timur Valiev
2020-06-11 08:40:07 -07:00
committed by Facebook GitHub Bot
parent f88d707dbb
commit 896a90aa26
22 changed files with 399 additions and 530 deletions

View File

@@ -0,0 +1,147 @@
/**
* 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
*/
export type FlipperPluginID = string;
export type FlipperMethodID = string;
export class FlipperResponder {
pluginId: FlipperPluginID;
methodId: FlipperMethodID;
private _client: FlipperClient;
constructor(
pluginId: FlipperPluginID,
methodId: FlipperMethodID,
client: FlipperClient
) {
this.pluginId = pluginId;
this.methodId = methodId;
this._client = client;
}
success(_response: any) {}
error(_response: any) {}
}
export type FlipperReceiver<T> = (
params: T,
responder: FlipperResponder,
) => void;
export class FlipperConnection {
pluginId: FlipperPluginID;
private client: FlipperClient;
constructor(pluginId: FlipperPluginID, client: FlipperClient) {
this.pluginId = pluginId;
this.client = client;
}
send(method: FlipperMethodID, data: any) {
this.client.sendData(this.pluginId, method, data);
}
receive<T>(method: FlipperMethodID, receiver: FlipperReceiver<T>) {
this.client.subscribe(this.pluginId, method, (data: T) => {
receiver(data, new FlipperResponder(this.pluginId, method, this.client));
});
}
}
export interface FlipperPlugin {
/**
* @return The id of this plugin. This is the namespace which Flipper desktop plugins will call
* methods on to route them to your plugin. This should match the id specified in your React
* plugin.
*/
getId(): string;
/**
* Called when a connection has been established. The connection passed to this method is valid
* until {@link FlipperPlugin#onDisconnect()} is called.
*/
onConnect(connection: FlipperConnection): void;
/**
* Called when the connection passed to `FlipperPlugin#onConnect(FlipperConnection)` is no
* longer valid. Do not try to use the connection in or after this method has been called.
*/
onDisconnect(): void;
/**
* Returns true if the plugin is meant to be run in background too, otherwise it returns false.
*/
runInBackground(): boolean;
}
export abstract class AbstractFlipperPlugin implements FlipperPlugin{
protected connection: FlipperConnection | null | undefined;
onConnect(connection: FlipperConnection): void {
this.connection = connection;
}
onDisconnect(): void {
this.connection = null;
}
abstract getId(): string;
abstract runInBackground(): boolean;
}
export abstract class FlipperClient {
_isConnected: boolean = false;
plugins: Map<FlipperPluginID, FlipperPlugin> = new Map();
addPlugin(plugin: FlipperPlugin) {
if (this._isConnected) {
plugin.onConnect(new FlipperConnection(plugin.getId(), this));
}
this.plugins.set(plugin.getId(), plugin);
}
getPlugin(id: FlipperPluginID): FlipperPlugin | undefined {
return this.plugins.get(id);
}
onConnect() {
if (this._isConnected) {
return;
}
this._isConnected = true;
Array.from(this.plugins.values()).map((plugin) =>
plugin.onConnect(new FlipperConnection(plugin.getId(), this)),
);
}
onDisconnect() {
this._isConnected = false;
Array.from(this.plugins.values()).map((plugin) => plugin.onDisconnect());
}
abstract start: (appName: string) => void;
abstract stop: () => void;
abstract sendData: (
plugin: FlipperPluginID,
method: FlipperMethodID,
data: any,
) => void;
abstract subscribe: <T>(
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (message: T) => void,
) => void;
abstract isAvailable: () => boolean;
}

View File

@@ -0,0 +1,50 @@
/**
* 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 {FlipperClient, AbstractFlipperPlugin} from './api';
import {newWebviewClient} from './webviewImpl';
class SeaMammalPlugin extends AbstractFlipperPlugin {
getId(): string {
return 'sea-mammals';
}
runInBackground(): boolean {
return true;
}
newRow(row: {id: string, url: string, title: string}) {
this.connection?.send("newRow", row)
}
}
class FlipperManager {
flipperClient: FlipperClient;
seaMammalPlugin: SeaMammalPlugin;
constructor() {
this.flipperClient = newWebviewClient();
this.seaMammalPlugin = new SeaMammalPlugin();
this.flipperClient.addPlugin(this.seaMammalPlugin);
this.flipperClient.start('Example JS App');
}
}
let flipperManager: FlipperManager | undefined;
export function init() {
if (!flipperManager) {
flipperManager = new FlipperManager();
}
}
export function flipper(): FlipperManager | undefined {
return flipperManager;
}

View File

@@ -0,0 +1,10 @@
/**
* 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
*/
export * from './api';

View File

@@ -0,0 +1,58 @@
/**
* 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 {FlipperClient} from './api';
import type {FlipperPluginID, FlipperMethodID} from './api';
class FlipperWebviewClient extends FlipperClient {
_subscriptions: Map<string, (message: any) => void> = new Map();
_client: FlipperClient | null = null;
start = (appName: string) => {
const bridge = (window as any).FlipperWebviewBridge;
bridge?.registerPlugins(this.plugins);
bridge?.start(appName);
};
stop = () => {
const bridge = (window as any).FlipperWebviewBridge;
bridge?.FlipperWebviewBridge.stop();
};
sendData = (plugin: FlipperPluginID, method: FlipperMethodID, data: any) => {
const bridge = (window as any).FlipperWebviewBridge;
bridge && bridge.sendFlipperObject(plugin, method, JSON.stringify(data));
};
subscribe = (
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (msg: any) => void,
) => {
this._subscriptions.set(plugin + method, handler);
};
isAvailable = () => {
return (window as any).FlipperWebviewBridge != null;
};
receive(plugin: FlipperPluginID, method: FlipperMethodID, data: string) {
const handler = this._subscriptions.get(plugin + method);
handler && handler(JSON.parse(data));
}
setClient(client: FlipperClient) {
this._client = client;
}
}
export function newWebviewClient(): FlipperClient {
return new FlipperWebviewClient();
}