Skip to content

Commit

Permalink
Issue 347- Improve TS type inference & simplify caching logic (plotly…
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc-Andre-Rivet committed Jan 31, 2019
1 parent 0d79937 commit cfb5ab0
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 188 deletions.
11 changes: 11 additions & 0 deletions packages/dash-table/src/core/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as R from 'ramda';

export type CacheKeyFragment = string | number | boolean;

export function getCache<TKey extends CacheKeyFragment[]>(cache: Map<CacheKeyFragment, any>, ...key: TKey) {
const cacheKeys = key.slice(0, -1);

return R.reduce((c, fragment) => {
return c.get(fragment) || c.set(fragment, new Map()).get(fragment);
}, cache, cacheKeys) as Map<CacheKeyFragment, any>;
}
21 changes: 21 additions & 0 deletions packages/dash-table/src/core/cache/memoizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { memoizeOne } from 'core/memoizer';
import { CacheKeyFragment, getCache } from '.';

export default <TKey extends CacheKeyFragment[]>() => {
return <TEntryFn extends (...a: any[]) => any>(fn: TEntryFn) => {
const cache = new Map<CacheKeyFragment, any>();

function get(...key: TKey) {
const lastKey = key.slice(-1)[0];

const nestedCache = getCache(cache, ...key);

return (
nestedCache.get(lastKey) ||
nestedCache.set(lastKey, memoizeOne(fn)).get(lastKey)
);
}

return { get };
};
};
18 changes: 18 additions & 0 deletions packages/dash-table/src/core/cache/value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CacheKeyFragment, getCache } from '.';

export default <TKey extends CacheKeyFragment[]>() =>
<TEntry>(fn: (...a: TKey) => TEntry) => {
const cache = new Map<CacheKeyFragment, any>();

function get(...key: TKey) {
const lastKey = key.slice(-1)[0];

const nestedCache = getCache(cache, ...key);

return nestedCache.has(lastKey) ?
nestedCache.get(lastKey) :
nestedCache.set(lastKey, fn(...key)).get(lastKey);
}

return { get };
};
31 changes: 0 additions & 31 deletions packages/dash-table/src/core/memoizerCache.ts

This file was deleted.

5 changes: 2 additions & 3 deletions packages/dash-table/src/dash-table/components/CellFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class CellFactory {
private readonly cellDropdowns = derivedDropdowns(),
private readonly cellOperations = derivedCellOperations(),
private readonly cellStyles = derivedCellStyles(),
private readonly cellWrappers = derivedCellWrappers(propsFn().id),
private readonly cellWrappers = derivedCellWrappers(),
private readonly relevantStyles = derivedRelevantCellStyles()
) { }

