Type improvements

Summary: some type simplifications, that makes it easier to reuse data sources and helps type inference

Reviewed By: passy

Differential Revision: D28413380

fbshipit-source-id: 261a8b981bf18a00edc3075926bd668322e1c37d
This commit is contained in:
Michel Weststrate
2021-06-07 08:08:53 -07:00
committed by Facebook GitHub Bot
parent 9a2677fc24
commit bc647972e1
12 changed files with 80 additions and 76 deletions

View File

@@ -20,12 +20,6 @@ const defaultLimit = 100 * 1000;
// rather than search and remove the affected individual items // rather than search and remove the affected individual items
const shiftRebuildTreshold = 0.05; const shiftRebuildTreshold = 0.05;
export type ExtractKeyType<T, KEY extends keyof T> = T[KEY] extends string
? string
: T[KEY] extends number
? number
: never;
type AppendEvent<T> = { type AppendEvent<T> = {
type: 'append'; type: 'append';
entry: Entry<T>; entry: Entry<T>;
@@ -83,19 +77,45 @@ type OutputChange =
newCount: number; newCount: number;
}; };
export class DataSource< export type DataSourceOptions<T, K extends keyof T> = {
T = any, /**
KEY extends keyof T = any, * If a key is set, the given field of the records is assumed to be unique,
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>, * 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;
};
export function createDataSource<T, KEY extends keyof T = any>(
initialSet: readonly T[],
options?: DataSourceOptions<T, KEY>,
): DataSource<T>;
export function createDataSource<T>(initialSet?: readonly T[]): DataSource<T>;
export function createDataSource<T, KEY extends keyof T>(
initialSet: readonly T[] = [],
options?: DataSourceOptions<T, KEY>,
): DataSource<T> {
const ds = new DataSource<T>(options?.key);
if (options?.limit !== undefined) {
ds.limit = options.limit;
}
initialSet.forEach((value) => ds.append(value));
return ds as any;
}
export class DataSource<T> {
private nextId = 0; private nextId = 0;
private _records: Entry<T>[] = []; private _records: Entry<T>[] = [];
private _recordsById: Map<KEY_TYPE, T> = new Map(); private _recordsById: Map<string, T> = new Map();
/** /**
* @readonly * @readonly
*/ */
public keyAttribute: undefined | keyof T; public keyAttribute: keyof T | undefined;
private idToIndex: Map<KEY_TYPE, number> = new Map(); private idToIndex: Map<string, number> = new Map();
// if we shift the window, we increase shiftOffset to correct idToIndex results, rather than remapping all values // if we shift the window, we increase shiftOffset to correct idToIndex results, rather than remapping all values
private shiftOffset = 0; private shiftOffset = 0;
@@ -113,7 +133,7 @@ export class DataSource<
*/ */
public readonly view: DataSourceView<T>; public readonly view: DataSourceView<T>;
constructor(keyAttribute: KEY | undefined) { constructor(keyAttribute: keyof T | undefined) {
this.keyAttribute = keyAttribute; this.keyAttribute = keyAttribute;
this.view = new DataSourceView<T>(this); this.view = new DataSourceView<T>(this);
} }
@@ -134,22 +154,22 @@ export class DataSource<
return unwrap(this._records[index]); return unwrap(this._records[index]);
} }
public has(key: KEY_TYPE) { public has(key: string) {
this.assertKeySet(); this.assertKeySet();
return this._recordsById.has(key); return this._recordsById.has(key);
} }
public getById(key: KEY_TYPE) { public getById(key: string): T | undefined {
this.assertKeySet(); this.assertKeySet();
return this._recordsById.get(key); return this._recordsById.get(key);
} }
public keys(): IterableIterator<KEY_TYPE> { public keys(): IterableIterator<string> {
this.assertKeySet(); this.assertKeySet();
return this._recordsById.keys(); return this._recordsById.keys();
} }
public entries(): IterableIterator<[KEY_TYPE, T]> { public entries(): IterableIterator<[string, T]> {
this.assertKeySet(); this.assertKeySet();
return this._recordsById.entries(); return this._recordsById.entries();
} }
@@ -178,7 +198,7 @@ export class DataSource<
* Returns the index of a specific key in the *records* set. * Returns the index of a specific key in the *records* set.
* Returns -1 if the record wansn't found * Returns -1 if the record wansn't found
*/ */
public getIndexOfKey(key: KEY_TYPE): number { public getIndexOfKey(key: string): number {
this.assertKeySet(); this.assertKeySet();
const stored = this.idToIndex.get(key); const stored = this.idToIndex.get(key);
return stored === undefined ? -1 : stored + this.shiftOffset; return stored === undefined ? -1 : stored + this.shiftOffset;
@@ -302,7 +322,7 @@ export class DataSource<
* *
* Warning: this operation can be O(n) if a key is set * Warning: this operation can be O(n) if a key is set
*/ */
public deleteByKey(keyValue: KEY_TYPE): boolean { public deleteByKey(keyValue: string): boolean {
this.assertKeySet(); this.assertKeySet();
const index = this.getIndexOfKey(keyValue); const index = this.getIndexOfKey(keyValue);
if (index === -1) { if (index === -1) {
@@ -382,7 +402,7 @@ export class DataSource<
} }
} }
private getKey(value: T): KEY_TYPE; private getKey(value: T): string;
private getKey(value: any): any { private getKey(value: any): any {
this.assertKeySet(); this.assertKeySet();
const key = value[this.keyAttribute!]; const key = value[this.keyAttribute!];
@@ -392,7 +412,7 @@ export class DataSource<
throw new Error(`Invalid key value: '${key}'`); throw new Error(`Invalid key value: '${key}'`);
} }
private storeIndexOfKey(key: KEY_TYPE, index: number) { private storeIndexOfKey(key: string, index: number) {
// de-normalize the index, so that on later look ups its corrected again // de-normalize the index, so that on later look ups its corrected again
this.idToIndex.set(key, index - this.shiftOffset); this.idToIndex.set(key, index - this.shiftOffset);
} }
@@ -448,7 +468,7 @@ class DataSourceView<T> {
*/ */
private _output: Entry<T>[] = []; private _output: Entry<T>[] = [];
constructor(datasource: DataSource<T, any, any>) { constructor(datasource: DataSource<T>) {
this.datasource = datasource; this.datasource = datasource;
} }

View File

@@ -16,7 +16,7 @@ type DataSourceProps<T extends object, C> = {
/** /**
* The data source to render * The data source to render
*/ */
dataSource: DataSource<T, any, any>; dataSource: DataSource<T>;
/** /**
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized * additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
*/ */

View File

@@ -39,7 +39,7 @@ type DataSourceProps<T extends object, C> = {
/** /**
* The data source to render * The data source to render
*/ */
dataSource: DataSource<T, any, any>; dataSource: DataSource<T>;
/** /**
* Automatically scroll if the user is near the end? * Automatically scroll if the user is near the end?
*/ */

View File

@@ -7,10 +7,7 @@
* @format * @format
*/ */
// ok for now, should be factored if this becomes a stand-alone lib import {DataSource, createDataSource} from '../DataSource';
// eslint-disable-next-line
import {createDataSource} from '../../state/createDataSource';
import {DataSource} from '../DataSource';
type Todo = { type Todo = {
id: string; id: string;
@@ -487,7 +484,7 @@ test('clear', () => {
function testEvents<T>( function testEvents<T>(
initial: T[], initial: T[],
op: (ds: DataSource<T, any, any>) => void, op: (ds: DataSource<T>) => void,
key?: keyof T, key?: keyof T,
): any[] { ): any[] {
const ds = createDataSource<T>(initial, {key}); const ds = createDataSource<T>(initial, {key});

View File

@@ -7,10 +7,7 @@
* @format * @format
*/ */
// ok for now, should be factored if this becomes a stand-alone lib import {DataSource, createDataSource} from '../DataSource';
// eslint-disable-next-line
import {createDataSource} from '../../state/createDataSource';
import {DataSource} from '../DataSource';
type Todo = { type Todo = {
id: string; id: string;

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
export {DataSource, ExtractKeyType} from './DataSource'; export {DataSource, createDataSource, DataSourceOptions} from './DataSource';
export { export {
DataSourceRendererVirtual, DataSourceRendererVirtual,
DataSourceVirtualizer, DataSourceVirtualizer,

View File

@@ -1,6 +1,6 @@
{ {
"name": "flipper-data-source", "name": "flipper-data-source",
"version": "0.0.1", "version": "0.0.2",
"description": "Library to power streamig data visualisations", "description": "Library to power streamig data visualisations",
"repository": "https://github.com/facebook/flipper", "repository": "https://github.com/facebook/flipper",
"homepage": "https://github.com/facebook/flipper/blob/master/desktop/flipper-plugin/src/data-source/README.md", "homepage": "https://github.com/facebook/flipper/blob/master/desktop/flipper-plugin/src/data-source/README.md",

View File

@@ -7,20 +7,17 @@
* @format * @format
*/ */
import {DataSource, ExtractKeyType} from '../data-source/index'; import {
DataSource,
createDataSource as baseCreateDataSource,
DataSourceOptions as BaseDataSourceOptions,
} from '../data-source/index';
import {registerStorageAtom} from '../plugin/PluginBase'; import {registerStorageAtom} from '../plugin/PluginBase';
type CreateDataSourceOptions<T, K extends keyof T> = { type CreateDataSourceOptions<T, K extends keyof T> = BaseDataSourceOptions<
/** T,
* If a key is set, the given field of the records is assumed to be unique, K
* 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? * Should this state persist when exporting a plugin?
* If set, the dataSource will be saved / loaded under the key provided * If set, the dataSource will be saved / loaded under the key provided
@@ -29,21 +26,15 @@ type CreateDataSourceOptions<T, K extends keyof T> = {
}; };
export function createDataSource<T, KEY extends keyof T = any>( export function createDataSource<T, KEY extends keyof T = any>(
initialSet: T[], initialSet: readonly T[],
options: CreateDataSourceOptions<T, KEY>, options: CreateDataSourceOptions<T, KEY>,
): DataSource<T, KEY, ExtractKeyType<T, KEY>>; ): DataSource<T>;
export function createDataSource<T>( export function createDataSource<T>(initialSet?: readonly T[]): DataSource<T>;
initialSet?: T[],
): DataSource<T, never, never>;
export function createDataSource<T, KEY extends keyof T>( export function createDataSource<T, KEY extends keyof T>(
initialSet: T[] = [], initialSet: readonly T[] = [],
options?: CreateDataSourceOptions<T, KEY>, options?: CreateDataSourceOptions<T, KEY>,
): DataSource<T, any, any> { ): DataSource<T> {
const ds = new DataSource<T, KEY>(options?.key); const ds = baseCreateDataSource(initialSet, options);
if (options?.limit !== undefined) {
ds.limit = options.limit;
}
registerStorageAtom(options?.persist, ds); registerStorageAtom(options?.persist, ds);
initialSet.forEach((value) => ds.append(value));
return ds; return ds;
} }

View File

@@ -30,6 +30,7 @@ import styled from '@emotion/styled';
import {DataTableManager} from './data-table/DataTableManager'; import {DataTableManager} from './data-table/DataTableManager';
import {Atom, createState} from '../state/atom'; import {Atom, createState} from '../state/atom';
import {useAssertStableRef} from '../utils/useAssertStableRef'; import {useAssertStableRef} from '../utils/useAssertStableRef';
import {DataSource} from '../data-source';
const {Text} = Typography; const {Text} = Typography;
@@ -62,7 +63,7 @@ interface DataListBaseProps<T extends Item> {
/** /**
* Items to display. Per item at least a title and unique id should be provided * Items to display. Per item at least a title and unique id should be provided
*/ */
items: readonly Item[]; items: DataSource<Item> | readonly Item[];
/** /**
* Custom render function. By default the component will render the `title` in bold and description (if any) below it * Custom render function. By default the component will render the `title` in bold and description (if any) below it
*/ */
@@ -152,7 +153,8 @@ export const DataList: React.FC<DataListProps<any>> = function DataList<
<DataTable<any> <DataTable<any>
{...tableProps} {...tableProps}
tableManagerRef={tableManagerRef} tableManagerRef={tableManagerRef}
records={items} records={Array.isArray(items) ? items : undefined}
dataSource={Array.isArray(items) ? undefined : (items as any)}
recordsKey="id" recordsKey="id"
columns={dataListColumns} columns={dataListColumns}
onSelect={handleSelect} onSelect={handleSelect}

View File

@@ -51,6 +51,7 @@ import {Formatter} from '../DataFormatter';
import {usePluginInstance} from '../../plugin/PluginContext'; import {usePluginInstance} from '../../plugin/PluginContext';
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {useInUnitTest} from '../../utils/useInUnitTest'; import {useInUnitTest} from '../../utils/useInUnitTest';
import {createDataSource} from 'flipper-plugin/src/state/createDataSource';
interface DataTableBaseProps<T = any> { interface DataTableBaseProps<T = any> {
columns: DataTableColumn<T>[]; columns: DataTableColumn<T>[];
@@ -66,9 +67,7 @@ interface DataTableBaseProps<T = any> {
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it... tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
onCopyRows?(records: T[]): string; onCopyRows?(records: T[]): string;
onContextMenu?: (selection: undefined | T) => React.ReactElement; onContextMenu?: (selection: undefined | T) => React.ReactElement;
onRenderEmpty?: onRenderEmpty?: null | ((dataSource?: DataSource<T>) => React.ReactElement);
| null
| ((dataSource?: DataSource<T, any, any>) => React.ReactElement);
} }
export type ItemRenderer<T> = ( export type ItemRenderer<T> = (
@@ -79,7 +78,7 @@ export type ItemRenderer<T> = (
type DataTableInput<T = any> = type DataTableInput<T = any> =
| { | {
dataSource: DataSource<T, any, any>; dataSource: DataSource<T>;
records?: undefined; records?: undefined;
recordsKey?: undefined; recordsKey?: undefined;
} }
@@ -525,11 +524,9 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
return props.dataSource; return props.dataSource;
} }
if (props.records) { if (props.records) {
const [dataSource] = useState(() => { const [dataSource] = useState(() =>
const ds = new DataSource<T>(props.recordsKey); createDataSource(props.records, {key: props.recordsKey}),
syncRecordsToDataSource(ds, props.records); );
return ds;
});
useEffect(() => { useEffect(() => {
syncRecordsToDataSource(dataSource, props.records); syncRecordsToDataSource(dataSource, props.records);
}, [dataSource, props.records]); }, [dataSource, props.records]);

View File

@@ -64,7 +64,7 @@ type DataManagerActions<T> =
| Action< | Action<
'selectItemById', 'selectItemById',
{ {
id: string | number; id: string;
addToSelection?: boolean; addToSelection?: boolean;
} }
> >
@@ -213,7 +213,7 @@ export const dataTableManagerReducer = produce<
} }
case 'setColumnFilterFromSelection': { case 'setColumnFilterFromSelection': {
const items = getSelectedItems( const items = getSelectedItems(
config.dataSource as DataSource, config.dataSource as DataSource<any>,
draft.selection, draft.selection,
); );
items.forEach((item, index) => { items.forEach((item, index) => {
@@ -258,7 +258,7 @@ export type DataTableManager<T> = {
end: number, end: number,
allowUnselect?: boolean, allowUnselect?: boolean,
): void; ): void;
selectItemById(id: string | number, addToSelection?: boolean): void; selectItemById(id: string, addToSelection?: boolean): void;
clearSelection(): void; clearSelection(): void;
getSelectedItem(): T | undefined; getSelectedItem(): T | undefined;
getSelectedItems(): readonly T[]; getSelectedItems(): readonly T[];

View File

@@ -33,7 +33,7 @@ export interface Request {
insights?: Insights; insights?: Insights;
} }
export type Requests = DataSource<Request, 'id', string>; export type Requests = DataSource<Request>;
export type SerializedRequest = Omit< export type SerializedRequest = Omit<
Request, Request,