From 2bf36fa964bc353ca8107b8a66423e99c3b8c83d Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Mon, 3 Jun 2024 12:01:33 +0200 Subject: [PATCH 1/2] Internalise and streamline react-suspense-fetch --- apps/storybook/package.json | 1 - .../src/TiledHeatmapMesh/checkerboard-api.ts | 53 ++++---- .../src/TiledHeatmapMesh/mandlebrot-api.ts | 59 ++++----- packages/app/package.json | 1 - packages/app/src/providers/DataProvider.tsx | 16 +-- packages/app/src/providers/models.ts | 8 +- packages/app/vite.config.js | 3 - packages/h5wasm/vite.config.js | 3 - packages/shared/package.json | 1 + packages/shared/src/react-suspense-fetch.ts | 120 ++++++++++++++++++ pnpm-lock.yaml | 39 ++---- 11 files changed, 199 insertions(+), 105 deletions(-) create mode 100644 packages/shared/src/react-suspense-fetch.ts diff --git a/apps/storybook/package.json b/apps/storybook/package.json index ca33661b7..0ec92f8f2 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -25,7 +25,6 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-icons": "5.2.1", - "react-suspense-fetch": "0.4.1", "three": "0.164.1" }, "devDependencies": { diff --git a/apps/storybook/src/TiledHeatmapMesh/checkerboard-api.ts b/apps/storybook/src/TiledHeatmapMesh/checkerboard-api.ts index 05029f6cb..733af176d 100644 --- a/apps/storybook/src/TiledHeatmapMesh/checkerboard-api.ts +++ b/apps/storybook/src/TiledHeatmapMesh/checkerboard-api.ts @@ -1,9 +1,9 @@ import type { Size } from '@h5web/lib'; import { getLayerSizes, TilesApi } from '@h5web/lib'; +import { createFetchStore } from '@h5web/shared/react-suspense-fetch'; import greenlet from 'greenlet'; import type { NdArray } from 'ndarray'; import ndarray from 'ndarray'; -import { createFetchStore } from 'react-suspense-fetch'; import type { Vector2 } from 'three'; import { MathUtils } from 'three'; @@ -22,33 +22,30 @@ export class CheckerboardTilesApi extends TilesApi { public constructor(size: Size, tileSize: Size) { super(tileSize, getLayerSizes(size, tileSize)); - this.store = createFetchStore( - async (tile: TileParams) => { - const { layer, offset } = tile; - const layerSize = this.layerSizes[layer]; - - // Clip slice to size of the level - const width = MathUtils.clamp( - layerSize.width - offset.x, - 0, - this.tileSize.width, - ); - const height = MathUtils.clamp( - layerSize.height - offset.y, - 0, - this.tileSize.height, - ); - - const value = Math.abs( - (Math.floor(offset.x / this.tileSize.width) % 2) - - (Math.floor(offset.y / this.tileSize.height) % 2), - ); - - const arr = await getCheckerboardArray(width * height, value); - return ndarray(arr, [height, width]); - }, - { type: 'Map', areEqual: areTilesEqual }, - ); + this.store = createFetchStore(async (tile: TileParams) => { + const { layer, offset } = tile; + const layerSize = this.layerSizes[layer]; + + // Clip slice to size of the level + const width = MathUtils.clamp( + layerSize.width - offset.x, + 0, + this.tileSize.width, + ); + const height = MathUtils.clamp( + layerSize.height - offset.y, + 0, + this.tileSize.height, + ); + + const value = Math.abs( + (Math.floor(offset.x / this.tileSize.width) % 2) - + (Math.floor(offset.y / this.tileSize.height) % 2), + ); + + const arr = await getCheckerboardArray(width * height, value); + return ndarray(arr, [height, width]); + }, areTilesEqual); } public get(layer: number, offset: Vector2): NdArray { diff --git a/apps/storybook/src/TiledHeatmapMesh/mandlebrot-api.ts b/apps/storybook/src/TiledHeatmapMesh/mandlebrot-api.ts index a5010c99b..a764197e7 100644 --- a/apps/storybook/src/TiledHeatmapMesh/mandlebrot-api.ts +++ b/apps/storybook/src/TiledHeatmapMesh/mandlebrot-api.ts @@ -1,10 +1,10 @@ import type { Size } from '@h5web/lib'; import { getLayerSizes, TilesApi } from '@h5web/lib'; +import { createFetchStore } from '@h5web/shared/react-suspense-fetch'; import type { Domain } from '@h5web/shared/vis-models'; import greenlet from 'greenlet'; import type { NdArray } from 'ndarray'; import ndarray from 'ndarray'; -import { createFetchStore } from 'react-suspense-fetch'; import type { Vector2 } from 'three'; import { MathUtils } from 'three'; @@ -71,39 +71,36 @@ export class MandelbrotTilesApi extends TilesApi { this.xDomain = xDomain; this.yDomain = yDomain; - this.store = createFetchStore( - async (tile: TileParams) => { - const { layer, offset } = tile; - const layerSize = this.layerSizes[layer]; + this.store = createFetchStore(async (tile: TileParams) => { + const { layer, offset } = tile; + const layerSize = this.layerSizes[layer]; - // Clip slice to size of the level - const width = MathUtils.clamp( - layerSize.width - offset.x, - 0, - this.tileSize.width, - ); - const height = MathUtils.clamp( - layerSize.height - offset.y, - 0, - this.tileSize.height, - ); + // Clip slice to size of the level + const width = MathUtils.clamp( + layerSize.width - offset.x, + 0, + this.tileSize.width, + ); + const height = MathUtils.clamp( + layerSize.height - offset.y, + 0, + this.tileSize.height, + ); - const xScale = (this.xDomain[1] - this.xDomain[0]) / layerSize.width; - const xRange: Domain = [ - this.xDomain[0] + xScale * offset.x, - this.xDomain[0] + xScale * (offset.x + width), - ]; - const yScale = (this.yDomain[1] - this.yDomain[0]) / layerSize.height; - const yRange: Domain = [ - this.yDomain[0] + yScale * offset.y, - this.yDomain[0] + yScale * (offset.y + height), - ]; + const xScale = (this.xDomain[1] - this.xDomain[0]) / layerSize.width; + const xRange: Domain = [ + this.xDomain[0] + xScale * offset.x, + this.xDomain[0] + xScale * (offset.x + width), + ]; + const yScale = (this.yDomain[1] - this.yDomain[0]) / layerSize.height; + const yRange: Domain = [ + this.yDomain[0] + yScale * offset.y, + this.yDomain[0] + yScale * (offset.y + height), + ]; - const arr = await mandlebrot(50, xRange, yRange, width, height); - return ndarray(arr, [height, width]); - }, - { type: 'Map', areEqual: areTilesEqual }, - ); + const arr = await mandlebrot(50, xRange, yRange, width, height); + return ndarray(arr, [height, width]); + }, areTilesEqual); } public get(layer: number, offset: Vector2): NdArray { diff --git a/packages/app/package.json b/packages/app/package.json index 0c4fb7672..0c6124a8a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -60,7 +60,6 @@ "react-icons": "5.2.1", "react-reflex": "4.2.6", "react-slider": "2.0.4", - "react-suspense-fetch": "0.4.1", "three": "0.164.1", "zustand": "4.5.2" }, diff --git a/packages/app/src/providers/DataProvider.tsx b/packages/app/src/providers/DataProvider.tsx index fa5242749..4373e6d6f 100644 --- a/packages/app/src/providers/DataProvider.tsx +++ b/packages/app/src/providers/DataProvider.tsx @@ -1,9 +1,9 @@ import { isGroup } from '@h5web/shared/guards'; import type { ChildEntity, Entity, Group } from '@h5web/shared/hdf5-models'; import { getNameFromPath } from '@h5web/shared/hdf5-utils'; +import { createFetchStore } from '@h5web/shared/react-suspense-fetch'; import type { PropsWithChildren } from 'react'; import { createContext, useContext, useMemo } from 'react'; -import { createFetchStore } from 'react-suspense-fetch'; import { hasAttribute } from '../utils'; import type { DataProviderApi } from './api'; @@ -70,10 +70,8 @@ function DataProvider(props: PropsWithChildren) { }, [api]); const valuesStore = useMemo(() => { - const store = createFetchStore(api.getValue.bind(api), { - type: 'Map', - areEqual: (a, b) => - a.dataset.path === b.dataset.path && a.selection === b.selection, + const store = createFetchStore(api.getValue.bind(api), (a, b) => { + return a.dataset.path === b.dataset.path && a.selection === b.selection; }); return Object.assign(store, { @@ -88,10 +86,10 @@ function DataProvider(props: PropsWithChildren) { }, [api]); const attrValuesStore = useMemo(() => { - const store = createFetchStore(api.getAttrValues.bind(api), { - type: 'Map', - areEqual: (a, b) => a.path === b.path, - }); + const store = createFetchStore( + api.getAttrValues.bind(api), + (a, b) => a.path === b.path, + ); return Object.assign(store, { getSingle: (entity: Entity, attrName: AttrName) => { diff --git a/packages/app/src/providers/models.ts b/packages/app/src/providers/models.ts index 16cbad752..3da3bdf6a 100644 --- a/packages/app/src/providers/models.ts +++ b/packages/app/src/providers/models.ts @@ -6,16 +6,16 @@ import type { ProvidedEntity, ScalarShape, } from '@h5web/shared/hdf5-models'; -import type { FetchStore } from 'react-suspense-fetch'; +import type { FetchStore } from '@h5web/shared/react-suspense-fetch'; import type { ImageAttribute } from '../vis-packs/core/models'; import type { NxAttribute } from '../vis-packs/nexus/models'; -export type EntitiesStore = FetchStore; +export type EntitiesStore = FetchStore; export type AttrName = NxAttribute | ImageAttribute | '_FillValue'; -export interface ValuesStore extends FetchStore { +export interface ValuesStore extends FetchStore { cancelOngoing: () => void; evictCancelled: () => void; } @@ -25,7 +25,7 @@ export interface ValuesStoreParams { selection?: string | undefined; } -export interface AttrValuesStore extends FetchStore { +export interface AttrValuesStore extends FetchStore { getSingle: (entity: Entity, attrName: AttrName) => unknown; } diff --git a/packages/app/vite.config.js b/packages/app/vite.config.js index 488e2b90e..5ce99140a 100644 --- a/packages/app/vite.config.js +++ b/packages/app/vite.config.js @@ -37,8 +37,5 @@ export default defineProject({ env: loadEnv('test', import.meta.dirname, 'VITEST_'), restoreMocks: true, testTimeout: 15_000, - server: { - deps: { inline: ['react-suspense-fetch'] }, - }, }, }); diff --git a/packages/h5wasm/vite.config.js b/packages/h5wasm/vite.config.js index 1387dc13f..65a2a1068 100644 --- a/packages/h5wasm/vite.config.js +++ b/packages/h5wasm/vite.config.js @@ -31,8 +31,5 @@ export default defineProject({ }, test: { env: loadEnv('test', import.meta.dirname, 'VITEST_'), - server: { - deps: { inline: ['react-suspense-fetch'] }, - }, }, }); diff --git a/packages/shared/package.json b/packages/shared/package.json index e48012c9e..7574a085a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -18,6 +18,7 @@ "./mock-models": "./src/mock-models.ts", "./mock-utils": "./src/mock-utils.ts", "./mock-values": "./src/mock-values.ts", + "./react-suspense-fetch": "./src/react-suspense-fetch.ts", "./styles.css": "./src/styles.css", ".": "./src/index.ts" }, diff --git a/packages/shared/src/react-suspense-fetch.ts b/packages/shared/src/react-suspense-fetch.ts new file mode 100644 index 000000000..9e2371d82 --- /dev/null +++ b/packages/shared/src/react-suspense-fetch.ts @@ -0,0 +1,120 @@ +type FetchFunc = ( + input: Input, + options: { signal: AbortSignal }, +) => Promise; + +type AreEqual = (a: Input, b: Input) => boolean; + +interface Instance { + get: () => Result; + abort: () => void; +} + +export interface FetchStore { + prefetch: (input: Input) => void; + get: (input: Input) => Result; + evict: (input: Input) => void; + abort: (input: Input) => void; +} + +export function createFetchStore( + fetchFunc: FetchFunc, + areEqual?: AreEqual, +) { + const cache = createCache(areEqual); + + return { + prefetch: (input: Input): void => { + if (!cache.has(input)) { + cache.set(input, createInstance(input, fetchFunc)); + } + }, + get: (input: Input): Result => { + const instance = cache.get(input) || createInstance(input, fetchFunc); + cache.set(input, instance); + return instance.get(); + }, + evict: (input: Input): void => { + cache.delete(input); + }, + abort: (input: Input): void => { + cache.get(input)?.abort(); + }, + }; +} + +function createCache(areEqual?: AreEqual) { + if (!areEqual) { + return new Map>(); + } + + return createMapLikeWithComparator>(areEqual); +} + +function createMapLikeWithComparator(areEqual: AreEqual) { + const map = new Map(); + + return { + set: (key: K, value: V) => { + map.set(key, value); + }, + has: (key: K) => { + for (const [k] of map) { + if (areEqual(k, key)) { + return true; + } + } + return false; + }, + get: (key: K) => { + for (const [k, v] of map) { + if (areEqual(k, key)) { + return v; + } + } + return undefined; + }, + delete: (key: K) => { + for (const [k] of map) { + if (areEqual(k, key)) { + map.delete(k); + } + } + }, + }; +} + +function createInstance( + input: Input, + fetchFunc: FetchFunc, +): Instance { + let promise: Promise | null = null; + let result: Result | null = null; + let error: unknown = null; + const controller = new AbortController(); + + promise = (async () => { + try { + result = await fetchFunc(input, { signal: controller.signal }); + } catch (error_) { + error = error_; + } finally { + promise = null; + } + })(); + + return { + get: () => { + if (promise) { + throw promise; // eslint-disable-line @typescript-eslint/no-throw-literal + } + if (error !== null) { + throw error; // eslint-disable-line @typescript-eslint/no-throw-literal + } + return result as Result; + }, + abort: () => { + controller.abort(); + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 827d396f3..4dde40242 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,9 +156,6 @@ importers: react-icons: specifier: 5.2.1 version: 5.2.1(react@18.3.1) - react-suspense-fetch: - specifier: 0.4.1 - version: 0.4.1 three: specifier: 0.164.1 version: 0.164.1 @@ -259,9 +256,6 @@ importers: react-slider: specifier: 2.0.4 version: 2.0.4(react@18.3.1) - react-suspense-fetch: - specifier: 0.4.1 - version: 0.4.1 three: specifier: 0.164.1 version: 0.164.1 @@ -5645,9 +5639,6 @@ packages: peerDependencies: react: ^16 || ^17 || ^18 - react-suspense-fetch@0.4.1: - resolution: {integrity: sha512-Kc8VzZUjDjvWfoOBzPEhniaJwgwOPqW0x94ec8e3GGhLe6SlZDU2YhYgoLqM9L8xzXeGR6nhP7/PnjvI1KoTlA==} - react-use-measure@2.1.1: resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} peerDependencies: @@ -9243,7 +9234,7 @@ snapshots: '@types/node': 20.12.11 optional: true - '@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3)': + '@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) @@ -9258,7 +9249,7 @@ snapshots: semver: 7.6.0 tsutils: 3.21.0(typescript@5.0.3) optionalDependencies: - typescript: 5.0.3 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -10587,17 +10578,17 @@ snapshots: '@babel/eslint-parser': 7.21.3(@babel/core@7.21.4)(eslint@8.57.0) '@babel/preset-react': 7.18.6(@babel/core@7.21.4) '@next/eslint-plugin-next': 13.2.4 - '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) + '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) confusing-browser-globals: 1.0.11 eslint: 8.57.0 eslint-config-prettier: 8.8.0(eslint@8.57.0) eslint-import-resolver-jsconfig: 1.1.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0) eslint-plugin-etc: 2.0.2(eslint@8.57.0)(typescript@5.0.3) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0) + eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) eslint-plugin-jest-dom: 4.0.3(eslint@8.57.0) eslint-plugin-jest-formatting: 3.1.0(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.0) @@ -10647,12 +10638,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0): dependencies: debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0) get-tsconfig: 4.7.3 globby: 13.2.2 is-core-module: 2.13.1 @@ -10661,14 +10652,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -10685,7 +10676,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.flat: 1.3.2 @@ -10694,7 +10685,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0))(eslint@8.57.0) has: 1.0.4 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10721,12 +10712,12 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-jest@27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3): + eslint-plugin-jest@27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.0.3) eslint: 8.57.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) + '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) transitivePeerDependencies: - supports-color - typescript @@ -12942,8 +12933,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-suspense-fetch@0.4.1: {} - react-use-measure@2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: debounce: 1.2.1 From 5fc96392ee08b690a71f1da03f396e5cd4ae673f Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Mon, 3 Jun 2024 15:02:07 +0200 Subject: [PATCH 2/2] Cache child entities in fetch store instead of custom cache --- packages/app/src/providers/DataProvider.tsx | 11 ++----- packages/shared/src/react-suspense-fetch.ts | 33 +++++++++++---------- pnpm-lock.yaml | 28 ++++++++--------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/packages/app/src/providers/DataProvider.tsx b/packages/app/src/providers/DataProvider.tsx index 4373e6d6f..34eba352d 100644 --- a/packages/app/src/providers/DataProvider.tsx +++ b/packages/app/src/providers/DataProvider.tsx @@ -1,5 +1,5 @@ import { isGroup } from '@h5web/shared/guards'; -import type { ChildEntity, Entity, Group } from '@h5web/shared/hdf5-models'; +import type { Entity } from '@h5web/shared/hdf5-models'; import { getNameFromPath } from '@h5web/shared/hdf5-utils'; import { createFetchStore } from '@h5web/shared/react-suspense-fetch'; import type { PropsWithChildren } from 'react'; @@ -43,21 +43,14 @@ function DataProvider(props: PropsWithChildren) { const { api, children } = props; const entitiesStore = useMemo(() => { - const childCache = new Map>(); - const store = createFetchStore(async (path: string) => { - const cachedEntity = childCache.get(path); - if (cachedEntity) { - return cachedEntity; - } - const entity = await api.getEntity(path); if (isGroup(entity)) { // Cache non-group children (datasets, datatypes and links) entity.children.forEach((child) => { if (!isGroup(child)) { - childCache.set(child.path, child); + store.preset(child.path, child); } }); } diff --git a/packages/shared/src/react-suspense-fetch.ts b/packages/shared/src/react-suspense-fetch.ts index 9e2371d82..2d02d5449 100644 --- a/packages/shared/src/react-suspense-fetch.ts +++ b/packages/shared/src/react-suspense-fetch.ts @@ -13,15 +13,16 @@ interface Instance { export interface FetchStore { prefetch: (input: Input) => void; get: (input: Input) => Result; + preset: (input: Input, result: Result) => void; evict: (input: Input) => void; abort: (input: Input) => void; } export function createFetchStore( fetchFunc: FetchFunc, - areEqual?: AreEqual, + areEqual: AreEqual = Object.is, ) { - const cache = createCache(areEqual); + const cache = createCache>(areEqual); return { prefetch: (input: Input): void => { @@ -34,6 +35,14 @@ export function createFetchStore( cache.set(input, instance); return instance.get(); }, + preset: (input: Input, result: Result): void => { + cache.set(input, { + get: () => result, + abort: () => { + /* noop */ + }, + }); + }, evict: (input: Input): void => { cache.delete(input); }, @@ -43,15 +52,7 @@ export function createFetchStore( }; } -function createCache(areEqual?: AreEqual) { - if (!areEqual) { - return new Map>(); - } - - return createMapLikeWithComparator>(areEqual); -} - -function createMapLikeWithComparator(areEqual: AreEqual) { +function createCache(areEqual: AreEqual) { const map = new Map(); return { @@ -88,9 +89,9 @@ function createInstance( input: Input, fetchFunc: FetchFunc, ): Instance { - let promise: Promise | null = null; - let result: Result | null = null; - let error: unknown = null; + let promise: Promise | undefined; + let result: Result | undefined; + let error: unknown; const controller = new AbortController(); promise = (async () => { @@ -99,7 +100,7 @@ function createInstance( } catch (error_) { error = error_; } finally { - promise = null; + promise = undefined; } })(); @@ -108,7 +109,7 @@ function createInstance( if (promise) { throw promise; // eslint-disable-line @typescript-eslint/no-throw-literal } - if (error !== null) { + if (error !== undefined) { throw error; // eslint-disable-line @typescript-eslint/no-throw-literal } return result as Result; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dde40242..341b44252 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9234,7 +9234,7 @@ snapshots: '@types/node': 20.12.11 optional: true - '@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) @@ -9249,7 +9249,7 @@ snapshots: semver: 7.6.0 tsutils: 3.21.0(typescript@5.0.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.0.3 transitivePeerDependencies: - supports-color @@ -10578,17 +10578,17 @@ snapshots: '@babel/eslint-parser': 7.21.3(@babel/core@7.21.4)(eslint@8.57.0) '@babel/preset-react': 7.18.6(@babel/core@7.21.4) '@next/eslint-plugin-next': 13.2.4 - '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) confusing-browser-globals: 1.0.11 eslint: 8.57.0 eslint-config-prettier: 8.8.0(eslint@8.57.0) eslint-import-resolver-jsconfig: 1.1.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-etc: 2.0.2(eslint@8.57.0)(typescript@5.0.3) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0) - eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) eslint-plugin-jest-dom: 4.0.3(eslint@8.57.0) eslint-plugin-jest-formatting: 3.1.0(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.0) @@ -10638,12 +10638,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0): + eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.16.0 eslint: 8.57.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) get-tsconfig: 4.7.3 globby: 13.2.2 is-core-module: 2.13.1 @@ -10652,14 +10652,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 5.56.0(eslint@8.57.0)(typescript@5.0.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -10676,7 +10676,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4)(eslint@8.57.0): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.flat: 1.3.2 @@ -10685,7 +10685,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.4(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) has: 1.0.4 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10712,12 +10712,12 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-jest@27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3): + eslint-plugin-jest@27.2.1(@typescript-eslint/eslint-plugin@5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.0.3) eslint: 8.57.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 5.56.0(@typescript-eslint/parser@5.56.0(eslint@8.57.0)(typescript@5.0.3))(eslint@8.57.0)(typescript@5.0.3) transitivePeerDependencies: - supports-color - typescript