Make sure plugins can serialize and deserialize

Summary:
This plugin adds serialization capabilities to Sandy plugins buy setting the a `persist: <key>` flag. This shouldn't be used for state that is unserializable, too big, too sensitive, or irrelevant during export / import.

Using an explicit `persist` flag is done to make plugins robust to changes over time; as long as the key is kept the same, state variables can be renamed and reordered without breaking the import / export format. Also it allows us to detect some changes in the import / export format and warn about it.

Alternative designs considered but not implemented would be:
1. requiring the user to explicitly return the state from the factory (e.g. `const todos = createState([]); return { todos }`,
2. or construct the state from client (e.g. `const todos = client.createState([])`)
3. enable persistence by default, and store states in the order the states were created (much like useState in React). This was implemented in the first versions of this diff, but as pointed out in the discussions, this is too sensitive too (accidental) format changes, as the storage format would be quite implicit

A nice benefit of the current approach, especially compared with alternative approach 1, is that state being restored is immediately visible in the plugin factory. In other words, directly after initialization `const todos = createState([])`, the `todos.get()` is actually set to the state that is being restored, rather than having still the initial state which is only overridden rather. So this behaves very much like the `useState` hook in React.

Furthermore, in the future we could use the same `persist` key in combination with other options, such as `saveToLocalStorage`, in case some state acts as a 'preference' (T69989583).

`TestUtils.startPlugin` supports starting plugins with an initial state by using the optional `initialState` field

Actually wiring up the serialization and deserialization into the export / import functionality of Flipper is done in the next diff.

Reviewed By: jknoxville

Differential Revision: D22432770

fbshipit-source-id: 9a4849582c2f6f54d1e40f65a6cba73092c28fe8
This commit is contained in:
Michel Weststrate
2020-07-14 09:04:59 -07:00
committed by Facebook GitHub Bot
parent 9d57a667ef
commit 0e4a6d659b
7 changed files with 192 additions and 65 deletions

View File

@@ -9,6 +9,7 @@
import * as TestUtils from '../test-utils/test-utils';
import * as testPlugin from './TestPlugin';
import {createState} from '../state/atom';
test('it can start a plugin and lifecycle events', () => {
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
@@ -114,9 +115,81 @@ test('a plugin cannot send messages after being disconnected', async () => {
});
test('a plugin can receive messages', async () => {
const {instance, sendEvent} = TestUtils.startPlugin(testPlugin);
const {instance, sendEvent, exportState} = TestUtils.startPlugin(testPlugin);
expect(instance.state.get().count).toBe(0);
sendEvent('inc', {delta: 2});
expect(instance.state.get().count).toBe(2);
expect(exportState()).toMatchInlineSnapshot(`
Object {
"counter": Object {
"count": 2,
},
}
`);
});
test('plugins support non-serializable state', async () => {
const {exportState} = TestUtils.startPlugin({
plugin() {
const field1 = createState(true);
const field2 = createState(
{
test: 3,
},
{
persist: 'field2',
},
);
return {
field1,
field2,
};
},
Component() {
return null;
},
});
// states are serialized in creation order
expect(exportState()).toEqual({field2: {test: 3}});
});
test('plugins support restoring state', async () => {
const {exportState} = TestUtils.startPlugin(
{
plugin() {
const field1 = createState(1, {persist: 'field1'});
const field2 = createState(2);
const field3 = createState(3, {persist: 'field3'});
expect(field1.get()).toBe('a');
expect(field2.get()).toBe(2);
expect(field3.get()).toBe('b');
return {};
},
Component() {
return null;
},
},
{
initialState: {field1: 'a', field3: 'b'},
},
);
expect(exportState()).toEqual({field1: 'a', field3: 'b'});
});
test('plugins cannot use a persist key twice', async () => {
expect(() => {
TestUtils.startPlugin({
plugin() {
const field1 = createState(1, {persist: 'test'});
const field2 = createState(2, {persist: 'test'});
return {field1, field2};
},
Component() {
return null;
},
});
}).toThrowErrorMatchingInlineSnapshot(
`"Some other state is already persisting with key \\"test\\""`,
);
});