Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions chartlets.js/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* Add `multiple` property for `Select` component to enable the selection
of multiple elements. The `default` mode is supported at the moment.

* Static information about callbacks retrieved from API is not cached
reducing unnecessary processing and improving performance. (#113)

* Callbacks will now only be invoked when there’s an actual change in state,
reducing unnecessary processing and improving performance. (#112)

Expand Down
7 changes: 7 additions & 0 deletions chartlets.js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions chartlets.js/packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"preview": "vite preview"
},
"dependencies": {
"micro-memoize": "^4.1.3",
"microdiff": "^1.4",
"zustand": "^5.0"
},
Expand Down
100 changes: 72 additions & 28 deletions chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import memoize from "micro-memoize";

import type {
Callback,
CallbackRef,
CallbackRequest,
ContribRef,
InputRef,
} from "@/types/model/callback";
import type { Input } from "@/types/model/channel";
import { getInputValues } from "@/actions/helpers/getInputValues";
import { formatObjPath } from "@/utils/objPath";
import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks";
import type { ContributionState } from "@/types/state/contribution";
import type { HostStore } from "@/types/state/host";
import { store } from "@/store";
import { shallowEqualArrays } from "@/utils/compare";
import { shallowEqualArrays } from "@/utils/shallowEqualArrays";
import type { ContribPoint } from "@/types/model/extension";

/**
* A reference to a property of an input of a callback of a contribution.
Expand All @@ -34,7 +37,7 @@ export function handleHostStoreChange() {
// Exit if there are no extensions (yet)
return;
}
const propertyRefs = getHostStorePropertyRefs();
const propertyRefs = getPropertyRefsForContribPoints(contributionsRecord);
if (!propertyRefs || propertyRefs.length === 0) {
// Exit if there are is nothing to be changed
return;
Expand Down Expand Up @@ -103,36 +106,77 @@ const getCallbackRequest = (
return { ...propertyRef, inputValues };
};

// TODO: use a memoized selector to get hostStorePropertyRefs
// Note that this will only be effective and once we split the
// static contribution infos and dynamic contribution states.
// The hostStorePropertyRefs only depend on the static
// contribution infos.

/**
* Get the static list of host state property references for all contributions.
* Get the static list of host state property references
* for given contribution points.
* Note: the export exists only for testing.
*/
function getHostStorePropertyRefs(): PropertyRef[] {
const { contributionsRecord } = store.getState();
export const getPropertyRefsForContribPoints = memoize(
_getPropertyRefsForContribPoints,
);

function _getPropertyRefsForContribPoints(
contributionsRecord: Record<ContribPoint, ContributionState[]>,
): PropertyRef[] {
const propertyRefs: PropertyRef[] = [];
Object.getOwnPropertyNames(contributionsRecord).forEach((contribPoint) => {
const contributions = contributionsRecord[contribPoint];
contributions.forEach((contribution, contribIndex) => {
(contribution.callbacks || []).forEach(
(callback, callbackIndex) =>
(callback.inputs || []).forEach((input, inputIndex) => {
if (!input.noTrigger && input.id === "@app" && input.property) {
propertyRefs.push({
contribPoint,
contribIndex,
callbackIndex,
inputIndex,
property: formatObjPath(input.property),
});
}
}),
[] as Input[],
);
propertyRefs.push(
...getPropertyRefsForContributions(contribPoint, contributions),
);
});
return propertyRefs;
}

/**
* Get the static list of host state property references
* for given contributions.
*/
const getPropertyRefsForContributions = memoize(
_getPropertyRefsForContributions,
);

function _getPropertyRefsForContributions(
contribPoint: string,
contributions: ContributionState[],
): PropertyRef[] {
const propertyRefs: PropertyRef[] = [];
contributions.forEach((contribution, contribIndex) => {
propertyRefs.push(
...getPropertyRefsForCallbacks(
contribPoint,
contribIndex,
contribution.callbacks,
),
);
});
return propertyRefs;
}

/**
* Get the static list of host state property references
* for given callbacks.
*/
const getPropertyRefsForCallbacks = memoize(_getPropertyRefsForCallbacks);

function _getPropertyRefsForCallbacks(
contribPoint: string,
contribIndex: number,
callbacks: Callback[] | undefined,
) {
const propertyRefs: PropertyRef[] = [];
(callbacks || []).forEach((callback, callbackIndex) => {
const inputs = callback.inputs || [];
inputs.forEach((input, inputIndex) => {
if (!input.noTrigger && input.id === "@app" && input.property) {
propertyRefs.push({
contribPoint,
contribIndex,
callbackIndex,
inputIndex,
property: formatObjPath(input.property),
});
}
});
});
return propertyRefs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
import { store } from "@/store";
import {
getCallbackRequests,
getPropertyRefsForContribPoints,
handleHostStoreChange,
type PropertyRef,
} from "./handleHostStoreChange";
Expand Down Expand Up @@ -46,6 +47,76 @@ describe("handleHostStoreChange", () => {
expect(store.getState().themeMode).toEqual("light");
});

it("should memoize computation of property refs", () => {
const contributionsRecord: Record<string, ContributionState[]> = {
panels: [
{
name: "p0",
container: { title: "Panel A" },
extension: "e0",
componentResult: {},
initialState: {},
callbacks: [
{
function: {
name: "callback",
parameters: [],
return: {},
},
inputs: [{ id: "@app", property: "variableName" }],
outputs: [{ id: "select", property: "value" }],
},
{
function: {
name: "callback2",
parameters: [],
return: {},
},
inputs: [
{ id: "@app", property: "datasetId" },
{ id: "@app", property: "variableName" },
],
outputs: [{ id: "plot", property: "value" }],
},
],
},
],
};
const propertyRefs1 = getPropertyRefsForContribPoints(contributionsRecord);
const propertyRefs2 = getPropertyRefsForContribPoints(contributionsRecord);
const propertyRefs3 = getPropertyRefsForContribPoints({
...contributionsRecord,
});
expect(propertyRefs1).toBe(propertyRefs2);
expect(propertyRefs2).not.toBe(propertyRefs3);
expect(propertyRefs1).toEqual([
{
callbackIndex: 0,
contribIndex: 0,
contribPoint: "panels",
inputIndex: 0,
property: "variableName",
},
{
callbackIndex: 1,
contribIndex: 0,
contribPoint: "panels",
inputIndex: 0,
property: "datasetId",
},
{
callbackIndex: 1,
contribIndex: 0,
contribPoint: "panels",
inputIndex: 1,
property: "variableName",
},
]);
expect(propertyRefs1).toEqual(propertyRefs2);
expect(propertyRefs1).toEqual(propertyRefs3);
expect(propertyRefs2).toEqual(propertyRefs3);
});

it("should generate callback requests", () => {
const extensions = [{ name: "e0", version: "0", contributes: ["panels"] }];
store.setState({
Expand Down
27 changes: 23 additions & 4 deletions chartlets.js/packages/lib/src/types/state/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,33 @@ import type { FrameworkOptions } from "./options";

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

// TODO: Split contributionsRecord into two fields comprising static
// contribution data and dynamic contribution states.
// This will allow memoizing the computation of property references
// (PropertyRef[]) on the level of the StoreState from static data only.
// The property references would then be just computed once.
// See function getPropertyRefsForContribPoints()
// in actions/handleHostStoreChange.ts

/**
* The state of the Chartlets main store.
*/
export interface StoreState {
/** Framework configuration */
/**
* Framework configuration.
*/
configuration: FrameworkOptions;
/** All extensions */
/**
* All extensions.
*/
extensions: Extension[];
/** API call result from `GET /contributions`. */
/**
* API call result from `GET /contributions`.
*/
contributionsResult: ApiResult<Contributions>;
/** A record that maps contribPoint --> ContributionState[].*/
/**
* A record that maps contribPoint --> ContributionState[].
*/
contributionsRecord: Record<ContribPoint, ContributionState[]>;
/**
* The app's current theme mode.
Expand Down
12 changes: 0 additions & 12 deletions chartlets.js/packages/lib/src/utils/compare.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { shallowEqualArrays } from "@/utils/compare";
import { shallowEqualArrays } from "@/utils/shallowEqualArrays";

describe("Test shallowEqualArrays()", () => {
const arr_a: string[] = ["a", "b", "c"];
Expand All @@ -11,15 +11,16 @@ describe("Test shallowEqualArrays()", () => {
const arr_g: (string | null)[] = ["a", "b", "c", null];
const arr_h = [1, [1, 2, 3], [4, 5, 6]];
const arr_i = [1, [1, 2, 3], [4, 5, 6]];
const arr_j: (number | string | null)[] = [1, 2, "c", null];
const arr_k: (number | string | null)[] = [1, 3, "c", null];
const arr_l: (number | string | null)[] = [1, 2, "c", null];
const arr_m: number[] = [1, 2];
const arr_n: number[] = [1, 2];
const arr_j: (number | string | null)[] = [1, 2.3, "c", null];
const arr_k: (number | string | null)[] = [1, 3.1, "c", null];
const arr_l: (number | string | null)[] = [1, 2.3, "c", null];
const arr_m: number[] = [1, 2, NaN, Infinity];
const arr_n: number[] = [1, 2, NaN, Infinity];
const arr_o: null[] = [null];
const arr_p: null[] = [null];
const arr_q: null[] = [];
it("works", () => {
expect(shallowEqualArrays(arr_a, arr_a)).toBe(true);
expect(shallowEqualArrays(arr_a, arr_b)).toBe(true);
expect(shallowEqualArrays(arr_a, arr_c)).toBe(false);
expect(shallowEqualArrays(arr_a, arr_d)).toBe(false);
Expand All @@ -33,6 +34,7 @@ describe("Test shallowEqualArrays()", () => {
expect(shallowEqualArrays(arr_m, arr_l)).toBe(false);
expect(shallowEqualArrays(arr_o, arr_p)).toBe(true);
expect(shallowEqualArrays(arr_p, arr_q)).toBe(false);
expect(shallowEqualArrays(arr_p)).toBe(false);
expect(shallowEqualArrays(arr_p, undefined)).toBe(false);
expect(shallowEqualArrays(undefined, arr_p)).toBe(false);
});
});
20 changes: 20 additions & 0 deletions chartlets.js/packages/lib/src/utils/shallowEqualArrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function shallowEqualArrays(
arr1: unknown[] | undefined,
arr2: unknown[] | undefined,
): boolean {
if (arr1 === arr2) {
return true;
}
if (!arr1 || !arr2) {
return false;
}
if (arr1.length !== arr2.length) {
return false;
}
for (let i = 0; i < arr1.length; i++) {
if (!Object.is(arr1[i], arr2[i])) {
return false;
}
}
return true;
}