Add support for async / custom plugin export
Summary: Sandy plugins can now set up an `onExport` handler to enable customizing the export format of a plugin: `client.onExport(callback: (idler, onStatusMessage) => Promise<state>)` Import will be done in next diff Reviewed By: nikoant Differential Revision: D26124440 fbshipit-source-id: c787c79d929aa8fb484f15a9340d7c87545793cb
This commit is contained in:
committed by
Facebook GitHub Bot
parent
32bde8cace
commit
34c915a739
@@ -43,7 +43,7 @@ import {selectPlugin} from './reducers/connections';
|
||||
import {State as Store, MiddlewareAPI} from './reducers/index';
|
||||
import {activateMenuItems} from './MenuBar';
|
||||
import {Message} from './reducers/pluginMessageQueue';
|
||||
import {Idler} from './utils/Idler';
|
||||
import {IdlerImpl} from './utils/Idler';
|
||||
import {processMessageQueue} from './utils/messageQueue';
|
||||
import {ToggleButton, SmallText, Layout} from './ui';
|
||||
import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin';
|
||||
@@ -173,7 +173,7 @@ class PluginContainer extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
idler?: Idler;
|
||||
idler?: IdlerImpl;
|
||||
pluginBeingProcessed: string | null = null;
|
||||
|
||||
state = {
|
||||
@@ -237,7 +237,7 @@ class PluginContainer extends PureComponent<Props, State> {
|
||||
pendingMessages?.length
|
||||
) {
|
||||
const start = Date.now();
|
||||
this.idler = new Idler();
|
||||
this.idler = new IdlerImpl();
|
||||
processMessageQueue(
|
||||
isSandyPlugin(activePlugin)
|
||||
? target.sandyPluginStates.get(activePlugin.id)!
|
||||
|
||||
@@ -21,7 +21,6 @@ Object {
|
||||
"deviceType": "physical",
|
||||
"logs": Array [],
|
||||
"os": "Android",
|
||||
"pluginStates": Object {},
|
||||
"serial": "serial",
|
||||
"title": "MockAndroidDevice",
|
||||
},
|
||||
@@ -31,7 +30,6 @@ Object {
|
||||
"deviceType": "physical",
|
||||
"logs": Array [],
|
||||
"os": "Android",
|
||||
"pluginStates": Object {},
|
||||
"serial": "serial",
|
||||
"title": "MockAndroidDevice",
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import {reportPlatformFailures} from '../utils/metrics';
|
||||
import CancellableExportStatus from './CancellableExportStatus';
|
||||
import {performance} from 'perf_hooks';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {Idler} from '../utils/Idler';
|
||||
import {IdlerImpl} from '../utils/Idler';
|
||||
import {
|
||||
exportStoreToFile,
|
||||
EXPORT_FLIPPER_TRACE_EVENT,
|
||||
@@ -86,7 +86,7 @@ export default class ShareSheetExportFile extends Component<Props, State> {
|
||||
runInBackground: false,
|
||||
};
|
||||
|
||||
idler = new Idler();
|
||||
idler = new IdlerImpl();
|
||||
|
||||
dispatchAndUpdateToolBarStatus(msg: string) {
|
||||
this.store.dispatch(
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
setExportURL,
|
||||
} from '../reducers/application';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {Idler} from '../utils/Idler';
|
||||
import {IdlerImpl} from '../utils/Idler';
|
||||
import {
|
||||
shareFlipperData,
|
||||
DataExportResult,
|
||||
@@ -95,7 +95,7 @@ export default class ShareSheetExportUrl extends Component<Props, State> {
|
||||
return this.context.store;
|
||||
}
|
||||
|
||||
idler = new Idler();
|
||||
idler = new IdlerImpl();
|
||||
|
||||
dispatchAndUpdateToolBarStatus(msg: string) {
|
||||
this.store.dispatch(
|
||||
|
||||
@@ -24,7 +24,7 @@ import {List, Map as ImmutableMap} from 'immutable';
|
||||
import React from 'react';
|
||||
import {KeyboardActions} from './MenuBar';
|
||||
import {TableBodyRow} from './ui';
|
||||
import {Idler} from './utils/Idler';
|
||||
import {Idler} from 'flipper-plugin';
|
||||
|
||||
type ID = string;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
_SandyPluginDefinition,
|
||||
DeviceType,
|
||||
DeviceLogListener,
|
||||
Idler,
|
||||
} from 'flipper-plugin';
|
||||
import type {DevicePluginDefinition, DevicePluginMap} from '../plugin';
|
||||
import {getFlipperLibImplementation} from '../utils/flipperLibImplementation';
|
||||
@@ -91,22 +92,31 @@ export default class BaseDevice {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
toJSON(): DeviceExport {
|
||||
async exportState(
|
||||
idler: Idler,
|
||||
onStatusMessage: (msg: string) => void,
|
||||
): Promise<Record<string, any>> {
|
||||
const pluginStates: Record<string, any> = {};
|
||||
|
||||
for (const instance of this.sandyPluginStates.values()) {
|
||||
if (instance.isPersistable()) {
|
||||
pluginStates[instance.definition.id] = instance.exportState();
|
||||
pluginStates[instance.definition.id] = await instance.exportState(
|
||||
idler,
|
||||
onStatusMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return pluginStates;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
os: this.os,
|
||||
title: this.title,
|
||||
deviceType: this.deviceType,
|
||||
serial: this.serial,
|
||||
logs: this.getLogs(),
|
||||
pluginStates,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export {connect} from 'react-redux';
|
||||
export {selectPlugin, StaticView} from './reducers/connections';
|
||||
export {writeBufferToFile, bufferToBlob} from './utils/screenshot';
|
||||
export {getPluginKey, getPersistedState} from './utils/pluginUtils';
|
||||
export {Idler} from './utils/Idler';
|
||||
export {Idler} from 'flipper-plugin';
|
||||
export {Store, MiddlewareAPI, State as ReduxState} from './reducers/index';
|
||||
export {default as BaseDevice} from './devices/BaseDevice';
|
||||
export {DeviceLogEntry, LogLevel, DeviceLogListener} from 'flipper-plugin';
|
||||
|
||||
@@ -14,13 +14,12 @@ import {Store} from './reducers/index';
|
||||
import {ReactNode, Component} from 'react';
|
||||
import BaseDevice from './devices/BaseDevice';
|
||||
import {serialize, deserialize} from './utils/serialization';
|
||||
import {Idler} from './utils/Idler';
|
||||
import {StaticView} from './reducers/connections';
|
||||
import {State as ReduxState} from './reducers';
|
||||
import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue';
|
||||
import {ActivatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import {Settings} from './reducers/settings';
|
||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||
import {Idler, _SandyPluginDefinition} from 'flipper-plugin';
|
||||
|
||||
type Parameters = {[key: string]: any};
|
||||
|
||||
|
||||
@@ -9,15 +9,9 @@
|
||||
|
||||
import {CancelledPromiseError} from './errors';
|
||||
import {sleep} from './promiseTimeout';
|
||||
import {Idler} from 'flipper-plugin';
|
||||
|
||||
export interface BaseIdler {
|
||||
shouldIdle(): boolean;
|
||||
idle(): Promise<void>;
|
||||
cancel(): void;
|
||||
isCancelled(): boolean;
|
||||
}
|
||||
|
||||
export class Idler implements BaseIdler {
|
||||
export class IdlerImpl implements Idler {
|
||||
private lastIdle = performance.now();
|
||||
private kill = false;
|
||||
|
||||
@@ -57,12 +51,16 @@ export class Idler implements BaseIdler {
|
||||
}
|
||||
|
||||
// This smills like we should be using generators :)
|
||||
export class TestIdler implements BaseIdler {
|
||||
export class TestIdler implements Idler {
|
||||
private resolver?: () => void;
|
||||
private kill = false;
|
||||
private autoRun = false;
|
||||
private hasProgressed = false;
|
||||
|
||||
constructor(autorun = false) {
|
||||
this.autoRun = autorun;
|
||||
}
|
||||
|
||||
shouldIdle() {
|
||||
if (this.kill) {
|
||||
return true;
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Idler, TestIdler} from '../Idler.tsx';
|
||||
import {IdlerImpl, TestIdler} from '../Idler.tsx';
|
||||
import {sleep} from '../promiseTimeout.tsx';
|
||||
|
||||
test('Idler should interrupt', async () => {
|
||||
const idler = new Idler();
|
||||
const idler = new IdlerImpl();
|
||||
let i = 0;
|
||||
try {
|
||||
for (; i < 500; i++) {
|
||||
@@ -30,7 +30,7 @@ test('Idler should interrupt', async () => {
|
||||
});
|
||||
|
||||
test('Idler should want to idle', async () => {
|
||||
const idler = new Idler(100);
|
||||
const idler = new IdlerImpl(100);
|
||||
expect(idler.shouldIdle()).toBe(false);
|
||||
await sleep(10);
|
||||
expect(idler.shouldIdle()).toBe(false);
|
||||
|
||||
@@ -28,8 +28,16 @@ import {
|
||||
createState,
|
||||
PluginClient,
|
||||
DevicePluginClient,
|
||||
sleep,
|
||||
} from 'flipper-plugin';
|
||||
import {selectPlugin} from '../../reducers/connections';
|
||||
import {TestIdler} from '../Idler';
|
||||
|
||||
const testIdler = new TestIdler();
|
||||
|
||||
function testOnStatusMessage() {
|
||||
// emtpy stub
|
||||
}
|
||||
|
||||
class TestPlugin extends FlipperPlugin<any, any, any> {}
|
||||
TestPlugin.title = 'TestPlugin';
|
||||
@@ -1103,7 +1111,20 @@ const sandyTestPlugin = new _SandyPluginDefinition(
|
||||
draft.testCount -= 1;
|
||||
});
|
||||
});
|
||||
return {};
|
||||
return {
|
||||
enableCustomExport() {
|
||||
client.onExport(async (idler, onStatus) => {
|
||||
if (idler.shouldIdle()) {
|
||||
await idler.idle();
|
||||
}
|
||||
await sleep(100);
|
||||
onStatus('hi');
|
||||
return {
|
||||
customExport: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
Component() {
|
||||
return null;
|
||||
@@ -1140,6 +1161,39 @@ test('Sandy plugins are exported properly', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Sandy plugins with custom export are exported properly', async () => {
|
||||
const {client, sendMessage, store} = await renderMockFlipperWithPlugin(
|
||||
sandyTestPlugin,
|
||||
);
|
||||
|
||||
// We do select another plugin, to verify that pending message queues are indeed processed before exporting
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: 'DeviceLogs',
|
||||
selectedApp: client.id,
|
||||
deepLinkPayload: null,
|
||||
}),
|
||||
);
|
||||
|
||||
client.sandyPluginStates
|
||||
.get(sandyTestPlugin.id)
|
||||
?.instanceApi.enableCustomExport();
|
||||
|
||||
// Deliberately not using 'act' here, to verify that exportStore itself makes sure buffers are flushed first
|
||||
sendMessage('inc', {});
|
||||
sendMessage('inc', {});
|
||||
sendMessage('inc', {});
|
||||
|
||||
const storeExport = await exportStore(store);
|
||||
const serial = storeExport.exportStoreData.device!.serial;
|
||||
expect(serial).not.toBeFalsy();
|
||||
expect(storeExport.exportStoreData.pluginStates2).toEqual({
|
||||
[`TestApp#Android#MockAndroidDevice#${serial}`]: {
|
||||
TestPlugin: {customExport: true},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Sandy plugins are imported properly', async () => {
|
||||
const data = {
|
||||
clients: [
|
||||
@@ -1190,7 +1244,7 @@ test('Sandy plugins are imported properly', async () => {
|
||||
expect(client2).not.toBe(client);
|
||||
expect(client2.plugins).toEqual([TestPlugin.id]);
|
||||
|
||||
expect(client.sandyPluginStates.get(TestPlugin.id)!.exportState())
|
||||
expect(client.sandyPluginStates.get(TestPlugin.id)!.exportStateSync())
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"counter": 0,
|
||||
@@ -1199,7 +1253,7 @@ test('Sandy plugins are imported properly', async () => {
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(client2.sandyPluginStates.get(TestPlugin.id)!.exportState())
|
||||
expect(client2.sandyPluginStates.get(TestPlugin.id)!.exportStateSync())
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"counter": 3,
|
||||
@@ -1227,6 +1281,18 @@ const sandyDeviceTestPlugin = new _SandyPluginDefinition(
|
||||
});
|
||||
return {
|
||||
counter,
|
||||
enableCustomExport() {
|
||||
client.onExport(async (idler, onStatus) => {
|
||||
if (idler.shouldIdle()) {
|
||||
await idler.idle();
|
||||
}
|
||||
onStatus('hi');
|
||||
await sleep(100);
|
||||
return {
|
||||
customExport: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
Component() {
|
||||
@@ -1277,7 +1343,9 @@ test('Sandy device plugins are exported / imported properly', async () => {
|
||||
const {counter} = device2.sandyPluginStates.get(TestPlugin.id)?.instanceApi;
|
||||
counter.set(counter.get() + 1);
|
||||
|
||||
expect(device.toJSON().pluginStates[TestPlugin.id]).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
(await device.exportState(testIdler, testOnStatusMessage))[TestPlugin.id],
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"counter": 0,
|
||||
"otherState": Object {
|
||||
@@ -1285,21 +1353,30 @@ test('Sandy device plugins are exported / imported properly', async () => {
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(device2.toJSON()).toMatchInlineSnapshot(`
|
||||
expect(await device2.exportState(testIdler, testOnStatusMessage))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"deviceType": "archivedPhysical",
|
||||
"logs": Array [],
|
||||
"os": "Android",
|
||||
"pluginStates": Object {
|
||||
"TestPlugin": Object {
|
||||
"counter": 4,
|
||||
"otherState": Object {
|
||||
"testCount": -3,
|
||||
},
|
||||
},
|
||||
},
|
||||
"serial": "2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial",
|
||||
"title": "MockAndroidDevice",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('Sandy device plugins with custom export are export properly', async () => {
|
||||
const {device, store} = await renderMockFlipperWithPlugin(
|
||||
sandyDeviceTestPlugin,
|
||||
);
|
||||
|
||||
device.sandyPluginStates
|
||||
.get(sandyDeviceTestPlugin.id)
|
||||
?.instanceApi.enableCustomExport();
|
||||
|
||||
const storeExport = await exportStore(store);
|
||||
expect(storeExport.exportStoreData.device!.pluginStates).toEqual({
|
||||
[sandyDeviceTestPlugin.id]: {customExport: true},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {readCurrentRevision} from './packageMetadata';
|
||||
import {tryCatchReportPlatformFailures} from './metrics';
|
||||
import {promisify} from 'util';
|
||||
import promiseTimeout from './promiseTimeout';
|
||||
import {Idler} from './Idler';
|
||||
import {TestIdler} from './Idler';
|
||||
import {setStaticView} from '../reducers/connections';
|
||||
import {
|
||||
resetSupportFormV2State,
|
||||
@@ -48,6 +48,7 @@ import {processMessageQueue} from './messageQueue';
|
||||
import {getPluginTitle} from './pluginUtils';
|
||||
import {capture} from './screenshot';
|
||||
import {uploadFlipperMedia} from '../fb-stubs/user';
|
||||
import {Idler} from 'flipper-plugin';
|
||||
|
||||
export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
|
||||
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
|
||||
@@ -109,9 +110,11 @@ type AddSaltToDeviceSerialOptions = {
|
||||
clients: Array<ClientExport>;
|
||||
pluginStates: PluginStatesExportState;
|
||||
pluginStates2: SandyPluginStates;
|
||||
devicePluginStates: Record<string, any>;
|
||||
pluginNotification: Array<PluginNotification>;
|
||||
selectedPlugins: Array<string>;
|
||||
statusUpdate?: (msg: string) => void;
|
||||
statusUpdate: (msg: string) => void;
|
||||
idler: Idler;
|
||||
};
|
||||
|
||||
export function displayFetchMetadataErrors(
|
||||
@@ -251,20 +254,23 @@ const serializePluginStates = async (
|
||||
return pluginExportState;
|
||||
};
|
||||
|
||||
function exportSandyPluginStates(
|
||||
async function exportSandyPluginStates(
|
||||
pluginsToProcess: PluginsToProcess,
|
||||
): SandyPluginStates {
|
||||
idler: Idler,
|
||||
statusUpdate: (msg: string) => void,
|
||||
): Promise<SandyPluginStates> {
|
||||
const res: SandyPluginStates = {};
|
||||
pluginsToProcess.forEach(({pluginId, client, pluginClass}) => {
|
||||
for (const key in pluginsToProcess) {
|
||||
const {pluginId, client, pluginClass} = pluginsToProcess[key];
|
||||
if (isSandyPlugin(pluginClass) && client.sandyPluginStates.has(pluginId)) {
|
||||
if (!res[client.id]) {
|
||||
res[client.id] = {};
|
||||
}
|
||||
res[client.id][pluginId] = client.sandyPluginStates
|
||||
res[client.id][pluginId] = await client.sandyPluginStates
|
||||
.get(pluginId)!
|
||||
.exportState();
|
||||
.exportState(idler, statusUpdate);
|
||||
}
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -322,6 +328,8 @@ async function addSaltToDeviceSerial({
|
||||
statusUpdate,
|
||||
selectedPlugins,
|
||||
pluginStates2,
|
||||
devicePluginStates,
|
||||
idler,
|
||||
}: AddSaltToDeviceSerialOptions): Promise<ExportType> {
|
||||
const {serial} = device;
|
||||
const newSerial = salt + '-' + serial;
|
||||
@@ -379,7 +387,7 @@ async function addSaltToDeviceSerial({
|
||||
fileVersion: remote.app.getVersion(),
|
||||
flipperReleaseRevision: revision,
|
||||
clients: updatedClients,
|
||||
device: newDevice.toJSON(),
|
||||
device: {...newDevice.toJSON(), pluginStates: devicePluginStates},
|
||||
deviceScreenshot: deviceScreenshot,
|
||||
store: {
|
||||
pluginStates: updatedPluginStates,
|
||||
@@ -415,11 +423,14 @@ export async function processStore(
|
||||
selectedPlugins,
|
||||
statusUpdate,
|
||||
}: ProcessStoreOptions,
|
||||
idler?: Idler,
|
||||
idler: Idler = new TestIdler(true),
|
||||
): Promise<ExportType> {
|
||||
if (device) {
|
||||
const {serial} = device;
|
||||
statusUpdate && statusUpdate('Capturing screenshot...');
|
||||
if (!statusUpdate) {
|
||||
statusUpdate = () => {};
|
||||
}
|
||||
statusUpdate('Capturing screenshot...');
|
||||
const deviceScreenshot = await capture(device).catch((e) => {
|
||||
console.warn('Failed to capture device screenshot when exporting. ' + e);
|
||||
return null;
|
||||
@@ -449,7 +460,9 @@ export async function processStore(
|
||||
idler,
|
||||
);
|
||||
|
||||
statusUpdate && statusUpdate('Uploading screenshot...');
|
||||
const devicePluginStates = await device.exportState(idler, statusUpdate);
|
||||
|
||||
statusUpdate('Uploading screenshot...');
|
||||
const deviceScreenshotLink =
|
||||
deviceScreenshot &&
|
||||
(await uploadFlipperMedia(deviceScreenshot, 'Image').catch((e) => {
|
||||
@@ -467,6 +480,8 @@ export async function processStore(
|
||||
statusUpdate,
|
||||
selectedPlugins,
|
||||
pluginStates2,
|
||||
devicePluginStates,
|
||||
idler,
|
||||
});
|
||||
|
||||
return exportFlipperData;
|
||||
@@ -478,8 +493,8 @@ export async function fetchMetadata(
|
||||
pluginsToProcess: PluginsToProcess,
|
||||
pluginStates: PluginStatesState,
|
||||
state: ReduxState,
|
||||
statusUpdate?: (msg: string) => void,
|
||||
idler?: Idler,
|
||||
statusUpdate: (msg: string) => void,
|
||||
idler: Idler,
|
||||
): Promise<{
|
||||
pluginStates: PluginStatesState;
|
||||
errors: {[plugin: string]: Error} | null;
|
||||
@@ -626,8 +641,8 @@ export function determinePluginsToProcess(
|
||||
|
||||
async function getStoreExport(
|
||||
store: MiddlewareAPI,
|
||||
statusUpdate?: (msg: string) => void,
|
||||
idler?: Idler,
|
||||
statusUpdate: (msg: string) => void = () => {},
|
||||
idler: Idler,
|
||||
): Promise<{
|
||||
exportData: ExportType;
|
||||
fetchMetaDataErrors: {[plugin: string]: Error} | null;
|
||||
@@ -657,10 +672,8 @@ async function getStoreExport(
|
||||
);
|
||||
const newPluginState = metadata.pluginStates;
|
||||
|
||||
// TODO: support async export like fetchMetaData T68683476
|
||||
// TODO: support device plugins T70582933
|
||||
const pluginStates2 = pluginsToProcess
|
||||
? exportSandyPluginStates(pluginsToProcess)
|
||||
? await exportSandyPluginStates(pluginsToProcess, idler, statusUpdate)
|
||||
: {};
|
||||
|
||||
getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
|
||||
@@ -691,8 +704,8 @@ async function getStoreExport(
|
||||
export async function exportStore(
|
||||
store: MiddlewareAPI,
|
||||
includeSupportDetails?: boolean,
|
||||
idler?: Idler,
|
||||
statusUpdate?: (msg: string) => void,
|
||||
idler: Idler = new TestIdler(true),
|
||||
statusUpdate: (msg: string) => void = () => {},
|
||||
): Promise<{
|
||||
serializedString: string;
|
||||
fetchMetaDataErrors: {
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
Message,
|
||||
DEFAULT_MAX_QUEUE_SIZE,
|
||||
} from '../reducers/pluginMessageQueue';
|
||||
import {Idler, BaseIdler} from './Idler';
|
||||
import {IdlerImpl} from './Idler';
|
||||
import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections';
|
||||
import {deconstructPluginKey} from './clientUtils';
|
||||
import {defaultEnabledBackgroundPlugins} from './pluginUtils';
|
||||
import {batch, _SandyPluginInstance} from 'flipper-plugin';
|
||||
import {batch, Idler, _SandyPluginInstance} from 'flipper-plugin';
|
||||
import {addBackgroundStat} from './pluginStats';
|
||||
|
||||
function processMessageClassic(
|
||||
@@ -168,7 +168,7 @@ export async function processMessageQueue(
|
||||
pluginKey: string,
|
||||
store: MiddlewareAPI,
|
||||
progressCallback?: (progress: {current: number; total: number}) => void,
|
||||
idler: BaseIdler = new Idler(),
|
||||
idler: Idler = new IdlerImpl(),
|
||||
): Promise<boolean> {
|
||||
if (!_SandyPluginInstance.is(plugin) && !plugin.persistedStateReducer) {
|
||||
return true;
|
||||
|
||||
@@ -22,13 +22,13 @@ export const stateSanitizer = (state: State) => {
|
||||
return {
|
||||
...device.toJSON(),
|
||||
logs: [],
|
||||
};
|
||||
} as any;
|
||||
}),
|
||||
selectedDevice: selectedDevice
|
||||
? {
|
||||
? ({
|
||||
...selectedDevice.toJSON(),
|
||||
logs: [],
|
||||
}
|
||||
} as any)
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Idler} from './Idler';
|
||||
import {Idler} from 'flipper-plugin';
|
||||
|
||||
export async function serialize(
|
||||
obj: Object,
|
||||
idler?: Idler,
|
||||
|
||||
@@ -59,6 +59,7 @@ test('Correct top level API exposed', () => {
|
||||
"DeviceType",
|
||||
"Draft",
|
||||
"FlipperLib",
|
||||
"Idler",
|
||||
"LogLevel",
|
||||
"LogTypes",
|
||||
"Logger",
|
||||
|
||||
@@ -69,6 +69,7 @@ export {
|
||||
useLogger,
|
||||
_LoggerContext,
|
||||
} from './utils/Logger';
|
||||
export {Idler} from './utils/Idler';
|
||||
|
||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||
// but is the least pain for plugin authors.
|
||||
|
||||
@@ -14,6 +14,12 @@ import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
||||
import {FlipperLib} from './FlipperLib';
|
||||
import {Device, RealFlipperDevice} from './DevicePlugin';
|
||||
import {batched} from '../state/batch';
|
||||
import {Idler} from '../utils/Idler';
|
||||
|
||||
type StateExportHandler = (
|
||||
idler: Idler,
|
||||
onStatusMessage: (msg: string) => void,
|
||||
) => Promise<Record<string, any>>;
|
||||
|
||||
export interface BasePluginClient {
|
||||
readonly device: Device;
|
||||
@@ -38,6 +44,12 @@ export interface BasePluginClient {
|
||||
*/
|
||||
onDeepLink(cb: (deepLink: unknown) => void): void;
|
||||
|
||||
/**
|
||||
* Triggered when the current plugin is being exported and should create a snapshot of the state exported.
|
||||
* Overrides the default export behavior and ignores any 'persist' flags of state.
|
||||
*/
|
||||
onExport(exporter: StateExportHandler): void;
|
||||
|
||||
/**
|
||||
* Register menu entries in the Flipper toolbar
|
||||
*/
|
||||
@@ -88,6 +100,8 @@ export abstract class BasePluginInstance {
|
||||
rootStates: Record<string, Atom<any>> = {};
|
||||
// last seen deeplink
|
||||
lastDeeplink?: any;
|
||||
// export handler
|
||||
exportHandler?: StateExportHandler;
|
||||
|
||||
menuEntries: NormalizedMenuEntry[] = [];
|
||||
|
||||
@@ -145,6 +159,12 @@ export abstract class BasePluginInstance {
|
||||
onDestroy: (cb) => {
|
||||
this.events.on('destroy', batched(cb));
|
||||
},
|
||||
onExport: (cb) => {
|
||||
if (this.exportHandler) {
|
||||
throw new Error('onExport handler already set');
|
||||
}
|
||||
this.exportHandler = cb;
|
||||
},
|
||||
addMenuEntry: (...entries) => {
|
||||
for (const entry of entries) {
|
||||
const normalized = normalizeMenuEntry(entry);
|
||||
@@ -204,14 +224,30 @@ export abstract class BasePluginInstance {
|
||||
}
|
||||
}
|
||||
|
||||
exportState() {
|
||||
exportStateSync() {
|
||||
// This method is mainly intended for unit testing
|
||||
if (this.exportHandler) {
|
||||
throw new Error(
|
||||
'Cannot export sync a plugin that does have an export handler',
|
||||
);
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
|
||||
);
|
||||
}
|
||||
|
||||
async exportState(
|
||||
idler: Idler,
|
||||
onStatusMessage: (msg: string) => void,
|
||||
): Promise<Record<string, any>> {
|
||||
if (this.exportHandler) {
|
||||
return await this.exportHandler(idler, onStatusMessage);
|
||||
}
|
||||
return this.exportStateSync();
|
||||
}
|
||||
|
||||
isPersistable(): boolean {
|
||||
return Object.keys(this.rootStates).length > 0;
|
||||
return !!this.exportHandler || Object.keys(this.rootStates).length > 0;
|
||||
}
|
||||
|
||||
protected assertNotDestroyed() {
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import {BasePluginInstance} from '../plugin/PluginBase';
|
||||
import {FlipperLib} from '../plugin/FlipperLib';
|
||||
import {stubLogger} from '../utils/Logger';
|
||||
import {Idler} from '../utils/Idler';
|
||||
|
||||
type Renderer = RenderResult<typeof queries>;
|
||||
|
||||
@@ -95,9 +96,14 @@ interface BasePluginResult {
|
||||
triggerDeepLink(deeplink: unknown): void;
|
||||
|
||||
/**
|
||||
* Grab all the persistable state
|
||||
* Grab all the persistable state, but will ignore any onExport handler
|
||||
*/
|
||||
exportState(): any;
|
||||
exportState(): Record<string, any>;
|
||||
|
||||
/**
|
||||
* Grab all the persistable state, respecting onExport handlers
|
||||
*/
|
||||
exportStateAsync(): Promise<Record<string, any>>;
|
||||
|
||||
/**
|
||||
* Trigger menu entry by label
|
||||
@@ -367,7 +373,9 @@ function createBasePluginResult(
|
||||
flipperLib: pluginInstance.flipperLib,
|
||||
activate: () => pluginInstance.activate(),
|
||||
deactivate: () => pluginInstance.deactivate(),
|
||||
exportState: () => pluginInstance.exportState(),
|
||||
exportStateAsync: () =>
|
||||
pluginInstance.exportState(createStubIdler(), () => {}),
|
||||
exportState: () => pluginInstance.exportStateSync(),
|
||||
triggerDeepLink: (deepLink: unknown) => {
|
||||
pluginInstance.triggerDeepLink(deepLink);
|
||||
},
|
||||
@@ -422,3 +430,18 @@ function createMockDevice(options?: StartPluginOptions): RealFlipperDevice {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createStubIdler(): Idler {
|
||||
return {
|
||||
shouldIdle() {
|
||||
return false;
|
||||
},
|
||||
idle() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
cancel() {},
|
||||
isCancelled() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
15
desktop/flipper-plugin/src/utils/Idler.tsx
Normal file
15
desktop/flipper-plugin/src/utils/Idler.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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 interface Idler {
|
||||
shouldIdle(): boolean;
|
||||
idle(): Promise<void>;
|
||||
cancel(): void;
|
||||
isCancelled(): boolean;
|
||||
}
|
||||
@@ -135,6 +135,13 @@ Usage: `client.onDeepLink(callback: (payload: unknown) => void)`
|
||||
|
||||
Trigger when the users navigates to this plugin using a deeplink, either from an external `flipper://` plugin URL, or because the user was linked here from another plugin.
|
||||
|
||||
#### `onExport`
|
||||
|
||||
Usage: `client.onExport(callback: (idler, onStatusMessage) => Promise<state>)`
|
||||
|
||||
Overrides the default serialization behavior of this plugin. Should return a promise with state that is to be stored.
|
||||
This process is async, so it is possible to first fetch some additional state from the device.
|
||||
|
||||
### Methods
|
||||
|
||||
#### `send`
|
||||
@@ -277,6 +284,14 @@ See the similarly named event under [`PluginClient`](#pluginclient).
|
||||
|
||||
See the similarly named event under [`PluginClient`](#pluginclient).
|
||||
|
||||
#### `onExport`
|
||||
|
||||
See the similarly named event under [`PluginClient`](#pluginclient).
|
||||
|
||||
#### `onImport`
|
||||
|
||||
See the similarly named event under [`PluginClient`](#pluginclient).
|
||||
|
||||
### Methods
|
||||
|
||||
#### `addMenuEntry`
|
||||
|
||||
Reference in New Issue
Block a user