diff --git a/ui/package.json b/ui/package.json index 72218c7..e63432e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,7 +12,7 @@ "generate-types-local": "npx openapi-typescript http://localhost:8000/openapi.json -o bedbase-types.d.ts" }, "dependencies": { - "@databio/gtars": "^0.5.3", + "@databio/gtars": "/home/bnt4me/virginia/repos/gtars/gtars-wasm/pkg", "@tanstack/react-query": "^5.28.0", "@tanstack/react-query-devtools": "^5.28.0", "@tanstack/react-table": "^8.15.3", diff --git a/ui/src/components/bed-analytics-components/chromosome-stats-panel.tsx b/ui/src/components/bed-analytics-components/chromosome-stats-panel.tsx index 4bb423d..d537e7b 100644 --- a/ui/src/components/bed-analytics-components/chromosome-stats-panel.tsx +++ b/ui/src/components/bed-analytics-components/chromosome-stats-panel.tsx @@ -119,4 +119,4 @@ const ChromosomeStatsPanel: React.FC = ({ rs, selectedFile }) => { ); }; -export default ChromosomeStatsPanel; \ No newline at end of file +export default ChromosomeStatsPanel; diff --git a/ui/src/pages/bed-analytics.tsx b/ui/src/pages/bed-analytics.tsx index 7a42f3c..403a54c 100644 --- a/ui/src/pages/bed-analytics.tsx +++ b/ui/src/pages/bed-analytics.tsx @@ -1,8 +1,7 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { Layout } from '../components/layout.tsx'; import { RegionSet, ChromosomeStatistics } from '@databio/gtars'; -import { handleBedFileInput } from '../utils.ts'; import { bytesToSize } from '../utils.ts'; import ChromosomeStatsPanel from '../components/bed-analytics-components/chromosome-stats-panel.tsx'; import RegionDistributionPlot from '../components/bed-analytics-components/bed-plots.tsx'; @@ -15,6 +14,7 @@ type BedGenomeStats = components['schemas']['RefGenValidReturnModel']; export const BEDAnalytics = () => { const [rs, setRs] = useState(null); const [loadingRS, setLoadingRS] = useState(false); + const [progress, setProgress] = useState<{ percent: number; status: string } | null>(null); const [totalProcessingTime, setTotalProcessingTime] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [inputMode, setInputMode] = useState<'file' | 'url'>('file'); @@ -28,6 +28,43 @@ export const BEDAnalytics = () => { const analyzeGenomeMutation = useAnalyzeGenome(); const fileInputRef = useRef(null); + const workerRef = useRef(null); + const startTimeRef = useRef(0); + + // Initialize worker + useEffect(() => { + workerRef.current = new Worker(new URL('../workers/bedAnalyzerWorker.ts', import.meta.url), { + type: 'module', + }); + + workerRef.current.onmessage = (e) => { + const { type } = e.data; + + if (type === 'status') { + setProgress((prev) => ({ percent: prev?.percent ?? 0, status: e.data.message })); + } else if (type === 'progress') { + setProgress({ + percent: e.data.percent, + status: 'Processing file...', + }); + } else if (type === 'result') { + const endTime = performance.now(); + const regionSet = new RegionSet(e.data.entries); + setRs(regionSet); + setTotalProcessingTime(endTime - startTimeRef.current); + setLoadingRS(false); + setProgress(null); + } else if (type === 'error') { + console.error('Worker error:', e.data.message); + setLoadingRS(false); + setProgress(null); + } + }; + + return () => { + workerRef.current?.terminate(); + }; + }, []); useEffect(() => { const urlParam = searchParams.get('bedUrl'); @@ -38,73 +75,35 @@ export const BEDAnalytics = () => { }, [searchParams]); useEffect(() => { - initializeRegionSet(); + initializeAnalysis(); }, [triggerSearch]); - const fetchBedFromUrl = async (url: string): Promise => { - // console.log(`${url[0]}, ${url[1]}, ${url}`); - const fetchUrl = - url.length === 32 && !url.startsWith('http') - ? `https://api.bedbase.org/v1/files/files/${url[0]}/${url[1]}/${url}.bed.gz` - : url; - // console.log(`${fetchUrl}`); - const response = await fetch(fetchUrl); - if (!response.ok) { - throw new Error(`Failed to fetch BED file: ${response.statusText}`); - } - const blob = await response.blob(); - const fileName = fetchUrl.split('/').pop() || 'remote-bed-file.bed'; - return new File([blob], fileName, { type: 'text/plain' }); - }; - - const initializeRegionSet = async () => { - let fileToProcess: File | null = null; + const initializeAnalysis = useCallback(() => { + if (!workerRef.current) return; if (inputMode === 'file' && selectedFile) { - fileToProcess = selectedFile; + setLoadingRS(true); + setRs(null); + setTotalProcessingTime(null); + setProgress({ percent: 0, status: 'Starting...' }); + startTimeRef.current = performance.now(); + workerRef.current.postMessage({ file: selectedFile }); } else if (inputMode === 'url' && bedUrl.trim()) { - try { - fileToProcess = await fetchBedFromUrl(bedUrl.trim()); - } catch (error) { - console.error('Error fetching URL:', error); - return; - } - } - - if (fileToProcess) { setLoadingRS(true); + setRs(null); setTotalProcessingTime(null); - - try { - const startTime = performance.now(); - - const syntheticEvent = { - target: { files: [fileToProcess] }, - } as unknown as Event; - - await handleBedFileInput(syntheticEvent, (entries) => { - setTimeout(() => { - const rs = new RegionSet(entries); - const endTime = performance.now(); - const totalTimeMs = endTime - startTime; - - setRs(rs); - setTotalProcessingTime(totalTimeMs); - setLoadingRS(false); - }, 10); - }); - } catch (error) { - setLoadingRS(false); - console.error('Error loading file:', error); - } + setProgress({ percent: 0, status: 'Starting...' }); + startTimeRef.current = performance.now(); + workerRef.current.postMessage({ url: bedUrl.trim() }); } - }; + }, [inputMode, selectedFile, bedUrl]); const unloadFile = () => { setRs(null); setTotalProcessingTime(null); setSelectedFile(null); setBedUrl(''); + setProgress(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -303,27 +302,34 @@ export const BEDAnalytics = () => { -
- {loadingRS && ( -
-
- Loading... + {loadingRS && progress && ( +
+
+
+ Loading... +
+ {progress.status}
- - Loading and analyzing... - + {progress.percent >= 0 && ( +
+
+
+ )}
)} {rs && !loadingRS && ( -
+
- - Results ready - + Results ready
)} @@ -333,34 +339,30 @@ export const BEDAnalytics = () => {
- - - - - - - - - - - - - - - - - {/**/} - {/* */} - {/* */} - {/**/} - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
Identifier{rs.identifier}
Mean region width{rs.meanRegionWidth}
Total number of regions{rs.numberOfRegions}
Total number of nucleotides{rs.nucleotidesLength}
First row{rs.first_region}
Data Format{classify?.data_format}
BED compliance{classify?.bed_compliance}
Identifier{rs.identifier}
Mean region width{rs.meanRegionWidth}
Total number of regions{rs.numberOfRegions}
Total number of nucleotides{rs.nucleotidesLength}
Data Format{classify?.data_format}
BED compliance{classify?.bed_compliance}
@@ -413,4 +415,4 @@ export const BEDAnalytics = () => { )} ); -}; \ No newline at end of file +}; diff --git a/ui/src/workers/bedAnalyzerWorker.ts b/ui/src/workers/bedAnalyzerWorker.ts new file mode 100644 index 0000000..6a99430 --- /dev/null +++ b/ui/src/workers/bedAnalyzerWorker.ts @@ -0,0 +1,112 @@ +import init, { + bedParserNew, + bedParserUpdate, + bedParserFinish, + bedParserFree, +} from '@databio/gtars'; + +let wasmReady = false; + +async function ensureWasm() { + if (wasmReady) return; + await init(); + wasmReady = true; +} + +async function processStream( + reader: ReadableStreamDefaultReader, + totalSize: number, + parser: number, +) { + let bytesProcessed = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + bedParserUpdate(parser, value); + + bytesProcessed += value.length; + self.postMessage({ + type: 'progress', + bytesProcessed, + totalSize, + percent: totalSize > 0 ? Math.round((100 * bytesProcessed) / totalSize) : -1, + }); + } +} + +async function processFile(file: File) { + self.postMessage({ type: 'status', message: 'Loading WASM module...' }); + await ensureWasm(); + + const parser = bedParserNew(); + + try { + self.postMessage({ type: 'status', message: 'Processing file...' }); + + const stream = file.stream(); + const reader = stream.getReader(); + await processStream(reader, file.size, parser); + + self.postMessage({ type: 'status', message: 'Building RegionSet...' }); + const entries = bedParserFinish(parser); + + self.postMessage({ type: 'result', entries }); + } catch (err) { + bedParserFree(parser); + throw err; + } +} + +async function processUrl(url: string) { + self.postMessage({ type: 'status', message: 'Loading WASM module...' }); + await ensureWasm(); + + self.postMessage({ type: 'status', message: 'Fetching file...' }); + + const fetchUrl = + url.length === 32 && !url.startsWith('http') + ? `https://api.bedbase.org/v1/files/files/${url[0]}/${url[1]}/${url}.bed.gz` + : url; + + const response = await fetch(fetchUrl); + if (!response.ok) { + throw new Error(`Failed to fetch BED file: ${response.statusText}`); + } + + const parser = bedParserNew(); + + try { + self.postMessage({ type: 'status', message: 'Processing file...' }); + + const contentLength = response.headers.get('content-length'); + const totalSize = contentLength ? parseInt(contentLength, 10) : 0; + const reader = response.body!.getReader(); + await processStream(reader, totalSize, parser); + + self.postMessage({ type: 'status', message: 'Building RegionSet...' }); + const entries = bedParserFinish(parser); + + self.postMessage({ type: 'result', entries }); + } catch (err) { + bedParserFree(parser); + throw err; + } +} + +self.onmessage = async (e: MessageEvent) => { + const { file, url } = e.data; + + try { + if (file) { + await processFile(file); + } else if (url) { + await processUrl(url); + } else { + throw new Error('No file or URL provided'); + } + } catch (error) { + self.postMessage({ type: 'error', message: (error as Error).message }); + } +};