Skip to content

Commit

Permalink
Implement H5wasmLocalFileProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Apr 8, 2024
1 parent 23d426d commit fba632e
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 50 deletions.
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],
);

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>
</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>) {
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();
this.fileId = this.remote.openFile(file);
}

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

0 comments on commit fba632e

Please sign in to comment.