Added support for serialization
Summary: Make sure that DataSources can be serialized directly with a single setting, just like plain state atoms Reviewed By: nikoant Differential Revision: D26944954 fbshipit-source-id: 2b0d625d7d67f27a7c2e33dd7c4b534dfa4d3e82
This commit is contained in:
committed by
Facebook GitHub Bot
parent
dd4cf9cb4a
commit
66774c90c6
@@ -14,6 +14,7 @@ import {PluginClient} from '../plugin/Plugin';
|
|||||||
import {DevicePluginClient} from '../plugin/DevicePlugin';
|
import {DevicePluginClient} from '../plugin/DevicePlugin';
|
||||||
import mockConsole from 'jest-mock-console';
|
import mockConsole from 'jest-mock-console';
|
||||||
import {sleep} from '../utils/sleep';
|
import {sleep} from '../utils/sleep';
|
||||||
|
import {createDataSource} from '../state/DataSource';
|
||||||
|
|
||||||
test('it can start a plugin and lifecycle events', () => {
|
test('it can start a plugin and lifecycle events', () => {
|
||||||
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
|
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
|
||||||
@@ -557,3 +558,29 @@ test('GKs are supported', () => {
|
|||||||
expect(plugin.instance.isTest()).toBe(false);
|
expect(plugin.instance.isTest()).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('plugins can serialize dataSources', () => {
|
||||||
|
const {instance, exportState} = TestUtils.startPlugin(
|
||||||
|
{
|
||||||
|
plugin(_client: PluginClient) {
|
||||||
|
const ds = createDataSource([1, 2, 3], {persist: 'ds'});
|
||||||
|
return {ds};
|
||||||
|
},
|
||||||
|
Component() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialState: {
|
||||||
|
ds: [4, 5],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(instance.ds.records).toEqual([4, 5]);
|
||||||
|
instance.ds.shift(1);
|
||||||
|
instance.ds.append(6);
|
||||||
|
expect(exportState()).toEqual({
|
||||||
|
ds: [5, 6],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export {
|
|||||||
} from './utils/Logger';
|
} from './utils/Logger';
|
||||||
export {Idler} from './utils/Idler';
|
export {Idler} from './utils/Idler';
|
||||||
|
|
||||||
export {createDataSource, DataSource} from './state/datasource/DataSource';
|
export {createDataSource, DataSource} from './state/DataSource';
|
||||||
|
|
||||||
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
||||||
export {DataTableManager} from './ui/datatable/DataTableManager';
|
export {DataTableManager} from './ui/datatable/DataTableManager';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {Atom} from '../state/atom';
|
|
||||||
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {Device, RealFlipperDevice} from './DevicePlugin';
|
import {Device, RealFlipperDevice} from './DevicePlugin';
|
||||||
@@ -88,6 +87,26 @@ export function getCurrentPluginInstance(): typeof currentPluginInstance {
|
|||||||
return currentPluginInstance;
|
return currentPluginInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Persistable {
|
||||||
|
serialize(): any;
|
||||||
|
deserialize(value: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerStorageAtom(
|
||||||
|
key: string | undefined,
|
||||||
|
persistable: Persistable,
|
||||||
|
) {
|
||||||
|
if (key && getCurrentPluginInstance()) {
|
||||||
|
const {rootStates} = getCurrentPluginInstance()!;
|
||||||
|
if (rootStates[key]) {
|
||||||
|
throw new Error(
|
||||||
|
`Some other state is already persisting with key "${key}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rootStates[key] = persistable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class BasePluginInstance {
|
export abstract class BasePluginInstance {
|
||||||
/** generally available Flipper APIs */
|
/** generally available Flipper APIs */
|
||||||
readonly flipperLib: FlipperLib;
|
readonly flipperLib: FlipperLib;
|
||||||
@@ -106,7 +125,7 @@ export abstract class BasePluginInstance {
|
|||||||
initialStates?: Record<string, any>;
|
initialStates?: Record<string, any>;
|
||||||
|
|
||||||
// all the atoms that should be serialized when making an export / import
|
// all the atoms that should be serialized when making an export / import
|
||||||
readonly rootStates: Record<string, Atom<any>> = {};
|
readonly rootStates: Record<string, Persistable> = {};
|
||||||
// last seen deeplink
|
// last seen deeplink
|
||||||
lastDeeplink?: any;
|
lastDeeplink?: any;
|
||||||
// export handler
|
// export handler
|
||||||
@@ -178,7 +197,7 @@ export abstract class BasePluginInstance {
|
|||||||
} else {
|
} else {
|
||||||
for (const key in this.rootStates) {
|
for (const key in this.rootStates) {
|
||||||
if (key in this.initialStates) {
|
if (key in this.initialStates) {
|
||||||
this.rootStates[key].set(this.initialStates[key]);
|
this.rootStates[key].deserialize(this.initialStates[key]);
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`,
|
`Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`,
|
||||||
@@ -289,7 +308,10 @@ export abstract class BasePluginInstance {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
|
Object.entries(this.rootStates).map(([key, atom]) => [
|
||||||
|
key,
|
||||||
|
atom.serialize(),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
property,
|
property,
|
||||||
sortBy as lodashSort,
|
sortBy as lodashSort,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
import {Persistable, registerStorageAtom} from '../plugin/PluginBase';
|
||||||
|
|
||||||
// If the dataSource becomes to large, after how many records will we start to drop items?
|
// If the dataSource becomes to large, after how many records will we start to drop items?
|
||||||
const dropFactor = 0.1;
|
const dropFactor = 0.1;
|
||||||
@@ -91,7 +92,7 @@ export class DataSource<
|
|||||||
T,
|
T,
|
||||||
KEY extends keyof T = any,
|
KEY extends keyof T = any,
|
||||||
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>
|
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>
|
||||||
> {
|
> implements Persistable {
|
||||||
private nextId = 0;
|
private nextId = 0;
|
||||||
private _records: Entry<T>[] = [];
|
private _records: Entry<T>[] = [];
|
||||||
|
|
||||||
@@ -129,6 +130,17 @@ export class DataSource<
|
|||||||
return this._records.map(unwrap);
|
return this._records.map(unwrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.records;
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(value: any[]) {
|
||||||
|
this.clear();
|
||||||
|
value.forEach((record) => {
|
||||||
|
this.append(record);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a direct reference to the stored records as lookup map,
|
* returns a direct reference to the stored records as lookup map,
|
||||||
* based on the key attribute set.
|
* based on the key attribute set.
|
||||||
@@ -711,8 +723,21 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateDataSourceOptions<T, K extends keyof T> = {
|
type CreateDataSourceOptions<T, K extends keyof T> = {
|
||||||
|
/**
|
||||||
|
* If a key is set, the given field of the records is assumed to be unique,
|
||||||
|
* and it's value can be used to perform lookups and upserts.
|
||||||
|
*/
|
||||||
key?: K;
|
key?: K;
|
||||||
|
/**
|
||||||
|
* The maximum amount of records that this DataSource will store.
|
||||||
|
* If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones
|
||||||
|
*/
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
/**
|
||||||
|
* Should this state persist when exporting a plugin?
|
||||||
|
* If set, the dataSource will be saved / loaded under the key provided
|
||||||
|
*/
|
||||||
|
persist?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createDataSource<T, KEY extends keyof T = any>(
|
export function createDataSource<T, KEY extends keyof T = any>(
|
||||||
@@ -730,6 +755,7 @@ export function createDataSource<T, KEY extends keyof T>(
|
|||||||
if (options?.limit !== undefined) {
|
if (options?.limit !== undefined) {
|
||||||
ds.limit = options.limit;
|
ds.limit = options.limit;
|
||||||
}
|
}
|
||||||
|
registerStorageAtom(options?.persist, ds);
|
||||||
initialSet.forEach((value) => ds.append(value));
|
initialSet.forEach((value) => ds.append(value));
|
||||||
return ds;
|
return ds;
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {produce, Draft, enableMapSet} from 'immer';
|
import {produce, Draft, enableMapSet} from 'immer';
|
||||||
import {useState, useEffect} from 'react';
|
import {useState, useEffect} from 'react';
|
||||||
import {getCurrentPluginInstance} from '../plugin/PluginBase';
|
import {Persistable, registerStorageAtom} from '../plugin/PluginBase';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export type Atom<T> = {
|
|||||||
update(recipe: (draft: Draft<T>) => void): void;
|
update(recipe: (draft: Draft<T>) => void): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AtomValue<T> implements Atom<T> {
|
class AtomValue<T> implements Atom<T>, Persistable {
|
||||||
value: T;
|
value: T;
|
||||||
listeners: ((value: T) => void)[] = [];
|
listeners: ((value: T) => void)[] = [];
|
||||||
|
|
||||||
@@ -38,6 +38,14 @@ class AtomValue<T> implements Atom<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deserialize(value: T) {
|
||||||
|
this.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.get();
|
||||||
|
}
|
||||||
|
|
||||||
update(recipe: (draft: Draft<T>) => void) {
|
update(recipe: (draft: Draft<T>) => void) {
|
||||||
this.set(produce(this.value, recipe));
|
this.set(produce(this.value, recipe));
|
||||||
}
|
}
|
||||||
@@ -77,15 +85,7 @@ export function createState(
|
|||||||
options: StateOptions = {},
|
options: StateOptions = {},
|
||||||
): Atom<any> {
|
): Atom<any> {
|
||||||
const atom = new AtomValue(initialValue);
|
const atom = new AtomValue(initialValue);
|
||||||
if (getCurrentPluginInstance() && options.persist) {
|
registerStorageAtom(options.persist, atom);
|
||||||
const {rootStates} = getCurrentPluginInstance()!;
|
|
||||||
if (rootStates[options.persist]) {
|
|
||||||
throw new Error(
|
|
||||||
`Some other state is already persisting with key "${options.persist}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rootStates[options.persist] = atom;
|
|
||||||
}
|
|
||||||
return atom;
|
return atom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import React, {
|
|||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
import {useVirtual} from 'react-virtual';
|
import {useVirtual} from 'react-virtual';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import React, {
|
|||||||
useReducer,
|
useReducer,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
|
import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
import {Layout} from '../Layout';
|
import {Layout} from '../Layout';
|
||||||
import {TableHead} from './TableHead';
|
import {TableHead} from './TableHead';
|
||||||
import {Percentage} from '../../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import type {DataTableColumn} from './DataTable';
|
import type {DataTableColumn} from './DataTable';
|
||||||
import {Percentage} from '../../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import {MutableRefObject, Reducer} from 'react';
|
import {MutableRefObject, Reducer} from 'react';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
import {DataSourceVirtualizer} from './DataSourceRenderer';
|
import {DataSourceVirtualizer} from './DataSourceRenderer';
|
||||||
import produce, {immerable, original} from 'immer';
|
import produce, {immerable, original} from 'immer';
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import React from 'react';
|
|||||||
import {normalizeCellValue} from './TableRow';
|
import {normalizeCellValue} from './TableRow';
|
||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
import {DataTableColumn} from './DataTable';
|
import {DataTableColumn} from './DataTable';
|
||||||
import {DataSource} from '../../state/datasource/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
|
|
||||||
const {Item, SubMenu} = Menu;
|
const {Item, SubMenu} = Menu;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import {DataTable, DataTableColumn} from '../DataTable';
|
import {DataTable, DataTableColumn} from '../DataTable';
|
||||||
import {render, act} from '@testing-library/react';
|
import {render, act} from '@testing-library/react';
|
||||||
import {createDataSource} from '../../../state/datasource/DataSource';
|
import {createDataSource} from '../../../state/DataSource';
|
||||||
import {computeDataTableFilter, DataTableManager} from '../DataTableManager';
|
import {computeDataTableFilter, DataTableManager} from '../DataTableManager';
|
||||||
import {Button} from 'antd';
|
import {Button} from 'antd';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user