js api improvements: responnders, communication protocol

Summary:
A few improvements to JS API:
1) non-dummy responders - now we can reply to flipper
2) respecting flipper communication protocol: getPlugins, getBackgroundplugins, init, deinit, execute

adding linters

Reviewed By: jknoxville

Differential Revision: D22307525

fbshipit-source-id: 2f629210f398d118cc0cb99097c9d473bb466e57
This commit is contained in:
Timur Valiev
2020-07-17 04:53:09 -07:00
committed by Facebook GitHub Bot
parent 3814c8fdfc
commit 7dbcfc89b0
8 changed files with 2069 additions and 87 deletions

View File

@@ -0,0 +1,4 @@
*.bundle.js
node_modules
lib
!.eslintrc.js

View File

@@ -0,0 +1,90 @@
/**
* 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
*/
const fbjs = require('eslint-config-fbjs');
// enforces copyright header and @format directive to be present in every file
const pattern = /^\*\r?\n[\S\s]*Facebook[\S\s]* \* @format\r?\n/;
const prettierConfig = {
// arrowParens=always is the default for Prettier 2.0, but other configs
// at Facebook appear to be leaking into this file, which is still on
// Prettier 1.x at the moment, so it is best to be explicit.
arrowParens: 'always',
requirePragma: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: false,
jsxBracketSameLine: true,
parser: 'flow',
};
module.exports = {
parser: 'babel-eslint',
root: true,
extends: ['fbjs', 'prettier'],
plugins: [
...fbjs.plugins,
'header',
'prettier',
'@typescript-eslint',
'import',
],
rules: {
// disable rules from eslint-config-fbjs
'no-new': 0, // new keyword needed e.g. new Notification
'no-catch-shadow': 0, // only relevant for IE8 and below
'no-bitwise': 0, // bitwise operations needed in some places
'consistent-return': 0,
'no-var': 2,
'prefer-const': [2, {destructuring: 'all'}],
'prefer-spread': 1,
'prefer-rest-params': 1,
'no-console': 0, // we're setting window.console in App.js
'no-multi-spaces': 2,
'prefer-promise-reject-errors': 1,
'no-throw-literal': 'error',
'no-extra-boolean-cast': 2,
'no-extra-semi': 2,
'no-unsafe-negation': 2,
'no-useless-computed-key': 2,
'no-useless-rename': 2,
// additional rules for this project
'header/header': [2, 'block', {pattern}],
'prettier/prettier': [2, prettierConfig],
'flowtype/object-type-delimiter': [0],
'import/no-unresolved': [2, {commonjs: true, amd: true}],
},
settings: {
'import/resolver': {
typescript: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
overrides: [
{
files: ['*.tsx', '*.ts'],
parser: '@typescript-eslint/parser',
rules: {
'prettier/prettier': [2, {...prettierConfig, parser: 'typescript'}],
'@typescript-eslint/no-unused-vars': [
1,
{
ignoreRestSiblings: true,
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
],
};

View File

@@ -1,12 +1,16 @@
{ {
"name": "flipper-client-sdk", "name": "flipper-client-sdk",
"version": "0.0.1", "version": "0.0.2",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"title": "Flipper SDK API", "title": "Flipper SDK API",
"scripts": { "scripts": {
"reset": "rimraf lib *.tsbuildinfo", "reset": "rimraf lib *.tsbuildinfo",
"build": "tsc -b" "build": "tsc -b",
"fix": "eslint . --fix --ext .js,.ts,.tsx",
"lint:tsc": "tsc --noemit",
"lint:eslint": "eslint . --ext .js,.ts,.tsx",
"lint": "yarn lint:eslint && yarn lint:tsc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -23,6 +27,24 @@
"licenseFilename": "LICENSE", "licenseFilename": "LICENSE",
"readmeFilename": "README.md", "readmeFilename": "README.md",
"devDependencies": { "devDependencies": {
"typescript": "^3.9.2" "typescript": "^3.9.2",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.19.2",
"ansi-to-html": "^0.6.3",
"babel-eslint": "^10.0.1",
"eslint": "^6.7.0",
"eslint-config-fbjs": "^3.1.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^4.7.0",
"eslint-plugin-header": "^3.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.4",
"prettier": "^2.0.0"
} }
} }

View File

@@ -12,47 +12,65 @@ export type FlipperPluginID = string;
export type FlipperMethodID = string; export type FlipperMethodID = string;
export class FlipperResponder { export class FlipperResponder {
pluginId: FlipperPluginID; messageID?: number;
methodId: FlipperMethodID; private client: FlipperClient;
private _client: FlipperClient;
constructor( constructor(messageID: number, client: FlipperClient) {
pluginId: FlipperPluginID, this.messageID = messageID;
methodId: FlipperMethodID, this.client = client;
client: FlipperClient
) {
this.pluginId = pluginId;
this.methodId = methodId;
this._client = client;
} }
success(_response: any) {} success(response?: any) {
this.client.sendData({id: this.messageID, success: response});
}
error(_response: any) {} error(response?: any) {
this.client.sendData({id: this.messageID, error: response});
}
} }
export type FlipperReceiver<T> = ( export type FlipperReceiver = (
params: T, params: any,
responder: FlipperResponder, responder: FlipperResponder,
) => void; ) => void;
export class FlipperConnection { export class FlipperConnection {
pluginId: FlipperPluginID; pluginId: FlipperPluginID;
private client: FlipperClient; private client: FlipperClient;
private subscriptions: Map<FlipperMethodID, FlipperReceiver> = new Map();
constructor(pluginId: FlipperPluginID, client: FlipperClient) { constructor(pluginId: FlipperPluginID, client: FlipperClient) {
this.pluginId = pluginId; this.pluginId = pluginId;
this.client = client; this.client = client;
} }
send(method: FlipperMethodID, data: any) { send(method: FlipperMethodID, params: any) {
this.client.sendData(this.pluginId, method, data); this.client.sendData({
method: 'execute',
params: {
api: this.pluginId,
method,
params,
},
});
} }
receive<T>(method: FlipperMethodID, receiver: FlipperReceiver<T>) { receive(method: FlipperMethodID, receiver: FlipperReceiver) {
this.client.subscribe(this.pluginId, method, (data: T) => { this.subscriptions.set(method, receiver);
receiver(data, new FlipperResponder(this.pluginId, method, this.client)); }
});
call(method: FlipperMethodID, params: any, responder: FlipperResponder) {
const receiver = this.subscriptions.get(method);
if (receiver == null) {
const errorMessage = `Receiver ${method} not found.`;
responder.error({message: errorMessage});
return;
}
receiver.call(receiver, params, responder);
}
hasReceiver(method: FlipperMethodID): boolean {
return this.subscriptions.has(method);
} }
} }
@@ -82,28 +100,14 @@ export interface FlipperPlugin {
runInBackground(): boolean; 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 { export abstract class FlipperClient {
_isConnected: boolean = false; private _isConnected: boolean = false;
plugins: Map<FlipperPluginID, FlipperPlugin> = new Map(); protected plugins: Map<FlipperPluginID, FlipperPlugin> = new Map();
protected connections: Map<FlipperPluginID, FlipperConnection> = new Map();
addPlugin(plugin: FlipperPlugin) { addPlugin(plugin: FlipperPlugin) {
if (this._isConnected) { if (this._isConnected) {
plugin.onConnect(new FlipperConnection(plugin.getId(), this)); this.connectPlugin(plugin);
} }
this.plugins.set(plugin.getId(), plugin); this.plugins.set(plugin.getId(), plugin);
} }
@@ -117,31 +121,130 @@ export abstract class FlipperClient {
return; return;
} }
this._isConnected = true; this._isConnected = true;
Array.from(this.plugins.values()).map((plugin) => Array.from(this.plugins.values())
plugin.onConnect(new FlipperConnection(plugin.getId(), this)), .filter((plugin) => plugin.runInBackground())
); .map(this.connectPlugin);
} }
onDisconnect() { onDisconnect() {
this._isConnected = false; this._isConnected = false;
Array.from(this.plugins.values()).map((plugin) => plugin.onDisconnect()); Array.from(this.plugins.values()).map(this.disconnectPlugin);
} }
abstract start: (appName: string) => void; abstract start(appName: string): void;
abstract stop: () => void; abstract stop(): void;
abstract sendData: ( abstract sendData(payload: any): void;
plugin: FlipperPluginID,
method: FlipperMethodID,
data: any,
) => void;
abstract subscribe: <T>( abstract isAvailable(): boolean;
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (message: T) => void,
) => void;
abstract isAvailable: () => boolean; protected onMessageReceived(message: {
method: string;
id: number;
params: any;
}) {
let responder: FlipperResponder | undefined;
try {
const {method, params, id} = message;
responder = new FlipperResponder(id, this);
if (method === 'getPlugins') {
responder.success({plugins: [...this.plugins.keys()]});
return;
}
if (method === 'getBackgroundPlugins') {
responder.success({
plugins: [...this.plugins.keys()].filter((key) =>
this.plugins.get(key)?.runInBackground(),
),
});
return;
}
if (method === 'init') {
const identifier = params['plugin'] as string;
const plugin = this.plugins.get(identifier);
if (plugin == null) {
const errorMessage = `Plugin ${identifier} not found for method ${method}`;
responder.error({message: errorMessage, name: 'PluginNotFound'});
return;
}
this.connectPlugin(plugin);
return;
}
if (method === 'deinit') {
const identifier = params['plugin'] as string;
const plugin = this.plugins.get(identifier);
if (plugin == null) {
const errorMessage = `Plugin ${identifier} not found for method ${method}`;
responder.error({message: errorMessage, name: 'PluginNotFound'});
return;
}
this.disconnectPlugin(plugin);
return;
}
if (method === 'execute') {
const identifier = params['api'] as string;
const connection = this.connections.get(identifier);
if (connection == null) {
const errorMessage = `Connection ${identifier} not found for plugin identifier`;
responder.error({message: errorMessage, name: 'ConnectionNotFound'});
return;
}
connection.call(
params['method'] as string,
params['params'],
responder,
);
return;
}
if (method === 'isMethodSupported') {
const identifier = params['api'].getString();
const connection = this.connections.get(identifier);
if (connection == null) {
const errorMessage = `Connection ${identifier} not found for plugin identifier`;
responder.error({message: errorMessage, name: 'ConnectionNotFound'});
return;
}
const isSupported = connection.hasReceiver(
params['method'].getString(),
);
responder.success({isSupported: isSupported});
return;
}
const response = {message: 'Received unknown method: ' + method};
responder.error(response);
} catch (e) {
if (responder) {
responder.error({
message: 'Unknown error during ' + JSON.stringify(message),
name: 'Unknown',
});
}
}
}
private connectPlugin(plugin: FlipperPlugin): void {
const id = plugin.getId();
const connection = new FlipperConnection(id, this);
plugin.onConnect(connection);
this.connections.set(id, connection);
}
private disconnectPlugin(plugin: FlipperPlugin): void {
const id = plugin.getId();
plugin.onDisconnect();
this.connections.delete(id);
}
} }

View File

@@ -7,10 +7,19 @@
* @format * @format
*/ */
import {FlipperClient, AbstractFlipperPlugin} from './api'; import {FlipperClient, FlipperConnection, FlipperPlugin} from './api';
import {newWebviewClient} from './webviewImpl'; import {newWebviewClient} from './webviewImpl';
class SeaMammalPlugin extends AbstractFlipperPlugin { export class SeaMammalPlugin implements FlipperPlugin {
protected connection: FlipperConnection | null | undefined;
onConnect(connection: FlipperConnection): void {
this.connection = connection;
}
onDisconnect(): void {
this.connection = null;
}
getId(): string { getId(): string {
return 'sea-mammals'; return 'sea-mammals';
} }
@@ -19,10 +28,9 @@ class SeaMammalPlugin extends AbstractFlipperPlugin {
return true; return true;
} }
newRow(row: {id: string, url: string, title: string}) { newRow(row: {id: string; url: string; title: string}) {
this.connection?.send("newRow", row) this.connection?.send('newRow', row);
} }
} }
class FlipperManager { class FlipperManager {

View File

@@ -9,8 +9,6 @@
import {FlipperClient} from './api'; import {FlipperClient} from './api';
import type {FlipperPluginID, FlipperMethodID} from './api';
class FlipperWebviewClient extends FlipperClient { class FlipperWebviewClient extends FlipperClient {
_subscriptions: Map<string, (message: any) => void> = new Map(); _subscriptions: Map<string, (message: any) => void> = new Map();
_client: FlipperClient | null = null; _client: FlipperClient | null = null;
@@ -26,31 +24,14 @@ class FlipperWebviewClient extends FlipperClient {
bridge?.FlipperWebviewBridge.stop(); bridge?.FlipperWebviewBridge.stop();
}; };
sendData = (plugin: FlipperPluginID, method: FlipperMethodID, data: any) => { sendData = (data: any) => {
const bridge = (window as any).FlipperWebviewBridge; const bridge = (window as any).FlipperWebviewBridge;
bridge && bridge.sendFlipperObject(plugin, method, JSON.stringify(data)); bridge && bridge.sendFlipperObject(data);
};
subscribe = (
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (msg: any) => void,
) => {
this._subscriptions.set(plugin + method, handler);
}; };
isAvailable = () => { isAvailable = () => {
return (window as any).FlipperWebviewBridge != null; 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 { export function newWebviewClient(): FlipperClient {

View File

@@ -18,7 +18,6 @@
"dom", "dom",
"es2017" "es2017"
], ],
"composite": true,
"baseUrl": ".", "baseUrl": ".",
"allowJs": true "allowJs": true
}, },

File diff suppressed because it is too large Load Diff