serialize Sandy plugins with serialization utils to support Date/Set/Map
Summary: Unlike non-sandy plugins, non-sandy plugins weren't serialized using our serialization utility yet. This diff addresses that, meaning that users don't have to bother about how to serialize maps, sets and dates. Unlike the old fashioned plugins, the `makeObjectSerialize` utility is used, rather than `serialize`. This normalizes the objects, but doesn't serialize them, which is done at the end of the export data process anyway for the whole tree. This avoids creating a double JSON serialization which is fully of ugly escape characters. This makes the onImport / onExport definition of the logs plugin nicer. Also improved the docs. Reviewed By: nikoant Differential Revision: D26146421 fbshipit-source-id: 6abfb6ee2e3312e2a13a11832ff103dc62fd844c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
e614993558
commit
594fa4d2bc
@@ -1501,3 +1501,133 @@ test('Sandy device plugin with custom import', async () => {
|
|||||||
?.instanceApi.counter.get(),
|
?.instanceApi.counter.get(),
|
||||||
).toBe(2);
|
).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));
|
||||||
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {getPluginTitle} from './pluginUtils';
|
|||||||
import {capture} from './screenshot';
|
import {capture} from './screenshot';
|
||||||
import {uploadFlipperMedia} from '../fb-stubs/user';
|
import {uploadFlipperMedia} from '../fb-stubs/user';
|
||||||
import {Idler} from 'flipper-plugin';
|
import {Idler} from 'flipper-plugin';
|
||||||
|
import {deserializeObject, makeObjectSerializable} from './serialization';
|
||||||
|
|
||||||
export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
|
export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
|
||||||
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
|
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
|
||||||
@@ -266,9 +267,14 @@ async function exportSandyPluginStates(
|
|||||||
if (!res[client.id]) {
|
if (!res[client.id]) {
|
||||||
res[client.id] = {};
|
res[client.id] = {};
|
||||||
}
|
}
|
||||||
res[client.id][pluginId] = await client.sandyPluginStates
|
res[client.id][pluginId] = await makeObjectSerializable(
|
||||||
.get(pluginId)!
|
await client.sandyPluginStates
|
||||||
.exportState(idler, statusUpdate);
|
.get(pluginId)!
|
||||||
|
.exportState(idler, statusUpdate),
|
||||||
|
idler,
|
||||||
|
statusUpdate,
|
||||||
|
'Serializing plugin: ' + pluginId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
@@ -453,7 +459,12 @@ export async function processStore(
|
|||||||
idler,
|
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...');
|
statusUpdate('Uploading screenshot...');
|
||||||
const deviceScreenshotLink =
|
const deviceScreenshotLink =
|
||||||
@@ -781,20 +792,9 @@ export function importDataToStore(source: string, data: string, store: Store) {
|
|||||||
source,
|
source,
|
||||||
supportRequestDetails,
|
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(
|
archivedDevice.loadDevicePlugins(
|
||||||
store.getState().plugins.devicePlugins,
|
store.getState().plugins.devicePlugins,
|
||||||
device.pluginStates,
|
deserializeObject(device.pluginStates),
|
||||||
);
|
);
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'REGISTER_DEVICE',
|
type: 'REGISTER_DEVICE',
|
||||||
@@ -823,7 +823,9 @@ export function importDataToStore(source: string, data: string, store: Store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
clients.forEach((client: {id: string; query: ClientQuery}) => {
|
clients.forEach((client: {id: string; query: ClientQuery}) => {
|
||||||
const sandyPluginStates = json.pluginStates2[client.id] || {};
|
const sandyPluginStates = deserializeObject(
|
||||||
|
json.pluginStates2[client.id] || {},
|
||||||
|
);
|
||||||
const clientPlugins: Array<string> = [
|
const clientPlugins: Array<string> = [
|
||||||
...keys
|
...keys
|
||||||
.filter((key) => {
|
.filter((key) => {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ test('export / import plugin does work', async () => {
|
|||||||
"logs": Array [
|
"logs": Array [
|
||||||
Object {
|
Object {
|
||||||
"app": "X",
|
"app": "X",
|
||||||
"date": 1611854112859,
|
"date": 2021-01-28T17:15:12.859Z,
|
||||||
"message": "test1",
|
"message": "test1",
|
||||||
"pid": 0,
|
"pid": 0,
|
||||||
"tag": "test",
|
"tag": "test",
|
||||||
@@ -130,7 +130,7 @@ test('export / import plugin does work', async () => {
|
|||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"app": "Y",
|
"app": "Y",
|
||||||
"date": 1611854117859,
|
"date": 2021-01-28T17:15:17.859Z,
|
||||||
"message": "test2",
|
"message": "test2",
|
||||||
"pid": 2,
|
"pid": 2,
|
||||||
"tag": "test",
|
"tag": "test",
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ export function supportsDevice(device: Device) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExportedState = {
|
type ExportedState = {
|
||||||
logs: (Omit<DeviceLogEntry, 'date'> & {date: number})[];
|
logs: DeviceLogEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function devicePlugin(client: DevicePluginClient) {
|
export function devicePlugin(client: DevicePluginClient) {
|
||||||
@@ -346,24 +346,13 @@ export function devicePlugin(client: DevicePluginClient) {
|
|||||||
logs: entries
|
logs: entries
|
||||||
.get()
|
.get()
|
||||||
.slice(-10000)
|
.slice(-10000)
|
||||||
.map((e) => ({
|
.map((e) => e.entry),
|
||||||
...e.entry,
|
|
||||||
date: e.entry.date.getTime(),
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
client.onImport<ExportedState>((data) => {
|
client.onImport<ExportedState>((data) => {
|
||||||
const imported = addEntriesToState(
|
const imported = addEntriesToState(
|
||||||
data.logs.map((log) =>
|
data.logs.map((log) => processEntry(log, '' + counter++)),
|
||||||
processEntry(
|
|
||||||
{
|
|
||||||
...log,
|
|
||||||
date: new Date(log.date),
|
|
||||||
},
|
|
||||||
'' + counter++,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
rows.set(imported.rows);
|
rows.set(imported.rows);
|
||||||
entries.set(imported.entries);
|
entries.set(imported.entries);
|
||||||
|
|||||||
@@ -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<state>)`
|
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.
|
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.
|
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`
|
#### `onImport`
|
||||||
|
|
||||||
Usage: `client.onImport(callback: (snapshot) => void)`
|
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.
|
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.
|
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<SerializedState>(() => {
|
||||||
|
return {
|
||||||
|
count: "" + counter.get()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
client.onImport<SerializedState>((data) => {
|
||||||
|
counter.set(parseInt(data.count, 10));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
#### `send`
|
#### `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:
|
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
|
#### The state atom object
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user