// React hook that owns the simulation worker: start a run, stream progress, // cancel by terminating the worker (à la seqlab's runner). import { useCallback, useEffect, useRef, useState } from 'react' import type { RawSignal, SimProgress } from './simulate.ts' import type { RunMessage, WorkerOut } from './simWorker.ts' import type { Phantom } from '../phantom/phantomTypes.ts' export type SimStatus = 'idle' | 'running' | 'done' | 'error' | 'cancelled' export interface SimState { status: SimStatus progress: SimProgress | null elapsedMs: number result: RawSignal | null error: string | null } const INITIAL: SimState = { status: 'idle', progress: null, elapsedMs: 0, result: null, error: null, } export function useSimulation() { const [state, setState] = useState(INITIAL) const workerRef = useRef(null) const teardown = useCallback(() => { if (workerRef.current) { workerRef.current.terminate() workerRef.current = null } }, []) useEffect(() => () => teardown(), [teardown]) const run = useCallback( (seqText: string, phantom: Phantom) => { teardown() const worker = new Worker(new URL('./simWorker.ts', import.meta.url), { type: 'module' }) workerRef.current = worker setState({ status: 'running', progress: null, elapsedMs: 0, result: null, error: null }) worker.onmessage = (ev: MessageEvent) => { const msg = ev.data if (msg.type === 'progress') { setState((s) => (s.status === 'running' ? { ...s, progress: msg.progress, elapsedMs: msg.elapsedMs } : s)) } else if (msg.type === 'done') { const result: RawSignal = { re: msg.re, im: msg.im, numReadouts: msg.numReadouts, samplesPerReadout: msg.samplesPerReadout, offsets: msg.offsets, maxSamplesPerReadout: msg.maxSamplesPerReadout, } setState((s) => ({ ...s, status: 'done', result, elapsedMs: msg.elapsedMs })) teardown() } else if (msg.type === 'error') { setState((s) => ({ ...s, status: 'error', error: msg.message })) teardown() } } worker.onerror = (ev) => { setState((s) => ({ ...s, status: 'error', error: ev.message || 'Worker error' })) teardown() } const runMsg: RunMessage = { type: 'run', seqText, phantom } worker.postMessage(runMsg) }, [teardown], ) const cancel = useCallback(() => { teardown() setState((s) => (s.status === 'running' ? { ...s, status: 'cancelled' } : s)) }, [teardown]) const reset = useCallback(() => { teardown() setState(INITIAL) }, [teardown]) return { state, run, cancel, reset } }