Add generics to server add-on connection

Reviewed By: mweststrate

Differential Revision: D34307356

fbshipit-source-id: 27e61355a85995368ebb197c42d58f4145473567
This commit is contained in:
Andrey Goncharov
2022-02-28 03:50:34 -08:00
committed by Facebook GitHub Bot
parent 673bb9135e
commit 01a5f3da90
14 changed files with 129 additions and 46 deletions

View File

@@ -48,13 +48,25 @@ export type FlipperPluginReceiverRes =
| undefined | undefined
| void; | void;
export type FlipperPluginReceiver = ( export type FlipperPluginReceiver<T> = (
data: any, data: T,
) => FlipperPluginReceiverRes | Promise<FlipperPluginReceiverRes>; ) => FlipperPluginReceiverRes | Promise<FlipperPluginReceiverRes>;
export interface ServerAddOnPluginConnection { export type EventsContract = Record<string, any>;
send(method: string, params: unknown): void; export type MethodsContract = Record<string, (params: any) => Promise<any>>;
receive(method: string, receiver: FlipperPluginReceiver): void;
export interface ServerAddOnPluginConnection<
Events extends EventsContract,
Methods extends MethodsContract,
> {
send<T extends keyof Events & string>(
method: T,
...params: Events[T] extends never ? [] : [Events[T]]
): void;
receive<T extends keyof Methods & string>(
method: T,
receiver: FlipperPluginReceiver<Parameters<Methods[T]>[0]>,
): void;
} }
export interface FlipperServerForServerAddOn extends FlipperServer { export interface FlipperServerForServerAddOn extends FlipperServer {
@@ -65,7 +77,10 @@ export interface FlipperServerForServerAddOn extends FlipperServer {
} }
export type ServerAddOnCleanup = () => Promise<void>; export type ServerAddOnCleanup = () => Promise<void>;
export type ServerAddOn = ( export type ServerAddOn<
connection: ServerAddOnPluginConnection, Events extends EventsContract,
Methods extends MethodsContract,
> = (
connection: ServerAddOnPluginConnection<Events, Methods>,
{flipperServer}: {flipperServer: FlipperServerForServerAddOn}, {flipperServer}: {flipperServer: FlipperServerForServerAddOn},
) => Promise<ServerAddOnCleanup>; ) => Promise<ServerAddOnCleanup>;

View File

@@ -47,6 +47,7 @@ export {
getErrorFromErrorLike, getErrorFromErrorLike,
deserializeRemoteError, deserializeRemoteError,
} from './utils/errors'; } from './utils/errors';
export {createControlledPromise} from './utils/controlledPromise';
export * from './GK'; export * from './GK';
export * from './clientUtils'; export * from './clientUtils';
export * from './settings'; export * from './settings';

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
type Res<T> = {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
} & (
| {
state: 'pending';
promiseVal: undefined;
}
| {state: 'resolved'; promiseVal: T}
| {state: 'rejected'; promiseVal: unknown}
);
export const createControlledPromise = <T,>(): Res<T> => {
let resolve!: Res<T>['resolve'];
let reject!: Res<T>['reject'];
let state: 'pending' | 'resolved' | 'rejected' = 'pending';
let promiseVal: T | unknown | undefined;
const promise = new Promise<T>((resolveP, rejectP) => {
resolve = ((val) => {
state = 'resolved';
promiseVal = val;
resolveP(val as T | PromiseLike<T>);
}) as typeof resolve;
reject = (err) => {
state = 'rejected';
promiseVal = err;
rejectP(err);
};
});
return {
promise,
resolve,
reject,
state,
promiseVal,
} as Res<T>;
};

View File

