From 05a9741bf6f5415bc4cfe57cc3407831036c097c Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 15 Mar 2025 09:12:21 +0100 Subject: [PATCH 1/6] save work --- chartlets.js/package-lock.json | 9 ++- chartlets.js/packages/lib/package.json | 1 + .../lib/src/actions/handleHostStoreChange.ts | 61 +++++++++++-------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/chartlets.js/package-lock.json b/chartlets.js/package-lock.json index ae12139d..78e4a138 100644 --- a/chartlets.js/package-lock.json +++ b/chartlets.js/package-lock.json @@ -5452,6 +5452,12 @@ "node": ">= 8" } }, + "node_modules/micro-memoize": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-4.1.3.tgz", + "integrity": "sha512-DzRMi8smUZXT7rCGikRwldEh6eO6qzKiPPopcr1+2EY3AYKpy5fu159PKWwIS9A6IWnrvPKDMcuFtyrroZa8Bw==", + "license": "MIT" + }, "node_modules/microdiff": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/microdiff/-/microdiff-1.4.0.tgz", @@ -8275,6 +8281,7 @@ "version": "0.1.4", "license": "MIT", "dependencies": { + "micro-memoize": "^4.1.3", "microdiff": "^1.4", "zustand": "^5.0" }, @@ -8322,4 +8329,4 @@ } } } -} \ No newline at end of file +} diff --git a/chartlets.js/packages/lib/package.json b/chartlets.js/packages/lib/package.json index cc196cdc..3c38875f 100644 --- a/chartlets.js/packages/lib/package.json +++ b/chartlets.js/packages/lib/package.json @@ -55,6 +55,7 @@ "preview": "vite preview" }, "dependencies": { + "micro-memoize": "^4.1.3", "microdiff": "^1.4", "zustand": "^5.0" }, diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index 31c7c55b..41b4d65c 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -1,4 +1,7 @@ +import memoize from "micro-memoize"; + import type { + Callback, CallbackRef, CallbackRequest, ContribRef, @@ -11,6 +14,7 @@ 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 type { ContribPoint } from "@/types/model/extension"; /** * A reference to a property of an input of a callback of a contribution. @@ -33,7 +37,7 @@ export function handleHostStoreChange() { // Exit if there are no extensions (yet) return; } - const propertyRefs = getHostStorePropertyRefs(); + const propertyRefs = getHostStorePropertyRefs(contributionsRecord); if (!propertyRefs || propertyRefs.length === 0) { // Exit if there are is nothing to be changed return; @@ -66,37 +70,42 @@ function getCallbackRequests( }); } -// 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. */ -function getHostStorePropertyRefs(): PropertyRef[] { - const { contributionsRecord } = store.getState(); +const getHostStorePropertyRefs = memoize(_getHostStorePropertyRefs); + +function getCallbackfn( + contribPoint: string, + contribution: ContributionState, + contribIndex: number, +) { + const propertyRefs: PropertyRef[] = []; + const callbacks: Callback[] = contribution.callbacks || []; + 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; +} + +function _getHostStorePropertyRefs( + contributionsRecord: Record, +): 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[], - ); - }); + contributions.forEach(getCallbackfn(propertyRefs, contribPoint)); }); return propertyRefs; } From a9ff949683987861a84ea0815d67702d59b056b1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 15 Mar 2025 10:16:09 +0100 Subject: [PATCH 2/6] Memoize computation of property references --- .../lib/src/actions/handleHostStoreChange.ts | 72 ++++++++++++++----- .../packages/lib/src/types/state/store.ts | 27 +++++-- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index 41b4d65c..8f24859a 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -7,7 +7,6 @@ import type { 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"; @@ -37,7 +36,7 @@ export function handleHostStoreChange() { // Exit if there are no extensions (yet) return; } - const propertyRefs = getHostStorePropertyRefs(contributionsRecord); + const propertyRefs = getPropertyRefsForContribPoints(contributionsRecord); if (!propertyRefs || propertyRefs.length === 0) { // Exit if there are is nothing to be changed return; @@ -71,18 +70,64 @@ function getCallbackRequests( } /** - * 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. */ -const getHostStorePropertyRefs = memoize(_getHostStorePropertyRefs); +const getPropertyRefsForContribPoints = memoize( + _getPropertyRefsForContribPoints, +); -function getCallbackfn( +function _getPropertyRefsForContribPoints( + contributionsRecord: Record, +): PropertyRef[] { + const propertyRefs: PropertyRef[] = []; + Object.getOwnPropertyNames(contributionsRecord).forEach((contribPoint) => { + const contributions = contributionsRecord[contribPoint]; + 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, - contribution: ContributionState, contribIndex: number, + callbacks: Callback[] | undefined, ) { const propertyRefs: PropertyRef[] = []; - const callbacks: Callback[] = contribution.callbacks || []; - callbacks.forEach((callback, callbackIndex) => { + (callbacks || []).forEach((callback, callbackIndex) => { const inputs = callback.inputs || []; inputs.forEach((input, inputIndex) => { if (!input.noTrigger && input.id === "@app" && input.property) { @@ -99,17 +144,6 @@ function getCallbackfn( return propertyRefs; } -function _getHostStorePropertyRefs( - contributionsRecord: Record, -): PropertyRef[] { - const propertyRefs: PropertyRef[] = []; - Object.getOwnPropertyNames(contributionsRecord).forEach((contribPoint) => { - const contributions = contributionsRecord[contribPoint]; - contributions.forEach(getCallbackfn(propertyRefs, contribPoint)); - }); - return propertyRefs; -} - function synchronizeThemeMode(hostStore: HostStore) { const newThemeMode = hostStore.get("themeMode"); const oldThemeMode = store.getState().themeMode; diff --git a/chartlets.js/packages/lib/src/types/state/store.ts b/chartlets.js/packages/lib/src/types/state/store.ts index 5edb8366..17dda663 100644 --- a/chartlets.js/packages/lib/src/types/state/store.ts +++ b/chartlets.js/packages/lib/src/types/state/store.ts @@ -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; - /** A record that maps contribPoint --> ContributionState[].*/ + /** + * A record that maps contribPoint --> ContributionState[]. + */ contributionsRecord: Record; /** * The app's current theme mode. From 878a7ebc619924fc600f24e2bd5d1131d83ae6f2 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 15 Mar 2025 10:46:12 +0100 Subject: [PATCH 3/6] Fix shallowEqualArrays() --- .../packages/lib/src/utils/compare.ts | 12 ----------- ...are.test.ts => shallowEqualArrays.test.ts} | 16 ++++++++------- .../lib/src/utils/shallowEqualArrays.ts | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 19 deletions(-) delete mode 100644 chartlets.js/packages/lib/src/utils/compare.ts rename chartlets.js/packages/lib/src/utils/{compare.test.ts => shallowEqualArrays.test.ts} (71%) create mode 100644 chartlets.js/packages/lib/src/utils/shallowEqualArrays.ts diff --git a/chartlets.js/packages/lib/src/utils/compare.ts b/chartlets.js/packages/lib/src/utils/compare.ts deleted file mode 100644 index eb917bc9..00000000 --- a/chartlets.js/packages/lib/src/utils/compare.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function shallowEqualArrays( - arr1?: unknown[], - arr2?: unknown[], -): boolean { - if (!arr1 || !arr2) { - return false; - } - if (arr1.length !== arr2.length) { - return false; - } - return arr1.every((val, index) => val === arr2[index]); -} diff --git a/chartlets.js/packages/lib/src/utils/compare.test.ts b/chartlets.js/packages/lib/src/utils/shallowEqualArrays.test.ts similarity index 71% rename from chartlets.js/packages/lib/src/utils/compare.test.ts rename to chartlets.js/packages/lib/src/utils/shallowEqualArrays.test.ts index 5889c4b9..39b58c84 100644 --- a/chartlets.js/packages/lib/src/utils/compare.test.ts +++ b/chartlets.js/packages/lib/src/utils/shallowEqualArrays.test.ts @@ -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"]; @@ -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); @@ -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); }); }); diff --git a/chartlets.js/packages/lib/src/utils/shallowEqualArrays.ts b/chartlets.js/packages/lib/src/utils/shallowEqualArrays.ts new file mode 100644 index 00000000..77c7af65 --- /dev/null +++ b/chartlets.js/packages/lib/src/utils/shallowEqualArrays.ts @@ -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; +} From d3354c784dd406cbc0dedc6f3d5c2d5a1c0a1a89 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 15 Mar 2025 10:46:25 +0100 Subject: [PATCH 4/6] Add test --- .../lib/src/actions/handleHostStoreChange.ts | 5 +- .../actions/handleHostStoreChanges.test.tsx | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index f88c15b3..5e12717d 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -13,7 +13,7 @@ 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"; /** @@ -109,8 +109,9 @@ const getCallbackRequest = ( /** * Get the static list of host state property references * for given contribution points. + * Note: the export exists only for testing. */ -const getPropertyRefsForContribPoints = memoize( +export const getPropertyRefsForContribPoints = memoize( _getPropertyRefsForContribPoints, ); diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx index 712abaf6..741880e4 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { store } from "@/store"; import { getCallbackRequests, + getPropertyRefsForContribPoints, handleHostStoreChange, type PropertyRef, } from "./handleHostStoreChange"; @@ -46,6 +47,76 @@ describe("handleHostStoreChange", () => { expect(store.getState().themeMode).toEqual("light"); }); + it("should memoize computation of property refs", () => { + const contributionsRecord: Record = { + 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({ From d45e7f09ffa119d503edb343db11db6e01604f79 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 20 Mar 2025 09:00:25 +0100 Subject: [PATCH 5/6] update --- chartlets.js/CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index ae32b0e5..28c8ce04 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -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) From 21b115abcd493c2f9a3230a6eaef32d2305f29ea Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 20 Mar 2025 11:18:53 +0100 Subject: [PATCH 6/6] Refined test --- chartlets.js/CHANGES.md | 3 +++ .../lib/src/actions/handleHostStoreChange.ts | 23 ++++++++++++++----- .../actions/handleHostStoreChanges.test.tsx | 19 +++++++++++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index 28c8ce04..50958765 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -6,6 +6,9 @@ * Static information about callbacks retrieved from API is not cached reducing unnecessary processing and improving performance. (#113) +* Relaxed requirements for adding new components to Chartlets.js via + plugins. Implementing `ComponentProps` is now optional. (#115) + * Callbacks will now only be invoked when there’s an actual change in state, reducing unnecessary processing and improving performance. (#112) diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index 5e12717d..30d00c1e 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -24,23 +24,31 @@ export interface PropertyRef extends ContribRef, CallbackRef, InputRef { property: string; } -export function handleHostStoreChange() { +/** + * This action is called once a host store change has been detected. + * Note this will only create callback requests for callbacks whose input + * value are affected by the change. + * + * @returns The array of callback requests made or `undefined`, + * if no callback requests have been made. + */ +export function handleHostStoreChange(): CallbackRequest[] | undefined { const { extensions, configuration, contributionsRecord } = store.getState(); const { hostStore } = configuration; if (!hostStore) { // Exit if no host store configured. // Actually, we should not come here. - return; + return undefined; } synchronizeThemeMode(hostStore); if (extensions.length === 0) { // Exit if there are no extensions (yet) - return; + return undefined; } const propertyRefs = getPropertyRefsForContribPoints(contributionsRecord); if (!propertyRefs || propertyRefs.length === 0) { // Exit if there are is nothing to be changed - return; + return undefined; } const callbackRequests = getCallbackRequests( propertyRefs, @@ -52,9 +60,12 @@ export function handleHostStoreChange() { (callbackRequest): callbackRequest is CallbackRequest => callbackRequest !== undefined, ); - if (filteredCallbackRequests && filteredCallbackRequests.length > 0) { - invokeCallbacks(filteredCallbackRequests); + if (!filteredCallbackRequests || !filteredCallbackRequests.length) { + return undefined; } + + invokeCallbacks(filteredCallbackRequests); + return filteredCallbackRequests; } // Exporting for testing only diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx index 741880e4..7ad89d90 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx @@ -12,6 +12,7 @@ import type { ContributionState } from "@/types/state/contribution"; describe("handleHostStoreChange", () => { let listeners: (() => void)[] = []; let hostState: Record = {}; + // noinspection JSUnusedGlobalSymbols const hostStore = { get: (key: string) => hostState[key], set: (key: string, value: unknown) => { @@ -181,12 +182,20 @@ describe("handleHostStoreChange", () => { }, }); hostStore.set("variableName", "CHL"); - handleHostStoreChange(); + expect(handleHostStoreChange()).toEqual([ + { + contribPoint: "panel", + contribIndex: 0, + callbackIndex: 0, + inputIndex: 0, + inputValues: ["CHL"], + property: "variableName", + }, + ]); - // calling it second time for coverage. No state change changes the - // control flow - handleHostStoreChange(); - // TODO: Update this test to assert the generated callback request + // calling it second time for coverage, + // because the no-state-change changes the control flow + expect(handleHostStoreChange()).toBeUndefined(); }); it("should memoize second call with same arguments", () => {