diff --git a/desktop/app/src/utils/__tests__/exportData.node.tsx b/desktop/app/src/utils/__tests__/exportData.node.tsx index d7466b5a8..bc0c3d793 100644 --- a/desktop/app/src/utils/__tests__/exportData.node.tsx +++ b/desktop/app/src/utils/__tests__/exportData.node.tsx @@ -1501,3 +1501,133 @@ test('Sandy device plugin with custom import', async () => { ?.instanceApi.counter.get(), ).toBe(2); }); + +test('Sandy plugins with complex data are imported / exported correctly', async () => { + const deviceplugin = new _SandyPluginDefinition( + TestUtils.createMockPluginDetails(), + { + plugin() { + const m = createState(new Map([['a', 1]]), {persist: 'map'}); + const s = createState(new Set([{x: 2}]), {persist: 'set'}); + const d = createState(new Date(1611913002865), {persist: 'date'}); + return { + m, + s, + d, + }; + }, + Component() { + return null; + }, + }, + ); + + const {store} = await renderMockFlipperWithPlugin(deviceplugin); + + const data = await exportStore(store); + expect(Object.values(data.exportStoreData.pluginStates2)).toMatchObject([ + { + TestPlugin: { + date: { + __flipper_object_type__: 'Date', + // no data asserted, since that is TZ sensitve + }, + map: { + __flipper_object_type__: 'Map', + data: [['a', 1]], + }, + set: { + __flipper_object_type__: 'Set', + data: [ + { + x: 2, + }, + ], + }, + }, + }, + ]); + + await importDataToStore('unittest.json', data.serializedString, store); + const api = store + .getState() + .connections.clients[1].sandyPluginStates.get(deviceplugin.id)?.instanceApi; + expect(api.m.get()).toMatchInlineSnapshot(` + Map { + "a" => 1, + } + `); + expect(api.s.get()).toMatchInlineSnapshot(` + Set { + Object { + "x": 2, + }, + } + `); + expect(api.d.get()).toEqual(new Date(1611913002865)); +}); + +test('Sandy device plugins with complex data are imported / exported correctly', async () => { + const deviceplugin = new _SandyPluginDefinition( + TestUtils.createMockPluginDetails({id: 'deviceplugin'}), + { + supportsDevice() { + return true; + }, + devicePlugin() { + const m = createState(new Map([['a', 1]]), {persist: 'map'}); + const s = createState(new Set([{x: 2}]), {persist: 'set'}); + const d = createState(new Date(1611913002865), {persist: 'date'}); + return { + m, + s, + d, + }; + }, + Component() { + return null; + }, + }, + ); + + const {store} = await renderMockFlipperWithPlugin(deviceplugin); + + const data = await exportStore(store); + expect(data.exportStoreData.device?.pluginStates).toMatchObject({ + deviceplugin: { + date: { + __flipper_object_type__: 'Date', + // no data asserted, since that is TZ sensitve + }, + map: { + __flipper_object_type__: 'Map', + data: [['a', 1]], + }, + set: { + __flipper_object_type__: 'Set', + data: [ + { + x: 2, + }, + ], + }, + }, + }); + await importDataToStore('unittest.json', data.serializedString, store); + const api = store + .getState() + .connections.devices[1].sandyPluginStates.get(deviceplugin.id)?.instanceApi; + expect(api.m.get()).toMatchInlineSnapshot(` + Map { + "a" => 1, + } + `); + expect(api.s.get()).toMatchInlineSnapshot(` + Set { + Object { + "x": 2, + }, + } + `); + expect(api.d.get()).toEqual(new Date(1611913002865)); +}); diff --git a/desktop/app/src/utils/exportData.tsx b/desktop/app/src/utils/exportData.tsx index 3f0f66fe0..e4d94a447 100644 --- a/desktop/app/src/utils/exportData.tsx +++ b/desktop/app/src/utils/exportData.tsx @@ -49,6 +49,7 @@ import {getPluginTitle} from './pluginUtils'; import {capture} from './screenshot'; import {uploadFlipperMedia} from '../fb-stubs/user'; import {Idler} from 'flipper-plugin'; +import {deserializeObject, makeObjectSerializable} from './serialization'; export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace'; export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace'; @@ -266,9 +267,14 @@ async function exportSandyPluginStates( if (!res[client.id]) { res[client.id] = {}; } - res[client.id][pluginId] = await client.sandyPluginStates - .get(pluginId)! - .exportState(idler, statusUpdate); + res[client.id][pluginId] = await makeObjectSerializable( + await client.sandyPluginStates + .get(pluginId)! + .exportState(idler, statusUpdate), + idler, + statusUpdate, + 'Serializing plugin: ' + pluginId, + ); } } return res; @@ -453,7 +459,12 @@ export async function processStore( idler, ); - const devicePluginStates = await device.exportState(idler, statusUpdate); + const devicePluginStates = await makeObjectSerializable( + await device.exportState(idler, statusUpdate), + idler, + statusUpdate, + 'Serializing device plugins', + ); statusUpdate('Uploading screenshot...'); const deviceScreenshotLink = @@ -781,20 +792,9 @@ export function importDataToStore(source: string, data: string, store: Store) { source, supportRequestDetails, }); - const devices = store.getState().connections.devices; - const matchedDevices = devices.filter( - (availableDevice) => availableDevice.serial === serial, - ); - if (matchedDevices.length > 0) { - store.dispatch({ - type: 'SELECT_DEVICE', - payload: matchedDevices[0], - }); - return; - } archivedDevice.loadDevicePlugins( store.getState().plugins.devicePlugins, - device.pluginStates, + deserializeObject(device.pluginStates), ); store.dispatch({ type: 'REGISTER_DEVICE', @@ -823,7 +823,9 @@ export function importDataToStore(source: string, data: string, store: Store) { }); clients.forEach((client: {id: string; query: ClientQuery}) => { - const sandyPluginStates = json.pluginStates2[client.id] || {}; + const sandyPluginStates = deserializeObject( + json.pluginStates2[client.id] || {}, + ); const clientPlugins: Array = [ ...keys .filter((key) => { diff --git a/desktop/plugins/logs/__tests__/index.node.ts b/desktop/plugins/logs/__tests__/index.node.ts index d93d1be7e..8721724c2 100644 --- a/desktop/plugins/logs/__tests__/index.node.ts +++ b/desktop/plugins/logs/__tests__/index.node.ts @@ -121,7 +121,7 @@ test('export / import plugin does work', async () => { "logs": Array [ Object { "app": "X", - "date": 1611854112859, + "date": 2021-01-28T17:15:12.859Z, "message": "test1", "pid": 0, "tag": "test", @@ -130,7 +130,7 @@ test('export / import plugin does work', async () => { }, Object { "app": "Y", - "date": 1611854117859, + "date": 2021-01-28T17:15:17.859Z, "message": "test2", "pid": 2, "tag": "test", diff --git a/desktop/plugins/logs/index.tsx b/desktop/plugins/logs/index.tsx index c220effa6..033671ad1 100644 --- a/desktop/plugins/logs/index.tsx +++ b/desktop/plugins/logs/index.tsx @@ -321,7 +321,7 @@ export function supportsDevice(device: Device) { } type ExportedState = { - logs: (Omit & {date: number})[]; + logs: DeviceLogEntry[]; }; export function devicePlugin(client: DevicePluginClient) { @@ -346,24 +346,13 @@ export function devicePlugin(client: DevicePluginClient) { logs: entries .get() .slice(-10000) - .map((e) => ({ - ...e.entry, - date: e.entry.date.getTime(), - })), + .map((e) => e.entry), }; }); client.onImport((data) => { const imported = addEntriesToState( - data.logs.map((log) => - processEntry( - { - ...log, - date: new Date(log.date), - }, - '' + counter++, - ), - ), + data.logs.map((log) => processEntry(log, '' + counter++)), ); rows.set(imported.rows); entries.set(imported.entries); diff --git a/docs/extending/flipper-plugin.mdx b/docs/extending/flipper-plugin.mdx index ab2d1201f..c8c6c12f9 100644 --- a/docs/extending/flipper-plugin.mdx +++ b/docs/extending/flipper-plugin.mdx @@ -139,9 +139,11 @@ 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 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. 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. + #### `onImport` Usage: `client.onImport(callback: (snapshot) => void)` @@ -149,6 +151,29 @@ Usage: `client.onImport(callback: (snapshot) => void)` Overrides the default de-serialization behavior of this plugin. Use it to update the state based on the snapshot data. This hook will be called immediately after constructing the plugin instance. +To synchonize the types of the data between `onImport` and `onExport`, it is possible to provide a type as generic to both hooks. +The next example stores `counter` under the `count` field, and stores it as string rather than as number. + +```typescript +type SerializedState = { + count: string; +} + +export function plugin(client: PluginClient) { + const counter = createState(0); + + client.onExport(() => { + return { + count: "" + counter.get() + } + }) + + client.onImport((data) => { + counter.set(parseInt(data.count, 10)); + }); +} +``` + ### Methods #### `send` @@ -372,7 +397,9 @@ Its value should be treated as immutable and is initialized by default using the Optionally, `options` can be provided when creating state. Supported options: -* `persist: string`. If the `persist` value is set, this state container will be serialized when n Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container will load its initial value from the snapshot, rather than using the `initialValue` parameter. The `persist` key should be unique within the plugin and only be set if the state stored in this container is JSON serializable, and won't become unreasonably large. See also `exportState` and `initialState` in the [`TestUtils`](#testutils) section. +* `persist: string`. If the `persist` value is set, this state container will be serialized when n Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container will load its initial value from the snapshot, rather than using the `initialValue` parameter. The `persist` key should be unique within the plugin and only be set if the state stored in this container is serializable and won't become unreasonably large. See also `exportState` and `initialState` in the [`TestUtils`](#testutils) section. + +Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects, arrays or Date, Set or Map objects. #### The state atom object