Skip to content

Commit 53ae761

Browse files
authored
Merge pull request #114 from bcdev/forman-113-memoize_getHostStorePropertyRefs
Memoize computation of property references
2 parents f6118e4 + 21b115a commit 53ae761

File tree

9 files changed

+240
-62
lines changed

9 files changed

+240
-62
lines changed

chartlets.js/CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
* Add `multiple` property for `Select` component to enable the selection
44
of multiple elements. The `default` mode is supported at the moment.
55

6+
* Static information about callbacks retrieved from API is not cached
7+
reducing unnecessary processing and improving performance. (#113)
8+
9+
* Relaxed requirements for adding new components to Chartlets.js via
10+
plugins. Implementing `ComponentProps` is now optional. (#115)
11+
612
* Callbacks will now only be invoked when there’s an actual change in state,
713
reducing unnecessary processing and improving performance. (#112)
814

chartlets.js/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chartlets.js/packages/lib/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"preview": "vite preview"
5656
},
5757
"dependencies": {
58+
"micro-memoize": "^4.1.3",
5859
"microdiff": "^1.4",
5960
"zustand": "^5.0"
6061
},

chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
import memoize from "micro-memoize";
2+
13
import type {
4+
Callback,
25
CallbackRef,
36
CallbackRequest,
47
ContribRef,
58
InputRef,
69
} from "@/types/model/callback";
7-
import type { Input } from "@/types/model/channel";
810
import { getInputValues } from "@/actions/helpers/getInputValues";
911
import { formatObjPath } from "@/utils/objPath";
1012
import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks";
1113
import type { ContributionState } from "@/types/state/contribution";
1214
import type { HostStore } from "@/types/state/host";
1315
import { store } from "@/store";
14-
import { shallowEqualArrays } from "@/utils/compare";
16+
import { shallowEqualArrays } from "@/utils/shallowEqualArrays";
17+
import type { ContribPoint } from "@/types/model/extension";
1518

1619
/**
1720
* A reference to a property of an input of a callback of a contribution.
@@ -21,23 +24,31 @@ export interface PropertyRef extends ContribRef, CallbackRef, InputRef {
2124
property: string;
2225
}
2326

24-
export function handleHostStoreChange() {
27+
/**
28+
* This action is called once a host store change has been detected.
29+
* Note this will only create callback requests for callbacks whose input
30+
* value are affected by the change.
31+
*
32+
* @returns The array of callback requests made or `undefined`,
33+
* if no callback requests have been made.
34+
*/
35+
export function handleHostStoreChange(): CallbackRequest[] | undefined {
2536
const { extensions, configuration, contributionsRecord } = store.getState();
2637
const { hostStore } = configuration;
2738
if (!hostStore) {
2839
// Exit if no host store configured.
2940
// Actually, we should not come here.
30-
return;
41+
return undefined;
3142
}
3243
synchronizeThemeMode(hostStore);
3344
if (extensions.length === 0) {
3445
// Exit if there are no extensions (yet)
35-
return;
46+
return undefined;
3647
}
37-
const propertyRefs = getHostStorePropertyRefs();
48+
const propertyRefs = getPropertyRefsForContribPoints(contributionsRecord);
3849
if (!propertyRefs || propertyRefs.length === 0) {
3950
// Exit if there are is nothing to be changed
40-
return;
51+
return undefined;
4152
}
4253
const callbackRequests = getCallbackRequests(
4354
propertyRefs,
@@ -49,9 +60,12 @@ export function handleHostStoreChange() {
4960
(callbackRequest): callbackRequest is CallbackRequest =>
5061
callbackRequest !== undefined,
5162
);
52-
if (filteredCallbackRequests && filteredCallbackRequests.length > 0) {
53-
invokeCallbacks(filteredCallbackRequests);
63+
if (!filteredCallbackRequests || !filteredCallbackRequests.length) {
64+
return undefined;
5465
}
66+
67+
invokeCallbacks(filteredCallbackRequests);
68+
return filteredCallbackRequests;
5569
}
5670

5771
// Exporting for testing only
@@ -103,36 +117,77 @@ const getCallbackRequest = (
103117
return { ...propertyRef, inputValues };
104118
};
105119

106-
// TODO: use a memoized selector to get hostStorePropertyRefs
107-
// Note that this will only be effective and once we split the
108-
// static contribution infos and dynamic contribution states.
109-
// The hostStorePropertyRefs only depend on the static
110-
// contribution infos.
111-
112120
/**
113-
* Get the static list of host state property references for all contributions.
121+
* Get the static list of host state property references
122+
* for given contribution points.
123+
* Note: the export exists only for testing.
114124
*/
115-
function getHostStorePropertyRefs(): PropertyRef[] {
116-
const { contributionsRecord } = store.getState();
125+
export const getPropertyRefsForContribPoints = memoize(
126+
_getPropertyRefsForContribPoints,
127+
);
128+
129+
function _getPropertyRefsForContribPoints(
130+
contributionsRecord: Record<ContribPoint, ContributionState[]>,
131+
): PropertyRef[] {
117132
const propertyRefs: PropertyRef[] = [];
118133
Object.getOwnPropertyNames(contributionsRecord).forEach((contribPoint) => {
119134
const contributions = contributionsRecord[contribPoint];
120-
contributions.forEach((contribution, contribIndex) => {
121-
(contribution.callbacks || []).forEach(
122-
(callback, callbackIndex) =>
123-
(callback.inputs || []).forEach((input, inputIndex) => {
124-
if (!input.noTrigger && input.id === "@app" && input.property) {
125-
propertyRefs.push({
126-
contribPoint,
127-
contribIndex,
128-
callbackIndex,
129-
inputIndex,
130-
property: formatObjPath(input.property),
131-
});
132-
}
133-
}),
134-
[] as Input[],
135-
);
135+
propertyRefs.push(
136+
...getPropertyRefsForContributions(contribPoint, contributions),
137+
);
138+
});
139+
return propertyRefs;
140+
}
141+
142+
/**
143+
* Get the static list of host state property references
144+
* for given contributions.
145+
*/
146+
const getPropertyRefsForContributions = memoize(
147+
_getPropertyRefsForContributions,
148+
);
149+
150+
function _getPropertyRefsForContributions(
151+
contribPoint: string,
152+
contributions: ContributionState[],
153+
): PropertyRef[] {
154+
const propertyRefs: PropertyRef[] = [];
155+
contributions.forEach((contribution, contribIndex) => {
156+
propertyRefs.push(
157+
...getPropertyRefsForCallbacks(
158+
contribPoint,
159+
contribIndex,
160+
contribution.callbacks,
161+
),
162+
);
163+
});
164+
return propertyRefs;
165+
}
166+
167+
/**
168+
* Get the static list of host state property references
169+
* for given callbacks.
170+
*/
171+
const getPropertyRefsForCallbacks = memoize(_getPropertyRefsForCallbacks);
172+
173+
function _getPropertyRefsForCallbacks(
174+
contribPoint: string,
175+
contribIndex: number,
176+
callbacks: Callback[] | undefined,
177+
) {
178+
const propertyRefs: PropertyRef[] = [];
179+
(callbacks || []).forEach((callback, callbackIndex) => {
180+
const inputs = callback.inputs || [];
181+
inputs.forEach((input, inputIndex) => {
182+
if (!input.noTrigger && input.id === "@app" && input.property) {
183+
propertyRefs.push({
184+
contribPoint,
185+
contribIndex,
186+
callbackIndex,
187+
inputIndex,
188+
property: formatObjPath(input.property),
189+
});
190+
}
136191
});
137192
});
138193
return propertyRefs;

chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
22
import { store } from "@/store";
33
import {
44
getCallbackRequests,
5+
getPropertyRefsForContribPoints,
56
handleHostStoreChange,
67
type PropertyRef,
78
} from "./handleHostStoreChange";
@@ -11,6 +12,7 @@ import type { ContributionState } from "@/types/state/contribution";
1112
describe("handleHostStoreChange", () => {
1213
let listeners: (() => void)[] = [];
1314
let hostState: Record<string, unknown> = {};
15+
// noinspection JSUnusedGlobalSymbols
1416
const hostStore = {
1517
get: (key: string) => hostState[key],
1618
set: (key: string, value: unknown) => {
@@ -46,6 +48,76 @@ describe("handleHostStoreChange", () => {
4648
expect(store.getState().themeMode).toEqual("light");
4749
});
4850

51+
it("should memoize computation of property refs", () => {
52+
const contributionsRecord: Record<string, ContributionState[]> = {
53+
panels: [
54+
{
55+
name: "p0",
56+
container: { title: "Panel A" },
57+
extension: "e0",
58+
componentResult: {},
59+
initialState: {},
60+
callbacks: [
61+
{
62+
function: {
63+
name: "callback",
64+
parameters: [],
65+
return: {},
66+
},
67+
inputs: [{ id: "@app", property: "variableName" }],
68+
outputs: [{ id: "select", property: "value" }],
69+
},
70+
{
71+
function: {
72+
name: "callback2",
73+
parameters: [],
74+
return: {},
75+
},
76+
inputs: [
77+
{ id: "@app", property: "datasetId" },
78+
{ id: "@app", property: "variableName" },
79+
],
80+
outputs: [{ id: "plot", property: "value" }],
81+
},
82+
],
83+
},
84+
],
85+
};
86+
const propertyRefs1 = getPropertyRefsForContribPoints(contributionsRecord);
87+
const propertyRefs2 = getPropertyRefsForContribPoints(contributionsRecord);
88+
const propertyRefs3 = getPropertyRefsForContribPoints({
89+
...contributionsRecord,
90+
});
91+
expect(propertyRefs1).toBe(propertyRefs2);
92+
expect(propertyRefs2).not.toBe(propertyRefs3);
93+
expect(propertyRefs1).toEqual([
94+
{
95+
callbackIndex: 0,
96+
contribIndex: 0,
97+
contribPoint: "panels",
98+
inputIndex: 0,
99+
property: "variableName",
100+
},
101+
{
102+
callbackIndex: 1,
103+
contribIndex: 0,
104+
contribPoint: "panels",
105+
inputIndex: 0,
106+
property: "datasetId",
107+
},
108+
{
109+
callbackIndex: 1,
110+
contribIndex: 0,
111+
contribPoint: "panels",
112+
inputIndex: 1,
113+
property: "variableName",
114+
},
115+
]);
116+
expect(propertyRefs1).toEqual(propertyRefs2);
117+
expect(propertyRefs1).toEqual(propertyRefs3);
118+
expect(propertyRefs2).toEqual(propertyRefs3);
119+
});
120+
49121
it("should generate callback requests", () => {
50122
const extensions = [{ name: "e0", version: "0", contributes: ["panels"] }];
51123
store.setState({
@@ -110,12 +182,20 @@ describe("handleHostStoreChange", () => {
110182
},
111183
});
112184
hostStore.set("variableName", "CHL");
113-
handleHostStoreChange();
185+
expect(handleHostStoreChange()).toEqual([
186+
{
187+
contribPoint: "panel",
188+
contribIndex: 0,
189+
callbackIndex: 0,
190+
inputIndex: 0,
191+
inputValues: ["CHL"],
192+
property: "variableName",
193+
},
194+
]);
114195

115-
// calling it second time for coverage. No state change changes the
116-
// control flow
117-
handleHostStoreChange();
118-
// TODO: Update this test to assert the generated callback request
196+
// calling it second time for coverage,
197+
// because the no-state-change changes the control flow
198+
expect(handleHostStoreChange()).toBeUndefined();
119199
});
120200

121201
it("should memoize second call with same arguments", () => {

chartlets.js/packages/lib/src/types/state/store.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,33 @@ import type { FrameworkOptions } from "./options";
99

1010
export type ThemeMode = "dark" | "light" | "system";
1111

12+
// TODO: Split contributionsRecord into two fields comprising static
13+
// contribution data and dynamic contribution states.
14+
// This will allow memoizing the computation of property references
15+
// (PropertyRef[]) on the level of the StoreState from static data only.
16+
// The property references would then be just computed once.
17+
// See function getPropertyRefsForContribPoints()
18+
// in actions/handleHostStoreChange.ts
19+
20+
/**
21+
* The state of the Chartlets main store.
22+
*/
1223
export interface StoreState {
13-
/** Framework configuration */
24+
/**
25+
* Framework configuration.
26+
*/
1427
configuration: FrameworkOptions;
15-
/** All extensions */
28+
/**
29+
* All extensions.
30+
*/
1631
extensions: Extension[];
17-
/** API call result from `GET /contributions`. */
32+
/**
33+
* API call result from `GET /contributions`.
34+
*/
1835
contributionsResult: ApiResult<Contributions>;
19-
/** A record that maps contribPoint --> ContributionState[].*/
36+
/**
37+
* A record that maps contribPoint --> ContributionState[].
38+
*/
2039
contributionsRecord: Record<ContribPoint, ContributionState[]>;
2140
/**
2241
* The app's current theme mode.

chartlets.js/packages/lib/src/utils/compare.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)