@@ -52,6 +52,7 @@ test('Correct top level API exposed', () => {
"Tracked", "Tracked",
"TrackingScope", "TrackingScope",
"batch", "batch",
"createControlledPromise",
"createDataSource", "createDataSource",
"createState", "createState",
"createTablePlugin", "createTablePlugin",

View File

@@ -146,6 +146,7 @@ export const TestUtils = TestUtilites;
export { export {
sleep, sleep,
timeout, timeout,
createControlledPromise,
DeviceOS, DeviceOS,
DeviceType, DeviceType,
DeviceLogEntry, DeviceLogEntry,

View File

@@ -8,12 +8,7 @@
*/ */
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import { import {BasePluginInstance, BasePluginClient} from './PluginBase';
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 {
@@ -22,6 +17,8 @@ import {
DeviceLogEntry, DeviceLogEntry,
CrashLog, CrashLog,
ServerAddOnControls, ServerAddOnControls,
EventsContract,
MethodsContract,
} from 'flipper-common'; } from 'flipper-common';
export type DeviceLogListener = (entry: DeviceLogEntry) => void; export type DeviceLogListener = (entry: DeviceLogEntry) => void;

View File

@@ -8,17 +8,16 @@
*/ */
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import { import {BasePluginInstance, BasePluginClient} from './PluginBase';
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,
EventsContract,
MethodsContract,
} from 'flipper-common';
type PreventIntersectionWith<Contract extends Record<string, any>> = { type PreventIntersectionWith<Contract extends Record<string, any>> = {
[Key in keyof Contract]?: never; [Key in keyof Contract]?: never;

View File

@@ -18,10 +18,11 @@ 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'; import {
EventsContract,
export type EventsContract = Record<string, any>; MethodsContract,
export type MethodsContract = Record<string, (params: any) => Promise<any>>; ServerAddOnControls,
} from 'flipper-common';
type StateExportHandler<T = any> = ( type StateExportHandler<T = any> = (
idler: Idler, idler: Idler,

View File

@@ -23,9 +23,9 @@ export type ServerAddOnModuleToDesktopConnectionEvents = {
export class ServerAddOnModuleToDesktopConnection export class ServerAddOnModuleToDesktopConnection
extends EventEmitter extends EventEmitter
implements ServerAddOnPluginConnection implements ServerAddOnPluginConnection<any, any>
{ {
private subscriptions: Map<string, FlipperPluginReceiver> = new Map(); private subscriptions: Map<string, FlipperPluginReceiver<any>> = new Map();
constructor(private readonly pluginName: string) { constructor(private readonly pluginName: string) {
super(); super();
@@ -44,7 +44,7 @@ export class ServerAddOnModuleToDesktopConnection
this.emit('message', message); this.emit('message', message);
} }
receive(method: string, receiver: FlipperPluginReceiver) { receive(method: string, receiver: FlipperPluginReceiver<any>) {
this.subscriptions.set(method, receiver); this.subscriptions.set(method, receiver);
} }

View File

@@ -7,13 +7,12 @@
* @format * @format
*/ */
import {ServerAddOnStartDetails} from 'flipper-common'; import {ServerAddOnStartDetails, createControlledPromise} from 'flipper-common';
import {loadServerAddOn} from '../loadServerAddOn'; import {loadServerAddOn} from '../loadServerAddOn';
import {PluginManager} from '../PluginManager'; import {PluginManager} from '../PluginManager';
import {ServerAddOnManager} from '../ServerAddManager'; import {ServerAddOnManager} from '../ServerAddManager';
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection'; import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
import { import {
createControlledPromise,
detailsBundled, detailsBundled,
detailsInstalled, detailsInstalled,
initialOwner, initialOwner,

View File

@@ -7,12 +7,11 @@
* @format * @format
*/ */
import {ServerAddOnStartDetails} from 'flipper-common'; import {ServerAddOnStartDetails, createControlledPromise} from 'flipper-common';
import {loadServerAddOn} from '../loadServerAddOn'; import {loadServerAddOn} from '../loadServerAddOn';
import {ServerAddOn} from '../ServerAddOn'; import {ServerAddOn} from '../ServerAddOn';
import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection'; import {ServerAddOnModuleToDesktopConnection} from '../ServerAddOnModuleToDesktopConnection';
import { import {
createControlledPromise,
detailsBundled, detailsBundled,
detailsInstalled, detailsInstalled,
initialOwner, initialOwner,

View File

@@ -17,17 +17,3 @@ export const detailsBundled: ServerAddOnStartDetails = {
export const detailsInstalled: ServerAddOnStartDetails = { export const detailsInstalled: ServerAddOnStartDetails = {
path: '/dagobar/', path: '/dagobar/',
}; };
export const createControlledPromise = <T,>() => {
let resolve!: (...res: T extends void ? [] : [T]) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((resolveP, rejectP) => {
resolve = resolveP as typeof resolve;
reject = rejectP;
});
return {
promise,
resolve,
reject,
};
};

View File

@@ -17,7 +17,7 @@ import {assertNotNull} from '../comms/Utilities';
import defaultPlugins from '../defaultPlugins'; import defaultPlugins from '../defaultPlugins';
interface ServerAddOnModule { interface ServerAddOnModule {
default: ServerAddOnFn; default: ServerAddOnFn<any, any>;
} }
export const loadServerAddOn = ( export const loadServerAddOn = (

View File

@@ -1174,6 +1174,43 @@ Usage: `safeStringify(dataStructure)`
Serialises the given data structure using `JSON.stringify`, but doesn't throw if the processes failed, but rather returns a `<unserializable ...>` string. Serialises the given data structure using `JSON.stringify`, but doesn't throw if the processes failed, but rather returns a `<unserializable ...>` string.
### createControlledPromise
Creates a promise and functions to resolve/reject it externally. Alsoprovides its current state.
Returns:
```ts
// When the promise is pending
type Res<T> = {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'pending';
promiseVal: undefined;
} | {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'resolved';
// Resolved value
promiseVal: T;
} | {
promise: Promise<T>;
resolve: (...res: T extends void ? [] : [T]) => void;
reject: (reason: unknown) => void;
state: 'rejected';
// Rejection reason
promiseVal: unknown;
}
```
Usage:
```js
const controllerPromise = createControlledPromise()
someService.on('event', (val) => controllerPromise.resolve(val))
await controllerPromise.promise
```
## TestUtils ## TestUtils
The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins. The object `TestUtils` as exposed from `flipper-plugin` exposes utilities to write unit tests for Sandy plugins.