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
640 lines
17 KiB
TypeScript
640 lines
17 KiB
TypeScript
/**
|
|
* 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 {FlipperDevicePlugin} from '../../plugin';
|
|
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
|
import {Store, Client, sleep} from '../../';
|
|
import {
|
|
selectPlugin,
|
|
starPlugin,
|
|
selectClient,
|
|
selectDevice,
|
|
} from '../../reducers/connections';
|
|
import {processMessageQueue} from '../messageQueue';
|
|
import {getPluginKey} from '../pluginUtils';
|
|
import {TestIdler} from '../Idler';
|
|
import {registerPlugins} from '../../reducers/plugins';
|
|
import {
|
|
SandyPluginDefinition,
|
|
TestUtils,
|
|
FlipperClient,
|
|
SandyPluginInstance,
|
|
} from 'flipper-plugin';
|
|
|
|
type Events = {
|
|
inc: {
|
|
delta?: number;
|
|
};
|
|
};
|
|
|
|
function plugin(client: FlipperClient<Events, {}>) {
|
|
const state = {
|
|
count: 0,
|
|
};
|
|
|
|
client.onMessage('inc', (params) => {
|
|
state.count += params.delta || 1;
|
|
});
|
|
|
|
return {
|
|
state,
|
|
};
|
|
}
|
|
|
|
const TestPlugin = new SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
plugin,
|
|
Component() {
|
|
return null;
|
|
},
|
|
},
|
|
);
|
|
|
|
function starTestPlugin(store: Store, client: Client) {
|
|
store.dispatch(
|
|
starPlugin({
|
|
plugin: TestPlugin,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function selectDeviceLogs(store: Store) {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'DeviceLogs',
|
|
selectedApp: null,
|
|
deepLinkPayload: null,
|
|
selectedDevice: store.getState().connections.selectedDevice!,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function selectTestPlugin(store: Store, client: Client) {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: TestPlugin.id,
|
|
selectedApp: client.query.app,
|
|
deepLinkPayload: null,
|
|
selectedDevice: store.getState().connections.selectedDevice!,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function getTestPluginState(
|
|
client: Client,
|
|
): ReturnType<typeof plugin>['state'] {
|
|
return client.sandyPluginStates.get(TestPlugin.id)!.instanceApi.state;
|
|
}
|
|
|
|
test('queue - events are processed immediately if plugin is selected', async () => {
|
|
const {store, client, sendMessage} = await createMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
);
|
|
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
|
|
sendMessage('noop', {});
|
|
sendMessage('noop', {});
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 4});
|
|
sendMessage('noop', {});
|
|
client.flushMessageBuffer();
|
|
expect(getTestPluginState(client)).toMatchInlineSnapshot(`
|
|
Object {
|
|
"count": 5,
|
|
}
|
|
`);
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(
|
|
`Object {}`,
|
|
);
|
|
});
|
|
|
|
test('queue - events are NOT processed immediately if plugin is NOT selected (but enabled)', async () => {
|
|
const {
|
|
store,
|
|
client,
|
|
sendMessage,
|
|
device,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
|
|
expect(getTestPluginState(client).count).toBe(0);
|
|
// the first message is already visible cause of the leading debounce
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
client.flushMessageBuffer();
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 3,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
await processMessageQueue(
|
|
client.sandyPluginStates.get(TestPlugin.id)!,
|
|
pluginKey,
|
|
store,
|
|
);
|
|
expect(getTestPluginState(client)).toEqual({
|
|
count: 6,
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// unstar. Messages don't arrive anymore
|
|
starTestPlugin(store, client);
|
|
// weird state...
|
|
selectTestPlugin(store, client);
|
|
sendMessage('inc', {delta: 3});
|
|
client.flushMessageBuffer();
|
|
// active, immediately processed
|
|
expect(client.sandyPluginStates.has(TestPlugin.id)).toBe(false);
|
|
|
|
// different plugin, and not starred, message will never arrive
|
|
selectDeviceLogs(store);
|
|
sendMessage('inc', {delta: 4});
|
|
client.flushMessageBuffer();
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// star again, plugin still not selected, message is queued
|
|
starTestPlugin(store, client);
|
|
sendMessage('inc', {delta: 5});
|
|
client.flushMessageBuffer();
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [{api: 'TestPlugin', method: 'inc', params: {delta: 5}}],
|
|
});
|
|
});
|
|
|
|
test('queue - events are queued for plugins that are favorite when app is not selected', async () => {
|
|
const {
|
|
client,
|
|
device,
|
|
store,
|
|
sendMessage,
|
|
createClient,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
|
|
|
|
const client2 = await createClient(device, 'TestApp2');
|
|
store.dispatch(selectClient(client2.id));
|
|
|
|
// Now we send a message to the second client, it should arrive,
|
|
// as the plugin was enabled already on the first client as well
|
|
sendMessage('inc', {delta: 2});
|
|
expect(getTestPluginState(client)).toEqual({count: 0});
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
test('queue - events are queued for plugins that are favorite when app is selected on different device', async () => {
|
|
const {
|
|
client,
|
|
store,
|
|
sendMessage,
|
|
createDevice,
|
|
createClient,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
|
|
|
|
const device2 = createDevice('serial2');
|
|
const client2 = await createClient(device2, client.query.app); // same app id
|
|
store.dispatch(selectDevice(device2));
|
|
store.dispatch(selectClient(client2.id));
|
|
|
|
// Now we send a message to the first and second client, it should arrive,
|
|
// as the plugin was enabled already on the first client as well
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3}, client2);
|
|
client.flushMessageBuffer();
|
|
client2.flushMessageBuffer();
|
|
expect(getTestPluginState(client)).toEqual({count: 0});
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
],
|
|
"TestApp#Android#MockAndroidDevice#serial2#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 3,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
test('queue - events processing will be paused', async () => {
|
|
const {
|
|
client,
|
|
device,
|
|
store,
|
|
sendMessage,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 3});
|
|
sendMessage('inc', {delta: 5});
|
|
client.flushMessageBuffer();
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
// controlled idler will signal and and off that idling is needed
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
client.sandyPluginStates.get(TestPlugin.id)!,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
expect(getTestPluginState(client)).toEqual({
|
|
count: 4,
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [{api: 'TestPlugin', method: 'inc', params: {delta: 5}}],
|
|
});
|
|
|
|
await idler.next();
|
|
expect(getTestPluginState(client)).toEqual({
|
|
count: 9,
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue).toEqual({
|
|
[pluginKey]: [],
|
|
});
|
|
|
|
// don't idle anymore
|
|
idler.run();
|
|
await p;
|
|
});
|
|
|
|
test('queue - messages that arrive during processing will be queued', async () => {
|
|
const {
|
|
client,
|
|
device,
|
|
store,
|
|
sendMessage,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
client.flushMessageBuffer();
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
client.sandyPluginStates.get(TestPlugin.id)!,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
// first message is consumed
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(getTestPluginState(client).count).toBe(3);
|
|
|
|
// Select the current plugin as active, still, messages should end up in the queue
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: TestPlugin.id,
|
|
selectedApp: client.id,
|
|
deepLinkPayload: null,
|
|
selectedDevice: device,
|
|
}),
|
|
);
|
|
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
|
|
|
|
sendMessage('inc', {delta: 4});
|
|
client.flushMessageBuffer();
|
|
// should not be processed yet
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
|
|
expect(getTestPluginState(client).count).toBe(3);
|
|
|
|
await idler.next();
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(0);
|
|
expect(getTestPluginState(client).count).toBe(10);
|
|
|
|
idler.run();
|
|
await p;
|
|
});
|
|
|
|
test('queue - processing can be cancelled', async () => {
|
|
const {
|
|
client,
|
|
device,
|
|
store,
|
|
sendMessage,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
sendMessage('inc', {delta: 4});
|
|
sendMessage('inc', {delta: 5});
|
|
client.flushMessageBuffer();
|
|
|
|
// process the message
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
const idler = new TestIdler();
|
|
|
|
const p = processMessageQueue(
|
|
client.sandyPluginStates.get(TestPlugin.id)!,
|
|
pluginKey,
|
|
store,
|
|
undefined,
|
|
idler,
|
|
);
|
|
|
|
// first message is consumed
|
|
await idler.next();
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(getTestPluginState(client).count).toBe(10);
|
|
|
|
idler.cancel();
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
|
|
expect(getTestPluginState(client).count).toBe(10);
|
|
await p;
|
|
});
|
|
|
|
test('queue - make sure resetting plugin state clears the message queue', async () => {
|
|
const {
|
|
client,
|
|
device,
|
|
store,
|
|
sendMessage,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
client.flushMessageBuffer();
|
|
|
|
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
|
|
|
|
store.dispatch({
|
|
type: 'CLEAR_PLUGIN_STATE',
|
|
payload: {clientId: client.id, devicePlugins: new Set()},
|
|
});
|
|
|
|
expect(store.getState().pluginMessageQueue[pluginKey]).toBe(undefined);
|
|
});
|
|
|
|
test('client - incoming messages are buffered and flushed together', async () => {
|
|
class StubDeviceLogs extends FlipperDevicePlugin<any, any, any> {
|
|
static id = 'DevicePlugin';
|
|
|
|
static supportsDevice() {
|
|
return true;
|
|
}
|
|
|
|
static persistedStateReducer = jest.fn();
|
|
}
|
|
|
|
const {
|
|
client,
|
|
store,
|
|
device,
|
|
sendMessage,
|
|
pluginKey,
|
|
} = await createMockFlipperWithPlugin(TestPlugin);
|
|
selectDeviceLogs(store);
|
|
|
|
store.dispatch(registerPlugins([StubDeviceLogs]));
|
|
sendMessage('inc', {});
|
|
sendMessage('inc', {delta: 2});
|
|
sendMessage('inc', {delta: 3});
|
|
|
|
// send a message to device logs
|
|
client.onMessage(
|
|
JSON.stringify({
|
|
method: 'execute',
|
|
params: {
|
|
api: 'DevicePlugin',
|
|
method: 'log',
|
|
params: {line: 'suff'},
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
|
|
expect(getTestPluginState(client).count).toBe(0);
|
|
// the first message is already visible cause of the leading debounce
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
expect(client.messageBuffer).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Object {
|
|
"messages": Array [
|
|
Object {
|
|
"api": "DevicePlugin",
|
|
"method": "log",
|
|
"params": Object {
|
|
"line": "suff",
|
|
},
|
|
},
|
|
],
|
|
"plugin": [Function],
|
|
},
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
|
|
"messages": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 3,
|
|
},
|
|
},
|
|
],
|
|
"plugin": undefined,
|
|
},
|
|
}
|
|
`);
|
|
expect(client.messageBuffer[pluginKey].plugin).toBeInstanceOf(
|
|
SandyPluginInstance,
|
|
);
|
|
|
|
await sleep(500);
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [
|
|
Object {
|
|
"api": "DevicePlugin",
|
|
"method": "log",
|
|
"params": Object {
|
|
"line": "suff",
|
|
},
|
|
},
|
|
],
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 3,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
expect(client.messageBuffer).toMatchInlineSnapshot(`Object {}`);
|
|
expect(StubDeviceLogs.persistedStateReducer.mock.calls).toMatchInlineSnapshot(
|
|
`Array []`,
|
|
);
|
|
|
|
// tigger processing the queue
|
|
const pluginKeyDevice = getPluginKey(client.id, device, StubDeviceLogs.id);
|
|
await processMessageQueue(StubDeviceLogs, pluginKeyDevice, store);
|
|
|
|
expect(StubDeviceLogs.persistedStateReducer.mock.calls)
|
|
.toMatchInlineSnapshot(`
|
|
Array [
|
|
Array [
|
|
Object {},
|
|
"log",
|
|
Object {
|
|
"line": "suff",
|
|
},
|
|
],
|
|
]
|
|
`);
|
|
|
|
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
|
|
Object {
|
|
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [],
|
|
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 2,
|
|
},
|
|
},
|
|
Object {
|
|
"api": "TestPlugin",
|
|
"method": "inc",
|
|
"params": Object {
|
|
"delta": 3,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
});
|