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

Implement H5wasmLocalFileProvider #1604

Merged
merged 1 commit into from
Apr 9, 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
34 changes: 8 additions & 26 deletions apps/demo/src/h5wasm/DropZone.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,35 @@
import { useCallback, useState } from 'react';
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

import styles from './H5WasmApp.module.css';
import type { H5File } from './models';
import UrlForm from './UrlForm';

const EXT = ['.h5', '.hdf5', '.hdf', '.nx', '.nx5', '.nexus', '.nxs', '.cxi'];

interface Props {
onH5File: (h5File: H5File) => void;
onDrop: (file: File) => void;
}

function DropZone(props: Props) {
const { onH5File } = props;

const [isReadingFile, setReadingFile] = useState(false);
function DropZone(props: PropsWithChildren<Props>) {
const { onDrop, children } = props;

const onDropAccepted = useCallback(
([file]: File[]) => {
const reader = new FileReader();
reader.addEventListener('abort', () => setReadingFile(false));
reader.addEventListener('error', () => setReadingFile(false));
reader.addEventListener('load', () => {
onH5File({ filename: file.name, buffer: reader.result as ArrayBuffer });
});

reader.readAsArrayBuffer(file);
setReadingFile(true);
},
[onH5File],
([file]: File[]) => onDrop(file),
[onDrop],
loichuder marked this conversation as resolved.
Show resolved Hide resolved
);

const { getRootProps, getInputProps, open, isDragActive, fileRejections } =
useDropzone({
multiple: false,
noClick: true,
noKeyboard: true,
disabled: isReadingFile,
onDropAccepted,
});

return (
<div
{...getRootProps({
className: isDragActive ? styles.activeDropZone : styles.dropZone,
'data-disabled': isReadingFile || undefined,
})}
>
<input {...getInputProps()} accept={EXT.join(',')} />
Expand All @@ -62,10 +47,7 @@ function DropZone(props: Props) {
Please drop a single file
</p>
)}
<p className={styles.fromUrl}>
You may also provide a URL if your file is hosted remotely:
</p>
<UrlForm onH5File={onH5File} />
{children}
</div>
Comment on lines -65 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small refactoring here to pass UrlForm as a child for better composition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it is really needed. DropZone is not used elsewhere ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it just felt weird having the UrlForm rendered by DropZone directly. They both allow providing a file but in different ways, so I though it was better to have them both being rendered from H5WasmApp.

</div>
</div>
Expand Down
25 changes: 21 additions & 4 deletions apps/demo/src/h5wasm/H5WasmApp.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import { App } from '@h5web/app';
import { H5WasmProvider } from '@h5web/h5wasm';
import { H5WasmLocalFileProvider, H5WasmProvider } from '@h5web/h5wasm';
import { useState } from 'react';
import { useSearch } from 'wouter';

import { getFeedbackURL } from '../utils';
import DropZone from './DropZone';
import type { H5File } from './models';
import styles from './H5WasmApp.module.css';
import type { RemoteFile } from './models';
import { getPlugin } from './plugin-utils';
import UrlForm from './UrlForm';

function H5WasmApp() {
const query = new URLSearchParams(useSearch());
const [h5File, setH5File] = useState<H5File>();
const [h5File, setH5File] = useState<File | RemoteFile>();

if (!h5File) {
return <DropZone onH5File={setH5File} />;
return (
<DropZone onDrop={setH5File}>
<p className={styles.fromUrl}>
You may also provide a URL if your file is hosted remotely:
</p>
<UrlForm onLoad={setH5File} />
</DropZone>
);
}

if (h5File instanceof File) {
return (
<H5WasmLocalFileProvider file={h5File}>
<App sidebarOpen={!query.has('wide')} getFeedbackURL={getFeedbackURL} />
</H5WasmLocalFileProvider>
);
}

return (
Expand Down
13 changes: 8 additions & 5 deletions apps/demo/src/h5wasm/UrlForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { FiLoader } from 'react-icons/fi';
import { useLocation, useSearch } from 'wouter';

import styles from './H5WasmApp.module.css';
import type { H5File } from './models';
import type { RemoteFile } from './models';

interface Props {
onH5File: (h5File: H5File) => void;
onLoad: (h5File: RemoteFile) => void;
}

function UrlForm(props: Props) {
const { onH5File } = props;
const { onLoad } = props;

const [, navigate] = useLocation();
const query = new URLSearchParams(useSearch());
Expand All @@ -26,9 +26,12 @@ function UrlForm(props: Props) {
const fetchFile = useCallback(async () => {
if (url) {
const { data } = await execute();
onH5File({ filename: url.slice(url.lastIndexOf('/') + 1), buffer: data });
onLoad({
filename: url.slice(url.lastIndexOf('/') + 1),
buffer: data,
});
}
}, [url, execute, onH5File]);
}, [url, execute, onLoad]);

useEffect(() => {
void fetchFile();
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/h5wasm/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface H5File {
export interface RemoteFile {
filename: string;
buffer: ArrayBuffer;
}
3 changes: 2 additions & 1 deletion packages/h5wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
}
},
"dependencies": {
"h5wasm": "0.7.3",
"comlink": "4.4.1",
"h5wasm": "0.7.4",
"nanoid": "5.0.6"
},
"devDependencies": {
Expand Down
18 changes: 9 additions & 9 deletions packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1786,8 +1786,8 @@ exports[`test file matches snapshot 1`] = `
0,
0,
0,
176,
18,
48,
19,
19,
0,
],
Expand All @@ -1802,7 +1802,7 @@ exports[`test file matches snapshot 1`] = `
0,
0,
0,
32,
160,
41,
19,
0,
Expand All @@ -1818,7 +1818,7 @@ exports[`test file matches snapshot 1`] = `
0,
0,
0,
56,
184,
41,
19,
0,
Expand Down Expand Up @@ -2095,7 +2095,7 @@ exports[`test file matches snapshot 1`] = `
0,
0,
0,
88,
216,
238,
18,
0,
Expand Down Expand Up @@ -2126,24 +2126,24 @@ exports[`test file matches snapshot 1`] = `
0,
0,
0,
16,
144,
244,
18,
0,
2,
0,
0,
0,
120,
248,
66,
18,
0,
3,
0,
0,
0,
168,
42,
40,
43,
19,
0,
],
Expand Down
1 change: 1 addition & 0 deletions packages/h5wasm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as H5WasmProvider } from './H5WasmProvider';
export { default as H5WasmLocalFileProvider } from './local/H5WasmLocalFileProvider';
export { Plugin } from './utils';
25 changes: 25 additions & 0 deletions packages/h5wasm/src/local/H5WasmLocalFileProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DataProvider } from '@h5web/app';
import type { PropsWithChildren } from 'react';
import { useMemo, useState } from 'react';

import { H5WasmLocalFileApi } from './h5wasm-local-file-api';

interface Props {
file: File;
}

function H5WasmLocalFileProvider(props: PropsWithChildren<Props>) {
loichuder marked this conversation as resolved.
Show resolved Hide resolved
const { file, children } = props;

const api = useMemo(() => new H5WasmLocalFileApi(file), [file]);

const [prevApi, setPrevApi] = useState(api);
if (prevApi !== api) {
setPrevApi(api);
void prevApi.cleanUp(); // https://github.com/silx-kit/h5web/pull/1568
}

return <DataProvider api={api}>{children}</DataProvider>;
}

export default H5WasmLocalFileProvider;
59 changes: 59 additions & 0 deletions packages/h5wasm/src/local/h5wasm-local-file-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ValuesStoreParams } from '@h5web/app';
import { DataProviderApi } from '@h5web/app';
import type {
AttributeValues,
Entity,
ProvidedEntity,
} from '@h5web/shared/hdf5-models';
import type { Remote } from 'comlink';

import { getEnhancedError, hasBigInts, sanitizeBigInts } from '../utils';
import { getH5WasmRemote } from './remote';
import type { H5WasmWorkerAPI } from './worker';

export class H5WasmLocalFileApi extends DataProviderApi {
private readonly remote: Remote<H5WasmWorkerAPI>;
private readonly fileId: Promise<bigint>;

public constructor(file: File) {
super(file.name);

this.remote = getH5WasmRemote();
loichuder marked this conversation as resolved.
Show resolved Hide resolved
this.fileId = this.remote.openFile(file);
loichuder marked this conversation as resolved.
Show resolved Hide resolved
}

public override async getEntity(path: string): Promise<ProvidedEntity> {
return this.remote.getEntity(await this.fileId, path);
}

public override async getValue(params: ValuesStoreParams): Promise<unknown> {
const { dataset, selection } = params;
const fileId = await this.fileId;

try {
const value = await this.remote.getValue(fileId, dataset.path, selection);
return hasBigInts(dataset.type) ? sanitizeBigInts(value) : value;
} catch (error) {
throw getEnhancedError(error);
}
}

public override async getAttrValues(
entity: Entity,
): Promise<AttributeValues> {
const fileId = await this.fileId;

return Object.fromEntries(
await Promise.all(
entity.attributes.map<Promise<[string, unknown]>>(async ({ name }) => [
name,
await this.remote.getAttrValue(fileId, entity.path, name),
]),
),
);
}

public async cleanUp(): Promise<number> {
return this.remote.closeFile(await this.fileId);
}
}
19 changes: 19 additions & 0 deletions packages/h5wasm/src/local/remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Remote } from 'comlink';
import { wrap } from 'comlink';

import type { H5WasmWorkerAPI } from './worker';

let remote: Remote<H5WasmWorkerAPI>;

export function getH5WasmRemote() {
if (remote) {
return remote;
}

const worker = new Worker(new URL('worker.ts', import.meta.url), {
type: 'module',
});

remote = wrap<H5WasmWorkerAPI>(worker);
return remote;
}
Loading
Loading