Files

246 lines
7.7 KiB
TypeScript
Raw Permalink Normal View History

2021-12-14 21:58:13 +05:30
import React from "react";
2022-01-30 20:47:33 +05:30
import { StepExecutionResult } from "../languages/types";
2021-12-14 21:58:13 +05:30
import {
WorkerRequestData,
WorkerResponseData,
2022-01-30 20:47:33 +05:30
} from "../languages/worker-constants";
import {
WorkerParseError,
WorkerRuntimeError,
} from "../languages/worker-errors";
2022-01-27 00:03:30 +05:30
import { useErrorBoundary } from "./providers/error-boundary-provider";
2021-12-14 21:58:13 +05:30
/** Possible states for the worker to be in */
type WorkerState =
| "loading" // Worker is not initialized yet
| "empty" // Worker loaded, no code loaded yet
2021-12-15 21:31:25 +05:30
| "ready" // Ready to start execution
2021-12-14 21:58:13 +05:30
| "processing" // Executing code
2021-12-15 21:31:25 +05:30
| "paused" // Execution currently paused
2022-01-22 13:53:57 +05:30
| "error" // Execution ended due to error
2021-12-14 21:58:13 +05:30
| "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.
*/
2021-12-18 17:15:22 +05:30
export const useExecController = <RS>(langName: string) => {
2021-12-14 21:58:13 +05:30
const workerRef = React.useRef<Worker | null>(null);
const [workerState, setWorkerState] = React.useState<WorkerState>("loading");
2022-01-27 00:03:30 +05:30
const errorBoundary = useErrorBoundary();
2021-12-14 21:58:13 +05:30
/**
* Type-safe wrapper to abstract request-response cycle into
2021-12-14 21:58:13 +05:30
* 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.
2021-12-14 21:58:13 +05:30
* On end, promise resolves with last (already used) response data.
*/
const requestWorker = (
request: WorkerRequestData,
2022-01-22 13:53:57 +05:30
onData?: (data: WorkerResponseData<RS, any>) => boolean
): Promise<WorkerResponseData<RS, any>> => {
2021-12-14 21:58:13 +05:30
return new Promise((resolve) => {
2022-01-22 13:53:57 +05:30
const handler = (ev: MessageEvent<WorkerResponseData<RS, any>>) => {
2021-12-14 21:58:13 +05:30
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,
2022-01-22 13:53:57 +05:30
res: WorkerResponseData<RS, any>
): never => {
2021-12-15 21:31:25 +05:30
throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`);
};
2021-12-14 21:58:13 +05:30
// Initialization and cleanup of web worker
React.useEffect(() => {
(async () => {
if (workerRef.current) throw new Error("Tried to reinitialize worker");
2021-12-18 17:15:22 +05:30
workerRef.current = new Worker(`../workers/${langName}.js`);
2022-01-27 00:03:30 +05:30
// Add event listener for bubbling errors to main thread
workerRef.current.addEventListener(
"message",
(event: MessageEvent<WorkerResponseData<RS, any>>) => {
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);
2021-12-14 21:58:13 +05:30
})();
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
*/
2022-01-22 13:53:57 +05:30
const prepare = React.useCallback(
async (code: string, input: string): Promise<WorkerParseError | void> => {
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;
}
);
2021-12-14 21:58:13 +05:30
}, []);
/**
* 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);
2021-12-14 21:58:13 +05:30
}, []);
/**
* 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<WorkerParseError | undefined> => {
const res = await requestWorker(
{ type: "ValidateCode", params: { code } },
(res) => res.type !== "validate"
);
return res.error;
},
[]
);
2021-12-15 21:31:25 +05:30
/**
* 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;
});
}, []);
2021-12-15 22:08:30 +05:30
/**
* Run a single step of execution
* @return Execution result
*/
2022-01-22 13:53:57 +05:30
const executeStep = React.useCallback(async (): Promise<{
result: StepExecutionResult<RS>;
error?: WorkerRuntimeError;
}> => {
2021-12-15 22:08:30 +05:30
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");
2022-01-22 13:53:57 +05:30
return { result: res.data, error: res.error };
2021-12-15 22:08:30 +05:30
}, []);
2021-12-14 21:58:13 +05:30
/**
* Execute the code loaded into the engine
* @param onResult Callback used when an execution result is received
*/
2021-12-15 21:31:25 +05:30
const execute = React.useCallback(
2021-12-14 21:58:13 +05:30
async (
2022-01-22 13:53:57 +05:30
onResult: (
result: StepExecutionResult<RS>,
error?: WorkerRuntimeError
) => void,
2021-12-17 00:51:43 +05:30
interval: number
2021-12-14 21:58:13 +05:30
) => {
setWorkerState("processing");
// Set up a streaming-response cycle with the worker
await requestWorker({ type: "Execute", params: { interval } }, (res) => {
if (res.type !== "result") return true;
2022-01-22 13:53:57 +05:30
onResult(res.data, res.error);
2021-12-15 21:31:25 +05:30
if (!res.data.nextStepLocation) {
2022-01-22 13:53:57 +05:30
// Program execution complete
2021-12-15 21:31:25 +05:30
setWorkerState("done");
return false;
} else if (res.data.signal === "paused") {
2022-01-22 13:53:57 +05:30
// Execution paused by user or breakpoint
2021-12-15 21:31:25 +05:30
setWorkerState("paused");
return false;
2022-01-22 13:53:57 +05:30
} else if (res.error) {
// Runtime error occured
setWorkerState("error");
return false;
2021-12-15 21:31:25 +05:30
} else return true;
2021-12-14 21:58:13 +05:30
});
},
[]
);
return {
state: workerState,
resetState,
prepare,
2021-12-15 21:31:25 +05:30
pauseExecution,
validateCode,
2021-12-15 21:31:25 +05:30
execute,
2021-12-15 22:08:30 +05:30
executeStep,
updateBreakpoints,
};
2021-12-14 21:58:13 +05:30
};