import React from "react"; import { StepExecutionResult } from "../languages/types"; import { WorkerRequestData, WorkerResponseData, } from "../languages/worker-constants"; import { WorkerParseError, WorkerRuntimeError, } from "../languages/worker-errors"; import { useErrorBoundary } from "./providers/error-boundary-provider"; /** Possible states for the worker to be in */ type WorkerState = | "loading" // Worker is not initialized yet | "empty" // Worker loaded, no code loaded yet | "ready" // Ready to start execution | "processing" // Executing code | "paused" // Execution currently paused | "error" // Execution ended due to error | "done"; // Program ended, reset now /** * React Hook that manages initialization, communication and * cleanup for the worker thread used for code execution. * * Also abstracts away the details of message-passing and exposes * an imperative API to the parent component. */ export const useExecController = (langName: string) => { const workerRef = React.useRef(null); const [workerState, setWorkerState] = React.useState("loading"); const errorBoundary = useErrorBoundary(); /** * Type-safe wrapper to abstract request-response cycle into * a simple imperative asynchronous call. Returns Promise that resolves * with response data. * * Note that if the worker misbehaves due to any reason, the returned response data * (or `onData` argument) may not correspond to the request. Check this in the caller. * * @param request Data to send in request * @param onData Optional argument - if passed, function enters response-streaming mode. * Callback is called with response data. Return `true` to keep the connection alive, `false` to end. * On end, promise resolves with last (already used) response data. */ const requestWorker = ( request: WorkerRequestData, onData?: (data: WorkerResponseData) => boolean ): Promise> => { return new Promise((resolve) => { const handler = (ev: MessageEvent>) => { if (!onData) { // Normal mode workerRef.current!.removeEventListener("message", handler); resolve(ev.data); } else { // Persistent connection mode const keepAlive = onData(ev.data); if (keepAlive) return; // keepAlive is false: terminate connection workerRef.current!.removeEventListener("message", handler); resolve(ev.data); } }; workerRef.current!.addEventListener("message", handler); workerRef.current!.postMessage(request); }); }; /** Utility to throw error on unexpected response */ const throwUnexpectedRes = ( fnName: string, res: WorkerResponseData ): never => { throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`); }; // Initialization and cleanup of web worker React.useEffect(() => { (async () => { if (workerRef.current) throw new Error("Tried to reinitialize worker"); workerRef.current = new Worker(`../workers/${langName}.js`); // Add event listener for bubbling errors to main thread workerRef.current.addEventListener( "message", (event: MessageEvent>) => { if (event.data.type !== "error") return; const plainError = event.data.error; const err = new Error( `[Worker] ${plainError.name}: ${plainError.message}` ); err.stack = event.data.error.stack; errorBoundary.throwError(err); // throw err; } ); const res = await requestWorker({ type: "Init" }); if (res.type === "ack" && res.data === "init") setWorkerState("empty"); else throwUnexpectedRes("init", res); })(); return () => { // Terminate worker and clean up workerRef.current!.terminate(); workerRef.current = null; }; }, []); /** * Load code and user input into the execution controller. * @param code Code content * @param input User input */ const prepare = React.useCallback( async (code: string, input: string): Promise => { const res = await requestWorker({ type: "Prepare", params: { code, input }, }); if (res.type === "ack" && res.data === "prepare") { if (res.error) return res.error; else setWorkerState("ready"); } else throwUnexpectedRes("loadCode", res); }, [] ); /** * Update debugging breakpoints in the execution controller. * @param points Array of line numbers having breakpoints */ const updateBreakpoints = React.useCallback(async (points: number[]) => { await requestWorker( { type: "UpdateBreakpoints", params: { points }, }, (res) => { if (res.type === "ack" && res.data === "bp-update") return false; else return true; } ); }, []); /** * Reset the state of the controller and engine. */ const resetState = React.useCallback(async () => { const res = await requestWorker({ type: "Reset" }); if (res.type === "ack" && res.data === "reset") setWorkerState("empty"); else throwUnexpectedRes("resetState", res); }, []); /** * Validate the syntax of the user's program code * @param code Code content of the user's program */ const validateCode = React.useCallback( async (code: string): Promise => { const res = await requestWorker( { type: "ValidateCode", params: { code } }, (res) => res.type !== "validate" ); return res.error; }, [] ); /** * Pause program execution */ const pauseExecution = React.useCallback(async () => { await requestWorker({ type: "Pause" }, (res) => { // We don't update state here - that's done by the execution stream instead if (!(res.type === "ack" && res.data === "pause")) return true; return false; }); }, []); /** * Run a single step of execution * @return Execution result */ const executeStep = React.useCallback(async (): Promise<{ result: StepExecutionResult; error?: WorkerRuntimeError; }> => { const res = await requestWorker( { type: "ExecuteStep" }, (res) => res.type !== "result" ); if (res.type !== "result") throw new Error("Something unexpected happened"); if (!res.data.nextStepLocation) setWorkerState("done"); return { result: res.data, error: res.error }; }, []); /** * Execute the code loaded into the engine * @param onResult Callback used when an execution result is received */ const execute = React.useCallback( async ( onResult: ( result: StepExecutionResult, error?: WorkerRuntimeError ) => void, interval: number ) => { setWorkerState("processing"); // Set up a streaming-response cycle with the worker await requestWorker({ type: "Execute", params: { interval } }, (res) => { if (res.type !== "result") return true; onResult(res.data, res.error); if (!res.data.nextStepLocation) { // Program execution complete setWorkerState("done"); return false; } else if (res.data.signal === "paused") { // Execution paused by user or breakpoint setWorkerState("paused"); return false; } else if (res.error) { // Runtime error occured setWorkerState("error"); return false; } else return true; }); }, [] ); return { state: workerState, resetState, prepare, pauseExecution, validateCode, execute, executeStep, updateBreakpoints, }; };