diff --git a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx index bcdebe749..76011f4af 100644 --- a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx @@ -14,6 +14,7 @@ import {PluginClient} from '../plugin/Plugin'; import {DevicePluginClient} from '../plugin/DevicePlugin'; import mockConsole from 'jest-mock-console'; import {sleep} from '../utils/sleep'; +import {createDataSource} from '../state/DataSource'; test('it can start a plugin and lifecycle events', () => { const {instance, ...p} = TestUtils.startPlugin(testPlugin); @@ -557,3 +558,29 @@ test('GKs are supported', () => { 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], + }); +}); diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 3f10397c9..03b38fa55 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -75,7 +75,7 @@ export { } from './utils/Logger'; 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 {DataTableManager} from './ui/datatable/DataTableManager'; diff --git a/desktop/flipper-plugin/src/plugin/PluginBase.tsx b/desktop/flipper-plugin/src/plugin/PluginBase.tsx index 9a4376e63..5dc8c1936 100644 --- a/desktop/flipper-plugin/src/plugin/PluginBase.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginBase.tsx @@ -9,7 +9,6 @@ import {SandyPluginDefinition} from './SandyPluginDefinition'; import {EventEmitter} from 'events'; -import {Atom} from '../state/atom'; import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry'; import {FlipperLib} from './FlipperLib'; import {Device, RealFlipperDevice} from './DevicePlugin'; @@ -88,6 +87,26 @@ export function getCurrentPluginInstance(): typeof 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 { /** generally available Flipper APIs */ readonly flipperLib: FlipperLib; @@ -106,7 +125,7 @@ export abstract class BasePluginInstance { initialStates?: Record; // all the atoms that should be serialized when making an export / import - readonly rootStates: Record> = {}; + readonly rootStates: Record = {}; // last seen deeplink lastDeeplink?: any; // export handler @@ -178,7 +197,7 @@ export abstract class BasePluginInstance { } else { for (const key in this.rootStates) { if (key in this.initialStates) { - this.rootStates[key].set(this.initialStates[key]); + this.rootStates[key].deserialize(this.initialStates[key]); } else { console.warn( `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( - Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]), + Object.entries(this.rootStates).map(([key, atom]) => [ + key, + atom.serialize(), + ]), ); } diff --git a/desktop/flipper-plugin/src/state/datasource/DataSource.tsx b/desktop/flipper-plugin/src/state/DataSource.tsx similarity index 96% rename from desktop/flipper-plugin/src/state/datasource/DataSource.tsx rename to desktop/flipper-plugin/src/state/DataSource.tsx index ff881b80f..986a760ea 100644 --- a/desktop/flipper-plugin/src/state/datasource/DataSource.tsx +++ b/desktop/flipper-plugin/src/state/DataSource.tsx @@ -13,6 +13,7 @@ import { property, sortBy as lodashSort, } from 'lodash'; +import {Persistable, registerStorageAtom} from '../plugin/PluginBase'; // If the dataSource becomes to large, after how many records will we start to drop items? const dropFactor = 0.1; @@ -91,7 +92,7 @@ export class DataSource< T, KEY extends keyof T = any, KEY_TYPE extends string | number | never = ExtractKeyType -> { +> implements Persistable { private nextId = 0; private _records: Entry[] = []; @@ -129,6 +130,17 @@ export class DataSource< 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, * based on the key attribute set. @@ -711,8 +723,21 @@ export class DataSource< } type CreateDataSourceOptions = { + /** + * 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; + /** + * 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; + /** + * 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( @@ -730,6 +755,7 @@ export function createDataSource( if (options?.limit !== undefined) { ds.limit = options.limit; } + registerStorageAtom(options?.persist, ds); initialSet.forEach((value) => ds.append(value)); return ds; } diff --git a/desktop/flipper-plugin/src/state/datasource/__tests__/datasource-basics.node.tsx b/desktop/flipper-plugin/src/state/__tests__/datasource-basics.node.tsx similarity index 100% rename from desktop/flipper-plugin/src/state/datasource/__tests__/datasource-basics.node.tsx rename to desktop/flipper-plugin/src/state/__tests__/datasource-basics.node.tsx diff --git a/desktop/flipper-plugin/src/state/datasource/__tests__/datasource-perf.node.tsx b/desktop/flipper-plugin/src/state/__tests__/datasource-perf.node.tsx similarity index 100% rename from desktop/flipper-plugin/src/state/datasource/__tests__/datasource-perf.node.tsx rename to desktop/flipper-plugin/src/state/__tests__/datasource-perf.node.tsx diff --git a/desktop/flipper-plugin/src/state/atom.tsx b/desktop/flipper-plugin/src/state/atom.tsx index da9ad078e..5efed5474 100644 --- a/desktop/flipper-plugin/src/state/atom.tsx +++ b/desktop/flipper-plugin/src/state/atom.tsx @@ -9,7 +9,7 @@ import {produce, Draft, enableMapSet} from 'immer'; import {useState, useEffect} from 'react'; -import {getCurrentPluginInstance} from '../plugin/PluginBase'; +import {Persistable, registerStorageAtom} from '../plugin/PluginBase'; enableMapSet(); @@ -19,7 +19,7 @@ export type Atom = { update(recipe: (draft: Draft) => void): void; }; -class AtomValue implements Atom { +class AtomValue implements Atom, Persistable { value: T; listeners: ((value: T) => void)[] = []; @@ -38,6 +38,14 @@ class AtomValue implements Atom { } } + deserialize(value: T) { + this.set(value); + } + + serialize() { + return this.get(); + } + update(recipe: (draft: Draft) => void) { this.set(produce(this.value, recipe)); } @@ -77,15 +85,7 @@ export function createState( options: StateOptions = {}, ): Atom { const atom = new AtomValue(initialValue); - if (getCurrentPluginInstance() && options.persist) { - const {rootStates} = getCurrentPluginInstance()!; - if (rootStates[options.persist]) { - throw new Error( - `Some other state is already persisting with key "${options.persist}"`, - ); - } - rootStates[options.persist] = atom; - } + registerStorageAtom(options.persist, atom); return atom; } diff --git a/desktop/flipper-plugin/src/ui/datatable/DataSourceRenderer.tsx b/desktop/flipper-plugin/src/ui/datatable/DataSourceRenderer.tsx index a0b5235ff..90c23f433 100644 --- a/desktop/flipper-plugin/src/ui/datatable/DataSourceRenderer.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/DataSourceRenderer.tsx @@ -16,7 +16,7 @@ import React, { useLayoutEffect, MutableRefObject, } from 'react'; -import {DataSource} from '../../state/datasource/DataSource'; +import {DataSource} from '../../state/DataSource'; import {useVirtual} from 'react-virtual'; import styled from '@emotion/styled'; diff --git a/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx b/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx index c2348f079..4ffd00446 100644 --- a/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/DataTable.tsx @@ -21,7 +21,7 @@ import React, { useReducer, } from 'react'; import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow'; -import {DataSource} from '../../state/datasource/DataSource'; +import {DataSource} from '../../state/DataSource'; import {Layout} from '../Layout'; import {TableHead} from './TableHead'; import {Percentage} from '../../utils/widthUtils'; diff --git a/desktop/flipper-plugin/src/ui/datatable/DataTableManager.tsx b/desktop/flipper-plugin/src/ui/datatable/DataTableManager.tsx index 08d5c21cd..af400f384 100644 --- a/desktop/flipper-plugin/src/ui/datatable/DataTableManager.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/DataTableManager.tsx @@ -10,7 +10,7 @@ import type {DataTableColumn} from './DataTable'; import {Percentage} from '../../utils/widthUtils'; import {MutableRefObject, Reducer} from 'react'; -import {DataSource} from '../../state/datasource/DataSource'; +import {DataSource} from '../../state/DataSource'; import {DataSourceVirtualizer} from './DataSourceRenderer'; import produce, {immerable, original} from 'immer'; diff --git a/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx b/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx index 2b0a60c04..659c6a29c 100644 --- a/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/TableContextMenu.tsx @@ -18,7 +18,7 @@ import React from 'react'; import {normalizeCellValue} from './TableRow'; import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib'; import {DataTableColumn} from './DataTable'; -import {DataSource} from '../../state/datasource/DataSource'; +import {DataSource} from '../../state/DataSource'; const {Item, SubMenu} = Menu; diff --git a/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx b/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx index be39d2232..806b7095b 100644 --- a/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx +++ b/desktop/flipper-plugin/src/ui/datatable/__tests__/DataTable.node.tsx @@ -10,7 +10,7 @@ import React, {createRef} from 'react'; import {DataTable, DataTableColumn} from '../DataTable'; import {render, act} from '@testing-library/react'; -import {createDataSource} from '../../../state/datasource/DataSource'; +import {createDataSource} from '../../../state/DataSource'; import {computeDataTableFilter, DataTableManager} from '../DataTableManager'; import {Button} from 'antd';