import { useCallback, useRef, useState } from "react"; import type { PlainValue, RunFailure, RunRequest, RunResult, RunSuccess } from "@plate-dyn/core"; import { useRunner } from "./RunnerContext"; export type SolveState = | { status: "idle" } | { status: "running"; startedAt: number; output: string[] } | { status: "done"; run: RunSuccess; value: T; output: string[] } | { status: "error"; run: RunFailure; output: string[] }; export interface Solve { state: SolveState; /** Most recent successful value, kept while a new run is in progress */ last: T | null; run: (req: RunRequest) => Promise; cancel: () => void; /** Forget the last value and go back to idle, so nothing stale is displayed */ reset: () => void; } /** * Drive one run at a time through the shared runner. `parse` types the raw * result; a parse failure is reported like any other error so a script that * returns the wrong shape never looks like a blank screen. */ export function useSolve(parse: (v: PlainValue) => T): Solve { const runner = useRunner(); const [state, setState] = useState>({ status: "idle" }); const [last, setLast] = useState(null); const seq = useRef(0); const run = useCallback( async (req: RunRequest) => { const id = ++seq.current; const output: string[] = []; setState({ status: "running", startedAt: performance.now(), output: [] }); const res = await runner.run(req, (text) => { output.push(text); if (seq.current === id) setState((s) => (s.status === "running" ? { ...s, output: [...output] } : s)); }); if (seq.current !== id) return undefined; if (res.ok) { try { const value = parse(res.value); setLast(value); setState({ status: "done", run: res, value, output: res.output }); } catch (e) { setState({ status: "error", run: { ...res, ok: false, error: `the script ran, but its result has an unexpected shape: ${(e as Error).message}`, }, output: res.output, }); } } else { setState({ status: "error", run: res, output: res.output }); } return res; }, [runner, parse], ); const cancel = useCallback(() => { seq.current++; runner.cancel(); setState({ status: "idle" }); }, [runner]); // Local state only: nothing is terminated, so the worker (and the numbl module it // has loaded) survives. Use `cancel` to stop a run that is actually in flight. const reset = useCallback(() => { seq.current++; setLast(null); setState({ status: "idle" }); }, []); return { state, last, run, cancel, reset }; }