Expand All @@ -36,7 +36,6 @@ export default class CellFactory {
data,
dropdown_properties, // legacy
editable,
id,
is_focused,
row_deletable,
row_selectable,
Expand Down Expand Up @@ -75,7 +74,7 @@ export default class CellFactory {
virtualized.offset
);

const dropdowns = this.cellDropdowns(id)(
const dropdowns = this.cellDropdowns(
columns,
virtualized.data,
virtualized.indices,
Expand Down
174 changes: 104 additions & 70 deletions packages/dash-table/src/dash-table/derived/cell/dropdowns.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,128 @@
import * as R from 'ramda';

import { memoizeOneFactory } from 'core/memoizer';
import { memoizeOne } from 'core/memoizer';
import memoizerCache from 'core/cache/memoizer';
import SyntaxTree from 'core/syntax-tree';

import {
IConditionalDropdown
} from 'dash-table/components/CellDropdown/types';

import {
Data,
Datum,
VisibleColumns,
ColumnId,
Indices,
IColumnDropdown,
IConditionalColumnDropdown,
IDropdownProperties,
DropdownValues
DropdownValues,
IBaseVisibleColumn,
IVisibleColumn
} from 'dash-table/components/Table/props';
import SyntaxTree from 'core/syntax-tree';
import memoizerCache from 'core/memoizerCache';
import { IConditionalDropdown } from 'dash-table/components/CellDropdown/types';

const mapData = R.addIndex<Datum, (DropdownValues | undefined)[]>(R.map);

const getDropdown = (
astCache: (key: [ColumnId, number], query: string) => SyntaxTree,
conditionalDropdowns: IConditionalDropdown[],
datum: Datum,
property: ColumnId,
staticDropdown: DropdownValues | undefined
): DropdownValues | undefined => {
const dropdowns = [
...(staticDropdown ? [staticDropdown] : []),
...R.map(
([cd]) => cd.dropdown,
R.filter(
([cd, i]) => astCache([property, i], cd.condition).evaluate(datum),
R.addIndex<IConditionalDropdown, [IConditionalDropdown, number]>(R.map)(
(cd, i) => [cd, i],
conditionalDropdowns
))
)
];
export default () => new Dropdowns().get;

return dropdowns.length ? dropdowns.slice(-1)[0] : undefined;
};
class Dropdowns {
/**
* Return the dropdown for each cell in the table.
*/
get = memoizeOne((
columns: VisibleColumns,
data: Data,
indices: Indices,
columnConditionalDropdown: any,
columnStaticDropdown: any,
dropdown_properties: any
) => mapData((datum, rowIndex) => R.map(column => {
const applicable = this.applicable.get(column.id, rowIndex)(
column,
indices[rowIndex],
columnConditionalDropdown,
columnStaticDropdown,
dropdown_properties
);

const getter = (
astCache: (key: [ColumnId, number], query: string) => SyntaxTree,
columns: VisibleColumns,
data: Data,
indices: Indices,
columnConditionalDropdown: IConditionalColumnDropdown[],
columnStaticDropdown: IColumnDropdown[],
dropdown_properties: IDropdownProperties
): (DropdownValues | undefined)[][] => mapData((datum, rowIndex) => R.map(column => {
const realIndex = indices[rowIndex];
return this.dropdown.get(column.id, rowIndex)(
applicable,
column,
datum
);
}, columns), data));

let legacyDropdown = (
(
dropdown_properties &&
dropdown_properties[column.id] &&
/**
* Returns the list of applicable dropdowns for a cell.
*/
private readonly applicable = memoizerCache<[ColumnId, number]>()((
column: IBaseVisibleColumn,
realIndex: number,
columnConditionalDropdown: any,
columnStaticDropdown: any,
dropdown_properties: any
): [any, any] => {
let legacyDropdown = (
(
dropdown_properties[column.id].length > realIndex ?
dropdown_properties[column.id][realIndex] :
null
)
) || column
).options;
dropdown_properties &&
dropdown_properties[column.id] &&
(
dropdown_properties[column.id].length > realIndex ?
dropdown_properties[column.id][realIndex] :
null
)
) || column
).options;

const conditional = columnConditionalDropdown.find((cs: any) => cs.id === column.id);
const base = columnStaticDropdown.find((ss: any) => ss.id === column.id);
const conditional = columnConditionalDropdown.find((cs: any) => cs.id === column.id);
const base = columnStaticDropdown.find((ss: any) => ss.id === column.id);

const conditionalDropdowns = (conditional && conditional.dropdowns) || [];
const staticDropdown = legacyDropdown || (base && base.dropdown);
return [
legacyDropdown || (base && base.dropdown),
(conditional && conditional.dropdowns) || []
];
});

return getDropdown(astCache, conditionalDropdowns, datum, column.id, staticDropdown);
}, columns), data);
/**
* Returns the highest priority dropdown from the
* applicable dropdowns.
*/
private readonly dropdown = memoizerCache<[ColumnId, number]>()((
applicableDropdowns: [any, any],
column: IVisibleColumn,
datum: Datum
) => {
const [staticDropdown, conditionalDropdowns] = applicableDropdowns;

const getterFactory = memoizeOneFactory(getter);
const matches = [
...(staticDropdown ? [staticDropdown] : []),
...R.map(
([cd]) => cd.dropdown,
R.filter(
([cd, i]) => this.evaluation.get(column.id, i)(
this.ast.get(column.id, i)(cd.condition),
datum
),
R.addIndex<IConditionalDropdown, [IConditionalDropdown, number]>(R.map)(
(cd, i) => [cd, i],
conditionalDropdowns
))
)
];

const decoratedGetter = (_id: string): ((
columns: VisibleColumns,
data: Data,
indices: Indices,
columnConditionalDropdown: any,
columnStaticDropdown: any,
dropdown_properties: any
) => (DropdownValues | undefined)[][]) => {
const astCache = memoizerCache<[ColumnId, number], [string], SyntaxTree>(
(query: string) => new SyntaxTree(query)
);
return matches.length ? matches.slice(-1)[0] : undefined;
});

return getterFactory().bind(undefined, astCache);
};
/**
* Get the query's AST.
*/
private readonly ast = memoizerCache<[ColumnId, number]>()((
query: string
) => new SyntaxTree(query));

export default memoizeOneFactory(decoratedGetter);
/**
* Evaluate if the query matches the cell's data.
*/
private readonly evaluation = memoizerCache<[ColumnId, number]>()((
ast: SyntaxTree,
datum: Datum
) => ast.evaluate(datum));
}
56 changes: 28 additions & 28 deletions packages/dash-table/src/dash-table/derived/cell/eventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@

import { memoizeOneFactory } from 'core/memoizer';
import memoizerCache from 'core/memoizerCache';
import valueCache from 'core/cache/value';
import { ICellFactoryProps } from 'dash-table/components/Table/props';
import { handleChange, handleClick, handleDoubleClick, handleOnMouseUp, handlePaste } from 'dash-table/handlers/cellEvents';

type CacheArgs = [Handler, number, number];
type GetterArgs = [HandlerFn, number, number];

export enum Handler {
Change = 'change',
Expand All @@ -18,34 +16,36 @@ export enum Handler {
export type CacheFn = (...args: CacheArgs) => Function;
export type HandlerFn = (...args: any[]) => any;

const getter = (propsFn: () => ICellFactoryProps): CacheFn => {
const cache = memoizerCache<CacheArgs, GetterArgs, Function>((...args: GetterArgs) => {
let [
handler,
rowIndex,
columnIndex
] = args;
export default (propsFn: () => ICellFactoryProps) => new EventHandler(propsFn).get;

return handler && handler.bind(undefined, rowIndex, columnIndex);
});
class EventHandler {
constructor(private readonly propsFn: () => ICellFactoryProps) {

}

const handlers = new Map<Handler, HandlerFn>([
[Handler.Change, handleChange.bind(undefined, propsFn)],
[Handler.Click, handleClick.bind(undefined, propsFn)],
[Handler.DoubleClick, handleDoubleClick.bind(undefined, propsFn)],
[Handler.MouseUp, handleOnMouseUp.bind(undefined, propsFn)],
[Handler.Paste, handlePaste.bind(undefined, propsFn)]
private readonly handlers = new Map<Handler, HandlerFn>([
[Handler.Change, handleChange.bind(undefined, this.propsFn)],
[Handler.Click, handleClick.bind(undefined, this.propsFn)],
[Handler.DoubleClick, handleDoubleClick.bind(undefined, this.propsFn)],
[Handler.MouseUp, handleOnMouseUp.bind(undefined, this.propsFn)],
[Handler.Paste, handlePaste.bind(undefined, this.propsFn)]
]);

return (...args: CacheArgs) => {
let [
handler,
rowIndex,
columnIndex
] = args;
private readonly cache = valueCache<[Handler, number, number]>()((
handler: Handler,
columnIndex: number,
rowIndex: number
) => {
let handlerFn = this.handlers.get(handler);

return cache(args, handlers.get(handler) as HandlerFn, rowIndex, columnIndex);
};
};
return handlerFn && handlerFn.bind(undefined, rowIndex, columnIndex);
});

export default memoizeOneFactory(getter);
get = (
handler: Handler,
columnIndex: number,
rowIndex: number
) => {
return this.cache.get(handler, columnIndex, rowIndex);
}
}
Loading

0 comments on commit cfb5ab0

Please sign in to comment.