Skip to content

Commit cfb5ab0

Browse files
Issue 347- Improve TS type inference & simplify caching logic (plotly#349)
1 parent 0d79937 commit cfb5ab0

File tree

9 files changed

+231
-188
lines changed

9 files changed

+231
-188
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as R from 'ramda';
2+
3+
export type CacheKeyFragment = string | number | boolean;
4+
5+
export function getCache<TKey extends CacheKeyFragment[]>(cache: Map<CacheKeyFragment, any>, ...key: TKey) {
6+
const cacheKeys = key.slice(0, -1);
7+
8+
return R.reduce((c, fragment) => {
9+
return c.get(fragment) || c.set(fragment, new Map()).get(fragment);
10+
}, cache, cacheKeys) as Map<CacheKeyFragment, any>;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { memoizeOne } from 'core/memoizer';
2+
import { CacheKeyFragment, getCache } from '.';
3+
4+
export default <TKey extends CacheKeyFragment[]>() => {
5+
return <TEntryFn extends (...a: any[]) => any>(fn: TEntryFn) => {
6+
const cache = new Map<CacheKeyFragment, any>();
7+
8+
function get(...key: TKey) {
9+
const lastKey = key.slice(-1)[0];
10+
11+
const nestedCache = getCache(cache, ...key);
12+
13+
return (
14+
nestedCache.get(lastKey) ||
15+
nestedCache.set(lastKey, memoizeOne(fn)).get(lastKey)
16+
);
17+
}
18+
19+
return { get };
20+
};
21+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { CacheKeyFragment, getCache } from '.';
2+
3+
export default <TKey extends CacheKeyFragment[]>() =>
4+
<TEntry>(fn: (...a: TKey) => TEntry) => {
5+
const cache = new Map<CacheKeyFragment, any>();
6+
7+
function get(...key: TKey) {
8+
const lastKey = key.slice(-1)[0];
9+
10+
const nestedCache = getCache(cache, ...key);
11+
12+
return nestedCache.has(lastKey) ?
13+
nestedCache.get(lastKey) :
14+
nestedCache.set(lastKey, fn(...key)).get(lastKey);
15+
}
16+
17+
return { get };
18+
};

packages/dash-table/src/core/memoizerCache.ts

-31
This file was deleted.

packages/dash-table/src/dash-table/components/CellFactory.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default class CellFactory {
2323
private readonly cellDropdowns = derivedDropdowns(),
2424
private readonly cellOperations = derivedCellOperations(),
2525
private readonly cellStyles = derivedCellStyles(),
26-
private readonly cellWrappers = derivedCellWrappers(propsFn().id),
26+
private readonly cellWrappers = derivedCellWrappers(),
2727
private readonly relevantStyles = derivedRelevantCellStyles()
2828
) { }
2929

@@ -36,7 +36,6 @@ export default class CellFactory {
3636
data,
3737
dropdown_properties, // legacy
3838
editable,
39-
id,
4039
is_focused,
4140
row_deletable,
4241
row_selectable,
@@ -75,7 +74,7 @@ export default class CellFactory {
7574
virtualized.offset
7675
);
7776

78-
const dropdowns = this.cellDropdowns(id)(
77+
const dropdowns = this.cellDropdowns(
7978
columns,
8079
virtualized.data,
8180
virtualized.indices,
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,128 @@
11
import * as R from 'ramda';
22

3-
import { memoizeOneFactory } from 'core/memoizer';
3+
import { memoizeOne } from 'core/memoizer';
4+
import memoizerCache from 'core/cache/memoizer';
5+
import SyntaxTree from 'core/syntax-tree';
6+
7+
import {
8+
IConditionalDropdown
9+
} from 'dash-table/components/CellDropdown/types';
410

511
import {
612
Data,
713
Datum,
814
VisibleColumns,
915
ColumnId,
1016
Indices,
11-
IColumnDropdown,
12-
IConditionalColumnDropdown,
13-
IDropdownProperties,
14-
DropdownValues
17+
DropdownValues,
18+
IBaseVisibleColumn,
19+
IVisibleColumn
1520
} from 'dash-table/components/Table/props';
16-
import SyntaxTree from 'core/syntax-tree';
17-
import memoizerCache from 'core/memoizerCache';
18-
import { IConditionalDropdown } from 'dash-table/components/CellDropdown/types';
1921

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

22-
const getDropdown = (
23-
astCache: (key: [ColumnId, number], query: string) => SyntaxTree,
24-
conditionalDropdowns: IConditionalDropdown[],
25-
datum: Datum,
26-
property: ColumnId,
27-
staticDropdown: DropdownValues | undefined
28-
): DropdownValues | undefined => {
29-
const dropdowns = [
30-
...(staticDropdown ? [staticDropdown] : []),
31-
...R.map(
32-
([cd]) => cd.dropdown,
33-
R.filter(
34-
([cd, i]) => astCache([property, i], cd.condition).evaluate(datum),
35-
R.addIndex<IConditionalDropdown, [IConditionalDropdown, number]>(R.map)(
36-
(cd, i) => [cd, i],
37-
conditionalDropdowns
38-
))
39-
)
40-
];
24+
export default () => new Dropdowns().get;
4125

42-
return dropdowns.length ? dropdowns.slice(-1)[0] : undefined;
43-
};
26+
class Dropdowns {
27+
/**
28+
* Return the dropdown for each cell in the table.
29+
*/
30+
get = memoizeOne((
31+
columns: VisibleColumns,
32+
data: Data,
33+
indices: Indices,
34+
columnConditionalDropdown: any,
35+
columnStaticDropdown: any,
36+
dropdown_properties: any
37+
) => mapData((datum, rowIndex) => R.map(column => {
38+
const applicable = this.applicable.get(column.id, rowIndex)(
39+
column,
40+
indices[rowIndex],
41+
columnConditionalDropdown,
42+
columnStaticDropdown,
43+
dropdown_properties
44+
);
4445

45-
const getter = (
46-
astCache: (key: [ColumnId, number], query: string) => SyntaxTree,
47-
columns: VisibleColumns,
48-
data: Data,
49-
indices: Indices,
50-
columnConditionalDropdown: IConditionalColumnDropdown[],
51-
columnStaticDropdown: IColumnDropdown[],
52-
dropdown_properties: IDropdownProperties
53-
): (DropdownValues | undefined)[][] => mapData((datum, rowIndex) => R.map(column => {
54-
const realIndex = indices[rowIndex];
46+
return this.dropdown.get(column.id, rowIndex)(
47+
applicable,
48+
column,
49+
datum
50+
);
51+
}, columns), data));
5552

56-
let legacyDropdown = (
57-
(
58-
dropdown_properties &&
59-
dropdown_properties[column.id] &&
53+
/**
54+
* Returns the list of applicable dropdowns for a cell.
55+
*/
56+
private readonly applicable = memoizerCache<[ColumnId, number]>()((
57+
column: IBaseVisibleColumn,
58+
realIndex: number,
59+
columnConditionalDropdown: any,
60+
columnStaticDropdown: any,
61+
dropdown_properties: any
62+
): [any, any] => {
63+
let legacyDropdown = (
6064
(
61-
dropdown_properties[column.id].length > realIndex ?
62-
dropdown_properties[column.id][realIndex] :
63-
null
64-
)
65-
) || column
66-
).options;
65+
dropdown_properties &&
66+
dropdown_properties[column.id] &&
67+
(
68+
dropdown_properties[column.id].length > realIndex ?
69+
dropdown_properties[column.id][realIndex] :
70+
null
71+
)
72+
) || column
73+
).options;
6774

68-
const conditional = columnConditionalDropdown.find((cs: any) => cs.id === column.id);
69-
const base = columnStaticDropdown.find((ss: any) => ss.id === column.id);
75+
const conditional = columnConditionalDropdown.find((cs: any) => cs.id === column.id);
76+
const base = columnStaticDropdown.find((ss: any) => ss.id === column.id);
7077

71-
const conditionalDropdowns = (conditional && conditional.dropdowns) || [];
72-
const staticDropdown = legacyDropdown || (base && base.dropdown);
78+
return [
79+
legacyDropdown || (base && base.dropdown),
80+
(conditional && conditional.dropdowns) || []
81+
];
82+
});
7383

74-
return getDropdown(astCache, conditionalDropdowns, datum, column.id, staticDropdown);
75-
}, columns), data);
84+
/**
85+
* Returns the highest priority dropdown from the
86+
* applicable dropdowns.
87+
*/
88+
private readonly dropdown = memoizerCache<[ColumnId, number]>()((
89+
applicableDropdowns: [any, any],
90+
column: IVisibleColumn,
91+
datum: Datum
92+
) => {
93+
const [staticDropdown, conditionalDropdowns] = applicableDropdowns;
7694

77-
const getterFactory = memoizeOneFactory(getter);
95+
const matches = [
96+
...(staticDropdown ? [staticDropdown] : []),
97+
...R.map(
98+
([cd]) => cd.dropdown,
99+
R.filter(
100+
([cd, i]) => this.evaluation.get(column.id, i)(
101+
this.ast.get(column.id, i)(cd.condition),
102+
datum
103+
),
104+
R.addIndex<IConditionalDropdown, [IConditionalDropdown, number]>(R.map)(
105+
(cd, i) => [cd, i],
106+
conditionalDropdowns
107+
))
108+
)
109+
];
78110

79-
const decoratedGetter = (_id: string): ((
80-
columns: VisibleColumns,
81-
data: Data,
82-
indices: Indices,
83-
columnConditionalDropdown: any,
84-
columnStaticDropdown: any,
85-
dropdown_properties: any
86-
) => (DropdownValues | undefined)[][]) => {
87-
const astCache = memoizerCache<[ColumnId, number], [string], SyntaxTree>(
88-
(query: string) => new SyntaxTree(query)
89-
);
111+
return matches.length ? matches.slice(-1)[0] : undefined;
112+
});
90113

91-
return getterFactory().bind(undefined, astCache);
92-
};
114+
/**
115+
* Get the query's AST.
116+
*/
117+
private readonly ast = memoizerCache<[ColumnId, number]>()((
118+
query: string
119+
) => new SyntaxTree(query));
93120

94-
export default memoizeOneFactory(decoratedGetter);
121+
/**
122+
* Evaluate if the query matches the cell's data.
123+
*/
124+
private readonly evaluation = memoizerCache<[ColumnId, number]>()((
125+
ast: SyntaxTree,
126+
datum: Datum
127+
) => ast.evaluate(datum));
128+
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11

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

76
type CacheArgs = [Handler, number, number];
8-
type GetterArgs = [HandlerFn, number, number];
97

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

21-
const getter = (propsFn: () => ICellFactoryProps): CacheFn => {
22-
const cache = memoizerCache<CacheArgs, GetterArgs, Function>((...args: GetterArgs) => {
23-
let [
24-
handler,
25-
rowIndex,
26-
columnIndex
27-
] = args;
19+
export default (propsFn: () => ICellFactoryProps) => new EventHandler(propsFn).get;
2820

29-
return handler && handler.bind(undefined, rowIndex, columnIndex);
30-
});
21+
class EventHandler {
22+
constructor(private readonly propsFn: () => ICellFactoryProps) {
23+
24+
}
3125

32-
const handlers = new Map<Handler, HandlerFn>([
33-
[Handler.Change, handleChange.bind(undefined, propsFn)],
34-
[Handler.Click, handleClick.bind(undefined, propsFn)],
35-
[Handler.DoubleClick, handleDoubleClick.bind(undefined, propsFn)],
36-
[Handler.MouseUp, handleOnMouseUp.bind(undefined, propsFn)],
37-
[Handler.Paste, handlePaste.bind(undefined, propsFn)]
26+
private readonly handlers = new Map<Handler, HandlerFn>([
27+
[Handler.Change, handleChange.bind(undefined, this.propsFn)],
28+
[Handler.Click, handleClick.bind(undefined, this.propsFn)],
29+
[Handler.DoubleClick, handleDoubleClick.bind(undefined, this.propsFn)],
30+
[Handler.MouseUp, handleOnMouseUp.bind(undefined, this.propsFn)],
31+
[Handler.Paste, handlePaste.bind(undefined, this.propsFn)]
3832
]);
3933

40-
return (...args: CacheArgs) => {
41-
let [
42-
handler,
43-
rowIndex,
44-
columnIndex
45-
] = args;
34+
private readonly cache = valueCache<[Handler, number, number]>()((
35+
handler: Handler,
36+
columnIndex: number,
37+
rowIndex: number
38+
) => {
39+
let handlerFn = this.handlers.get(handler);
4640

47-
return cache(args, handlers.get(handler) as HandlerFn, rowIndex, columnIndex);
48-
};
49-
};
41+
return handlerFn && handlerFn.bind(undefined, rowIndex, columnIndex);
42+
});
5043

51-
export default memoizeOneFactory(getter);
44+
get = (
45+
handler: Handler,
46+
columnIndex: number,
47+
rowIndex: number
48+
) => {
49+
return this.cache.get(handler, columnIndex, rowIndex);
50+
}
51+
}

0 commit comments

Comments
 (0)