Support receiving messages in Sandy plugins

Summary: This diffs adds the capability to listen to messages in Sandy plugins. Although API wise it looks more like the old `this.subscribe`, semantically it behaves like the `persistedStateReducer`; messages are queued if the plugin is enabled but not active.

Reviewed By: nikoant

Differential Revision: D22282711

fbshipit-source-id: 885faa702fe779ac8d593c1d224b2be13e688d47
This commit is contained in:
Michel Weststrate
2020-07-01 08:58:40 -07:00
committed by Facebook GitHub Bot
parent 6c79408b0f
commit bb0c8e0df0
8 changed files with 841 additions and 59 deletions

View File

@@ -24,6 +24,9 @@ export function plugin(client: FlipperClient<Events, Methods>) {
const connectStub = jest.fn();
const disconnectStub = jest.fn();
const destroyStub = jest.fn();
const state = {
count: 0,
};
// TODO: add tests for sending and receiving data T68683442
// including typescript assertions
@@ -31,12 +34,23 @@ export function plugin(client: FlipperClient<Events, Methods>) {
client.onConnect(connectStub);
client.onDisconnect(disconnectStub);
client.onDestroy(destroyStub);
client.onMessage('inc', ({delta}) => {
state.count += delta;
});
function _unused_JustTypeChecks() {
// @ts-expect-error Argument of type '"bla"' is not assignable
client.send('bla', {});
// @ts-expect-error Argument of type '{ stuff: string; }' is not assignable to parameter of type
client.send('currentState', {stuff: 'nope'});
// @ts-expect-error
client.onMessage('stuff', (_params) => {
// noop
});
client.onMessage('inc', (params) => {
// @ts-expect-error
params.bla;
});
}
async function getCurrentState() {
@@ -48,6 +62,7 @@ export function plugin(client: FlipperClient<Events, Methods>) {
destroyStub,
disconnectStub,
getCurrentState,
state,
};
}

View File

@@ -93,3 +93,11 @@ test('a plugin cannot send messages after being disconnected', async () => {
}
expect(threw).toBeTruthy();
});
test('a plugin can receive messages', async () => {
const {instance, sendEvent} = TestUtils.startPlugin(testPlugin);
expect(instance.state.count).toBe(0);
sendEvent('inc', {delta: 2});
expect(instance.state.count).toBe(2);
});

View File

@@ -13,6 +13,11 @@ import {EventEmitter} from 'events';
type EventsContract = Record<string, any>;
type MethodsContract = Record<string, (params: any) => Promise<any>>;
type Message = {
method: string;
params?: any;
};
/**
* API available to a plugin factory
*/
@@ -47,6 +52,17 @@ export interface FlipperClient<
method: Method,
params: Parameters<Methods[Method]>[0],
): ReturnType<Methods[Method]>;
/**
* Subscribe to a specific event arriving from the device.
*
* Messages can only arrive if the plugin is enabled and connected.
* For background plugins messages will be batched and arrive the next time the plugin is connected.
*/
onMessage<Event extends keyof Events>(
event: Event,
callback: (params: Events[Event]) => void,
): void;
}
/**
@@ -73,6 +89,10 @@ export type FlipperPluginFactory<
export type FlipperPluginComponent = React.FC<{}>;
export class SandyPluginInstance {
static is(thing: any): thing is SandyPluginInstance {
return thing instanceof SandyPluginInstance;
}
/** base client provided by Flipper */
realClient: RealFlipperClient;
/** client that is bound to this instance */
@@ -111,6 +131,9 @@ export class SandyPluginInstance {
params as any,
);
},
onMessage: (event, callback) => {
this.events.on('event-' + event, callback);
},
};
this.instanceApi = definition.module.plugin(this.client);
}
@@ -156,6 +179,12 @@ export class SandyPluginInstance {
this.destroyed = true;
}
receiveMessages(messages: Message[]) {
messages.forEach((message) => {
this.events.emit('event-' + message.method, message.params);
});
}
toJSON() {
this.assertNotDestroyed();
// TODO: T68683449

View File

@@ -36,12 +36,19 @@ interface StartPluginOptions {
type ExtractClientType<Module extends FlipperPluginModule<any>> = Parameters<
Module['plugin']
>[0];
type ExtractMethodsType<
Module extends FlipperPluginModule<any>
> = ExtractClientType<Module> extends FlipperClient<any, infer Methods>
? Methods
: never;
type ExtractEventsType<
Module extends FlipperPluginModule<any>
> = ExtractClientType<Module> extends FlipperClient<infer Events, any>
? Events
: never;
interface StartPluginResult<Module extends FlipperPluginModule<any>> {
/**
* the instantiated plugin for this test
@@ -73,6 +80,24 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
params: Parameters<ExtractMethodsType<Module>[Method]>[0],
) => ReturnType<ExtractMethodsType<Module>[Method]>
>;
/**
* Send event to the plugin
*/
sendEvent<Event extends keyof ExtractEventsType<Module>>(
event: Event,
params: ExtractEventsType<Module>[Event],
): void;
/**
* Send events to the plugin
* The structure used here reflects events that can be recorded
* with the pluginRecorder
*/
sendEvents(
events: {
method: keyof ExtractEventsType<Module>;
params: any; // afaik we can't type this :-(
}[],
): void;
}
export function startPlugin<Module extends FlipperPluginModule<any>>(
@@ -107,16 +132,28 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
// we start connected
pluginInstance.connect();
return {
const res: StartPluginResult<Module> = {
module,
instance: pluginInstance.instanceApi,
connect: () => pluginInstance.connect(),
disconnect: () => pluginInstance.disconnect(),
destroy: () => pluginInstance.destroy(),
onSend: sendStub,
// @ts-ignore
_backingInstance: pluginInstance,
sendEvent: (event, params) => {
res.sendEvents([
{
method: event,
params,
},
]);
},
sendEvents: (messages) => {
pluginInstance.receiveMessages(messages as any);
},
};
// @ts-ignore
res._backingInstance = pluginInstance;
return res;
}
export function renderPlugin<Module extends FlipperPluginModule<any>>(