diff --git a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx index 4edd3abc0..78917e0f0 100644 --- a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx @@ -357,6 +357,27 @@ test('plugins can handle import errors', async () => { }); test('plugins can have custom export handler', async () => { + const {exportStateAsync} = TestUtils.startPlugin({ + plugin(client: PluginClient) { + const field1 = createState(0, {persist: 'field1'}); + + client.onExport(async () => { + await sleep(10); + return { + b: 3, + }; + }); + + return {field1}; + }, + Component() { + return null; + }, + }); + expect(await exportStateAsync()).toEqual({b: 3}); +}); + +test('plugins can have custom export handler that doesnt return', async () => { const {exportStateAsync} = TestUtils.startPlugin( { plugin(client: PluginClient) { @@ -364,9 +385,7 @@ test('plugins can have custom export handler', async () => { client.onExport(async () => { await sleep(10); - return { - b: 3, - }; + field1.set(field1.get() + 1); }); return {field1}; @@ -377,12 +396,11 @@ test('plugins can have custom export handler', async () => { }, { initialState: { - a: 1, - b: 2, + field1: 1, }, }, ); - expect(await exportStateAsync()).toEqual({b: 3}); + expect(await exportStateAsync()).toEqual({field1: 2}); }); test('plugins can receive deeplinks', async () => { diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index 176731cff..6362013b4 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -21,7 +21,7 @@ import {Logger} from '../utils/Logger'; type StateExportHandler = ( idler: Idler, onStatusMessage: (msg: string) => void, -) => Promise; +) => Promise; type StateImportHandler = (data: T) => void; export interface BasePluginClient { @@ -54,8 +54,11 @@ export interface BasePluginClient { /** * 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. + * + * If an object is returned from the handler, that will be taken as export. + * Otherwise, if nothing is returned, the handler will be run, and after the handler has finished the `persist` keys of the different states will be used as export basis. */ - onExport(exporter: StateExportHandler): void; + onExport(exporter: StateExportHandler): void; /** * Triggered directly after the plugin instance was created, if the plugin is being restored from a snapshot. @@ -350,6 +353,10 @@ export abstract class BasePluginInstance { 'Cannot export sync a plugin that does have an export handler', ); } + return this.serializeRootStates(); + } + + private serializeRootStates() { return Object.fromEntries( Object.entries(this.rootStates).map(([key, atom]) => [ key, @@ -363,9 +370,13 @@ export abstract class BasePluginInstance { onStatusMessage: (msg: string) => void, ): Promise> { if (this.exportHandler) { - return await this.exportHandler(idler, onStatusMessage); + const result = await this.exportHandler(idler, onStatusMessage); + if (result !== undefined) { + return result; + } + // intentional fall-through, the export handler merely updated the state, but prefers the default export format } - return this.exportStateSync(); + return this.serializeRootStates(); } isPersistable(): boolean { diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index be9258d08..63c2ed3a2 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -149,11 +149,13 @@ Trigger when the users navigates to this plugin using a deeplink, either from an Usage: `client.onExport(callback: (idler, onStatusMessage) => Promise)` -Overrides the default serialization behavior of this plugin. Should return a promise with persistable state that is to be stored. +Overrides the default serialization behavior of this plugin. Should return a promise with persistable state that is to be stored, or nothing at all. This process is async, so it is possible to first fetch some additional state from the device. Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects. +If nothing is returned, the handler will be run, and after the handler has finished the `persist` keys of the different states will be used as export basis. + #### `onImport` Usage: `client.onImport(callback: (snapshot) => void)`