Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use zustand store inside suspense fetch store to track progress #1693

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/app/src/__tests__/Explorer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react';
import { expect, test } from 'vitest';

import { SLOW_TIMEOUT } from '../providers/mock/mock-api';
import { SLOW_TIMEOUT } from '../providers/mock/utils';
import { renderApp } from '../test-utils';

test('select root group by default', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/__tests__/NexusPack.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react';
import { expect, test } from 'vitest';

import { SLOW_TIMEOUT } from '../providers/mock/mock-api';
import { SLOW_TIMEOUT } from '../providers/mock/utils';
import {
getSelectedVisTab,
getVisTabs,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/__tests__/Visualizer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react';
import { expect, test } from 'vitest';

import { SLOW_TIMEOUT } from '../providers/mock/mock-api';
import { SLOW_TIMEOUT } from '../providers/mock/utils';
import { mockConsoleMethod, renderApp } from '../test-utils';
import { Vis } from '../vis-packs/core/visualizations';

Expand Down
5 changes: 0 additions & 5 deletions packages/app/src/providers/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
AttrName,
AttrValuesStore,
EntitiesStore,
ProgressCallback,
ValuesStore,
} from './models';

Expand All @@ -24,8 +23,6 @@ export interface DataContextValue {

// Undocumented
getExportURL?: DataProviderApi['getExportURL'];
addProgressListener: (cb: ProgressCallback) => void;
removeProgressListener: (cb: ProgressCallback) => void;
getSearchablePaths?: DataProviderApi['getSearchablePaths'];
}

Expand Down Expand Up @@ -92,8 +89,6 @@ function DataProvider(props: PropsWithChildren<Props>) {
valuesStore,
attrValuesStore,
getExportURL: api.getExportURL?.bind(api),
addProgressListener: api.addProgressListener.bind(api),
removeProgressListener: api.removeProgressListener.bind(api),
getSearchablePaths: api.getSearchablePaths?.bind(api),
}}
>
Expand Down
50 changes: 13 additions & 37 deletions packages/app/src/providers/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable promise/prefer-await-to-callbacks */
import type {
ArrayShape,
AttributeValues,
Expand All @@ -7,6 +6,7 @@ import type {
ProvidedEntity,
Value,
} from '@h5web/shared/hdf5-models';
import type { OnProgress } from '@h5web/shared/react-suspense-fetch';
import type {
AxiosInstance,
AxiosProgressEvent,
Expand All @@ -16,18 +16,10 @@ import type {
} from 'axios';
import axios from 'axios';

import type {
ExportFormat,
ExportURL,
ProgressCallback,
ValuesStoreParams,
} from './models';
import type { ExportFormat, ExportURL, ValuesStoreParams } from './models';

export abstract class DataProviderApi {
protected readonly client: AxiosInstance;
protected progress = new Map<ValuesStoreParams, number>();

private readonly progressListeners = new Set<ProgressCallback>();

public constructor(
public readonly filepath: string,
Expand All @@ -53,36 +45,25 @@ export abstract class DataProviderApi {

public getSearchablePaths?(path: string): Promise<string[]>;

public addProgressListener(cb: ProgressCallback): void {
this.progressListeners.add(cb);
cb([...this.progress.values()]); // notify once
}

public removeProgressListener(cb: ProgressCallback): void {
this.progressListeners.delete(cb);
}

protected async cancellableFetchValue(
endpoint: string,
storeParams: ValuesStoreParams,
queryParams: Record<string, string | boolean | undefined>,
signal?: AbortSignal,
onProgress?: OnProgress,
responseType?: ResponseType,
): Promise<AxiosResponse> {
this.progress.set(storeParams, 0);
this.notifyProgressChange();

try {
return await this.client.get(endpoint, {
signal,
params: queryParams,
responseType,
onDownloadProgress: (evt: AxiosProgressEvent) => {
if (evt.total !== undefined && evt.total > 0) {
this.progress.set(storeParams, evt.loaded / evt.total);
this.notifyProgressChange();
}
},
onDownloadProgress:
onProgress &&
((evt: AxiosProgressEvent) => {
if (evt.total !== undefined && evt.total > 0) {
onProgress(evt.loaded / evt.total);
}
}),
});
} catch (error) {
if (axios.isCancel(error)) {
Expand All @@ -93,21 +74,16 @@ export abstract class DataProviderApi {
);
}
throw error;
} finally {
// Remove progress when request fulfills
this.progress.delete(storeParams);
this.notifyProgressChange();
}
}

private notifyProgressChange() {
this.progressListeners.forEach((cb) => cb([...this.progress.values()]));
}

public abstract getEntity(path: string): Promise<ProvidedEntity>;

public abstract getValue(
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
loichuder marked this conversation as resolved.
Show resolved Hide resolved
): Promise<unknown>;

public abstract getAttrValues(entity: Entity): Promise<AttributeValues>;
}
21 changes: 16 additions & 5 deletions packages/app/src/providers/h5grove/h5grove-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
Value,
} from '@h5web/shared/hdf5-models';
import { DTypeClass } from '@h5web/shared/hdf5-models';
import type { OnProgress } from '@h5web/shared/react-suspense-fetch';
import type { AxiosRequestConfig } from 'axios';

import { DataProviderApi } from '../api';
Expand Down Expand Up @@ -44,21 +45,29 @@ export class H5GroveApi extends DataProviderApi {
public override async getValue(
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
): Promise<H5GroveDataResponse> {
const { dataset } = params;

if (dataset.type.class === DTypeClass.Opaque) {
return new Uint8Array(await this.fetchBinaryData(params, signal));
return new Uint8Array(
await this.fetchBinaryData(params, signal, onProgress),
);
}

const DTypedArray = h5groveTypedArrayFromDType(dataset.type);
if (DTypedArray) {
const buffer = await this.fetchBinaryData(params, signal, true);
const buffer = await this.fetchBinaryData(
params,
signal,
onProgress,
true,
);
const array = new DTypedArray(buffer);
return hasScalarShape(dataset) ? array[0] : array;
}

return this.fetchData(params, signal);
return this.fetchData(params, signal, onProgress);
}

public override async getAttrValues(
Expand Down Expand Up @@ -146,16 +155,17 @@ export class H5GroveApi extends DataProviderApi {
private async fetchData(
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
): Promise<H5GroveDataResponse> {
const { data } = await this.cancellableFetchValue(
`/data/`,
params,
{
path: params.dataset.path,
selection: params.selection,
flatten: true,
},
signal,
onProgress,
);

return data;
Expand All @@ -164,18 +174,19 @@ export class H5GroveApi extends DataProviderApi {
private async fetchBinaryData(
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
safe = false,
): Promise<ArrayBuffer> {
const { data } = await this.cancellableFetchValue(
'/data/',
params,
{
path: params.dataset.path,
selection: params.selection,
format: 'bin',
dtype: safe ? 'safe' : undefined,
},
signal,
onProgress,
'arraybuffer',
);

Expand Down
7 changes: 5 additions & 2 deletions packages/app/src/providers/hsds/hsds-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from '@h5web/shared/hdf5-models';
import { EntityKind } from '@h5web/shared/hdf5-models';
import { buildEntityPath, getChildEntity } from '@h5web/shared/hdf5-utils';
import type { OnProgress } from '@h5web/shared/react-suspense-fetch';

import { DataProviderApi } from '../api';
import type { ExportFormat, ExportURL, ValuesStoreParams } from '../models';
Expand Down Expand Up @@ -127,11 +128,12 @@ export class HsdsApi extends DataProviderApi {
public override async getValue(
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
): Promise<unknown> {
const { dataset } = params;
assertHsdsDataset(dataset);

const value = await this.fetchValue(dataset.id, params, signal);
const value = await this.fetchValue(dataset.id, params, signal, onProgress);

// https://github.com/HDFGroup/hsds/issues/88
// HSDS does not reduce the number of dimensions when selecting indices
Expand Down Expand Up @@ -217,13 +219,14 @@ export class HsdsApi extends DataProviderApi {
entityId: HsdsId,
params: ValuesStoreParams,
signal?: AbortSignal,
onProgress?: OnProgress,
): Promise<HsdsValueResponse> {
const { selection } = params;
const { data } = await this.cancellableFetchValue(
`/datasets/${entityId}/value`,
params,
{ select: selection && `[${selection}]` },
signal,
onProgress,
);
return data.value;
}
Expand Down
53 changes: 10 additions & 43 deletions packages/app/src/providers/mock/mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
assertArrayShape,
assertDefined,
hasNumericType,
isGroup,
} from '@h5web/shared/guards';
import type {
ArrayShape,
Expand All @@ -22,9 +21,13 @@ import {
import { DataProviderApi } from '../api';
import type { ExportFormat, ExportURL, ValuesStoreParams } from '../models';
import { makeMockFile } from './mock-file';
import { findMockEntity, sliceValue } from './utils';

export const SLOW_TIMEOUT = 3000;
import {
cancellableDelay,
findMockEntity,
getChildrenPaths,
sliceValue,
SLOW_TIMEOUT,
} from './utils';

export class MockApi extends DataProviderApi {
private readonly mockFile: GroupWithChildren;
Expand All @@ -33,6 +36,7 @@ export class MockApi extends DataProviderApi {
private readonly _getExportURL?: DataProviderApi['getExportURL'],
) {
const mockFile = makeMockFile();

super(mockFile.name);
this.mockFile = mockFile;
}
Expand Down Expand Up @@ -65,7 +69,7 @@ export class MockApi extends DataProviderApi {
}

if (dataset.name.startsWith('slow')) {
await this.cancellableDelay(signal);
await cancellableDelay(signal);
}

const { value } = dataset;
Expand Down Expand Up @@ -132,43 +136,6 @@ export class MockApi extends DataProviderApi {
}

public override async getSearchablePaths(path: string): Promise<string[]> {
return this.getEntityPaths(path);
}

private getEntityPaths(entityPath: string): string[] {
loichuder marked this conversation as resolved.
Show resolved Hide resolved
const entity = findMockEntity(this.mockFile, entityPath);
if (!entity) {
return [];
}

if (!isGroup(entity)) {
return [entity.path];
}

return entity.children.reduce<string[]>(
(acc, child) => [...acc, ...this.getEntityPaths(child.path)],
[entity.path],
);
}

private async cancellableDelay(signal?: AbortSignal) {
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
signal?.removeEventListener('abort', handleAbort);
resolve();
}, SLOW_TIMEOUT);

function handleAbort() {
clearTimeout(timeout);
signal?.removeEventListener('abort', handleAbort);
reject(
new Error(
typeof signal?.reason === 'string' ? signal.reason : 'cancelled',
),
);
}

signal?.addEventListener('abort', handleAbort);
});
return getChildrenPaths(this.mockFile, path);
}
}
Loading