Add error handling logic

This commit is contained in:
Nilay Majorwar 2022-01-22 13:53:57 +05:30
parent d8481c097b
commit 0efd4c79ef
9 changed files with 291 additions and 131 deletions

View File

@ -1,33 +0,0 @@
import { DocumentRange } from "./types";
/**
* Special error class, to be thrown when encountering a
* syntax error while parsing a program.
*/
export class ParseError extends Error {
/** Location of syntax error in the program */
range: DocumentRange;
/**
* Create an instance of ParseError
* @param message Error message
* @param range Location of syntactically incorrect code
*/
constructor(message: string, range: DocumentRange) {
super(message);
this.range = range;
this.name = "ParseError";
}
}
/**
* Special error class, to be thrown when something happens
* that is indicative of a bug in the language implementation.
*/
export class UnexpectedError extends Error {
/** Create an instance of UnexpectedError */
constructor() {
super("Something unexpected occured");
this.name = "UnexpectedError";
}
}

View File

@ -1,4 +1,9 @@
import { LanguageEngine, StepExecutionResult } from "./types"; import { LanguageEngine, StepExecutionResult } from "./types";
import {
isRuntimeError,
serializeRuntimeError,
WorkerRuntimeError,
} from "./worker-errors";
type ExecuteAllArgs<RS> = { type ExecuteAllArgs<RS> = {
/** Interval between two execution steps, in milliseconds */ /** Interval between two execution steps, in milliseconds */
@ -94,30 +99,53 @@ class ExecutionController<RS> {
* Run a single step of execution * Run a single step of execution
* @returns Result of execution * @returns Result of execution
*/ */
executeStep(): StepExecutionResult<RS> { executeStep(): {
this._result = this._engine.executeStep(); result: StepExecutionResult<RS>;
this._result.signal = "paused"; error?: WorkerRuntimeError;
return this._result; } {
try {
this._result = this._engine.executeStep();
this._result.signal = "paused";
return { result: this._result };
} catch (error) {
if (isRuntimeError(error))
return { result: this._result!, error: serializeRuntimeError(error) };
else throw error;
}
} }
/** /**
* Execute the loaded program until stopped. * Execute the loaded program until stopped. Throws if an error other than RuntimeError is encountered.
* @param param0.interval Interval between two execution steps * @param param0.interval Interval between two execution steps
* @param param0.onResult Callback called with result on each execution step * @param param0.onResult Callback called with result on each execution step
* @returns Promise that resolves with result of last execution step * @returns Promise that resolves with result of last execution step and RuntimeError if any
*/ */
executeAll({ interval, onResult }: ExecuteAllArgs<RS>) { executeAll({ interval, onResult }: ExecuteAllArgs<RS>): Promise<{
result: StepExecutionResult<RS>;
error?: WorkerRuntimeError;
}> {
// Clear paused state // Clear paused state
this._isPaused = false; this._isPaused = false;
return new Promise(async (resolve) => { return new Promise(async (resolve, reject) => {
while (true) { while (true) {
const doBreak = this.runExecLoopIteration(); try {
onResult(this._result!); const doBreak = this.runExecLoopIteration();
if (doBreak) break; onResult(this._result!);
await this.sleep(interval); if (doBreak) break;
await this.sleep(interval);
} catch (error) {
if (isRuntimeError(error)) {
this._isPaused = true;
resolve({
result: this._result!,
error: serializeRuntimeError(error),
});
} else reject(error);
break;
}
} }
resolve(this._result!); resolve({ result: this._result! });
}); });
} }

View File

@ -1,25 +1,33 @@
import ExecutionController from "./execution-controller"; import ExecutionController from "./execution-controller";
import { LanguageEngine, StepExecutionResult } from "./types"; import { LanguageEngine, StepExecutionResult } from "./types";
import { import * as E from "./worker-errors";
WorkerAckType, import * as C from "./worker-constants";
WorkerRequestData,
WorkerResponseData,
} from "./worker-constants";
/** Create a worker response for update acknowledgement */ /** Create a worker response for acknowledgement */
const ackMessage = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({ const ackMessage = <RS, A extends C.WorkerAckType>(
ackType: A,
error?: C.WorkerAckError[A]
): C.WorkerResponseData<RS, A> => ({
type: "ack", type: "ack",
data: state, data: ackType,
error,
}); });
/** Create a worker response for execution result */ /** Create a worker response for execution result */
const resultMessage = <RS>( const resultMessage = <RS, A extends C.WorkerAckType>(
result: StepExecutionResult<RS> result: StepExecutionResult<RS>,
): WorkerResponseData<RS> => ({ error?: E.WorkerRuntimeError
): C.WorkerResponseData<RS, A> => ({
type: "result", type: "result",
data: result, data: result,
error,
}); });
/** Create a worker response for unexpected errors */
const errorMessage = <RS, A extends C.WorkerAckType>(
error: Error
): C.WorkerResponseData<RS, A> => ({ type: "error", error });
/** Initialize the execution controller */ /** Initialize the execution controller */
const initController = () => { const initController = () => {
postMessage(ackMessage("init")); postMessage(ackMessage("init"));
@ -42,8 +50,14 @@ const prepare = <RS>(
controller: ExecutionController<RS>, controller: ExecutionController<RS>,
{ code, input }: { code: string; input: string } { code, input }: { code: string; input: string }
) => { ) => {
controller.prepare(code, input); try {
postMessage(ackMessage("prepare")); controller.prepare(code, input);
postMessage(ackMessage("prepare"));
} catch (error) {
if (E.isParseError(error))
postMessage(ackMessage("prepare", E.serializeParseError(error)));
else throw error;
}
}; };
/** /**
@ -62,11 +76,15 @@ const updateBreakpoints = <RS>(
* Execute the entire program loaded on engine, * Execute the entire program loaded on engine,
* and return result of execution. * and return result of execution.
*/ */
const execute = <RS>(controller: ExecutionController<RS>, interval: number) => { const execute = async <RS>(
controller.executeAll({ controller: ExecutionController<RS>,
interval: number
) => {
const { result, error } = await controller.executeAll({
interval, interval,
onResult: (res) => postMessage(resultMessage(res)), onResult: (res) => postMessage(resultMessage(res)),
}); });
if (error) postMessage(resultMessage(result, error));
}; };
/** Trigger pause in program execution */ /** Trigger pause in program execution */
@ -77,8 +95,8 @@ const pauseExecution = async <RS>(controller: ExecutionController<RS>) => {
/** Run a single execution step */ /** Run a single execution step */
const executeStep = <RS>(controller: ExecutionController<RS>) => { const executeStep = <RS>(controller: ExecutionController<RS>) => {
const result = controller.executeStep(); const { result, error } = controller.executeStep();
postMessage(resultMessage(result)); postMessage(resultMessage(result, error));
}; };
/** /**
@ -88,16 +106,23 @@ const executeStep = <RS>(controller: ExecutionController<RS>) => {
export const setupWorker = <RS>(engine: LanguageEngine<RS>) => { export const setupWorker = <RS>(engine: LanguageEngine<RS>) => {
const controller = new ExecutionController(engine); const controller = new ExecutionController(engine);
addEventListener("message", async (ev: MessageEvent<WorkerRequestData>) => { addEventListener("message", async (ev: MessageEvent<C.WorkerRequestData>) => {
if (ev.data.type === "Init") return initController(); try {
if (ev.data.type === "Reset") return resetController(controller); if (ev.data.type === "Init") return initController();
if (ev.data.type === "Prepare") return prepare(controller, ev.data.params); if (ev.data.type === "Reset") return resetController(controller);
if (ev.data.type === "Execute") if (ev.data.type === "Prepare")
return execute(controller, ev.data.params.interval); return prepare(controller, ev.data.params);
if (ev.data.type === "Pause") return await pauseExecution(controller); if (ev.data.type === "Execute")
if (ev.data.type === "ExecuteStep") return executeStep(controller); return execute(controller, ev.data.params.interval);
if (ev.data.type === "UpdateBreakpoints") if (ev.data.type === "Pause") return await pauseExecution(controller);
return updateBreakpoints(controller, ev.data.params.points); if (ev.data.type === "ExecuteStep") return executeStep(controller);
if (ev.data.type === "UpdateBreakpoints")
return updateBreakpoints(controller, ev.data.params.points);
} catch (error) {
// Error here indicates an implementation bug
console.error(error);
postMessage(errorMessage(error as Error));
}
throw new Error("Invalid worker message type"); throw new Error("Invalid worker message type");
}); });
}; };

View File

@ -1,4 +1,5 @@
import { StepExecutionResult } from "./types"; import { DocumentRange, StepExecutionResult } from "./types";
import * as E from "./worker-errors";
/** Types of requests the worker handles */ /** Types of requests the worker handles */
export type WorkerRequestData = export type WorkerRequestData =
@ -39,7 +40,28 @@ export type WorkerAckType =
| "prepare" // on preparing for execution | "prepare" // on preparing for execution
| "pause"; // on pausing execution | "pause"; // on pausing execution
/** Errors associated with each response ack type */
export type WorkerAckError = {
init: undefined;
reset: undefined;
"bp-update": undefined;
prepare: E.WorkerParseError;
pause: undefined;
};
/** Types of responses the worker can send */ /** Types of responses the worker can send */
export type WorkerResponseData<RS> = export type WorkerResponseData<RS, A extends WorkerAckType> =
| { type: "ack"; data: WorkerAckType } /** Ack for one-off requests, optionally containing error occured (if any) */
| { type: "result"; data: StepExecutionResult<RS> }; | {
type: "ack";
data: A;
error?: WorkerAckError[A];
}
/** Response containing step execution result, and runtime error (if any) */
| {
type: "result";
data: StepExecutionResult<RS>;
error?: E.WorkerRuntimeError;
}
/** Response indicating a bug in worker/engine logic */
| { type: "error"; error: Error };

71
engines/worker-errors.ts Normal file
View File

@ -0,0 +1,71 @@
import { DocumentRange } from "./types";
/**
* Special error class, to be thrown when encountering a
* syntax error while parsing a program.
*/
export class ParseError extends Error {
/** Location of syntax error in the program */
range: DocumentRange;
/**
* Create an instance of ParseError
* @param message Error message
* @param range Location of syntactically incorrect code
*/
constructor(message: string, range: DocumentRange) {
super(message);
this.name = "ParseError";
this.range = range;
}
}
/**
* Special error class, to be thrown when encountering an error
* at runtime that is indicative of a bug in the user's program.
*/
export class RuntimeError extends Error {
/**
* Create an instance of RuntimeError
* @param message Error message
*/
constructor(message: string) {
super(message);
this.name = "RuntimeError";
}
}
/** Check if an error object is instance of a ParseError */
export const isParseError = (error: any): error is ParseError => {
return error instanceof ParseError || error.name === "ParseError";
};
/** Check if an error object is instance of a RuntimeError */
export const isRuntimeError = (error: any): error is RuntimeError => {
return error instanceof RuntimeError || error.name === "RuntimeError";
};
/** Error sent by worker in case of parsing error */
export type WorkerParseError = {
name: "ParseError";
message: string;
range: DocumentRange;
};
/** Error sent by worker in case error at runtime */
export type WorkerRuntimeError = {
name: "RuntimeError";
message: string;
};
/** Serialize a RuntimeError instance into a plain object */
export const serializeRuntimeError = (
error: RuntimeError
): WorkerRuntimeError => {
return { name: "RuntimeError", message: error.message };
};
/** Serialize a ParseError instance into a plain object */
export const serializeParseError = (error: ParseError): WorkerParseError => {
return { name: "ParseError", message: error.message, range: error.range };
};

View File

@ -7,6 +7,7 @@ import { LanguageProvider, StepExecutionResult } from "../engines/types";
import { OutputViewer, OutputViewerRef } from "../ui/output-viewer"; import { OutputViewer, OutputViewerRef } from "../ui/output-viewer";
import { ExecutionControls } from "./execution-controls"; import { ExecutionControls } from "./execution-controls";
import { RendererRef, RendererWrapper } from "./renderer-wrapper"; import { RendererRef, RendererWrapper } from "./renderer-wrapper";
import { WorkerRuntimeError } from "../engines/worker-errors";
type Props<RS> = { type Props<RS> = {
langName: string; langName: string;
@ -36,10 +37,14 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
const [execInterval, setExecInterval] = React.useState(20); const [execInterval, setExecInterval] = React.useState(20);
/** Utility that updates UI with the provided execution result */ /** Utility that updates UI with the provided execution result */
const updateWithResult = (result: StepExecutionResult<any>) => { const updateWithResult = (
result: StepExecutionResult<any>,
error?: WorkerRuntimeError
) => {
rendererRef.current!.updateState(result.rendererState); rendererRef.current!.updateState(result.rendererState);
codeEditorRef.current!.updateHighlights(result.nextStepLocation); codeEditorRef.current!.updateHighlights(result.nextStepLocation);
outputEditorRef.current!.append(result.output); outputEditorRef.current!.append(result.output);
if (error) outputEditorRef.current!.setError(error);
}; };
/** Reset and begin a new execution */ /** Reset and begin a new execution */
@ -54,13 +59,14 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
// Reset any existing execution state // Reset any existing execution state
outputEditorRef.current!.reset(); outputEditorRef.current!.reset();
await execController.resetState(); await execController.resetState();
await execController.prepare( const error = await execController.prepare(
codeEditorRef.current!.getValue(), codeEditorRef.current!.getValue(),
inputEditorRef.current!.getValue() inputEditorRef.current!.getValue()
); );
// Begin execution // Check for ParseError, else begin execution
await execController.execute(updateWithResult, execInterval); if (error) outputEditorRef.current!.setError(error);
else await execController.execute(updateWithResult, execInterval);
}; };
/** Pause the ongoing execution */ /** Pause the ongoing execution */
@ -82,8 +88,8 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
} }
// Run and update execution states // Run and update execution states
const result = await execController.executeStep(); const response = await execController.executeStep();
updateWithResult(result); updateWithResult(response.result, response.error);
}; };
/** Resume the currently paused execution */ /** Resume the currently paused execution */
@ -101,7 +107,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
/** Stop the currently active execution */ /** Stop the currently active execution */
const stopExecution = async () => { const stopExecution = async () => {
// Check if controller has execution // Check if controller has execution
if (!["paused", "processing"].includes(execController.state)) { if (!["paused", "processing", "error"].includes(execController.state)) {
console.error("No active execution in controller"); console.error("No active execution in controller");
return; return;
} }
@ -112,7 +118,6 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
// Reset all execution states // Reset all execution states
await execController.resetState(); await execController.resetState();
outputEditorRef.current!.reset();
rendererRef.current!.updateState(null); rendererRef.current!.updateState(null);
codeEditorRef.current!.updateHighlights(null); codeEditorRef.current!.updateHighlights(null);
}; };
@ -122,6 +127,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
const currState = execController.state; const currState = execController.state;
if (currState === "processing") return "running"; if (currState === "processing") return "running";
else if (currState === "paused") return "paused"; else if (currState === "paused") return "paused";
else if (currState === "error") return "error";
else return "off"; else return "off";
}; };

View File

@ -25,6 +25,9 @@ const styles = {
}, },
}; };
/** Possible states of the debug controls component */
type DebugControlsState = "off" | "running" | "paused" | "error";
/** Input field for changing execution interval */ /** Input field for changing execution interval */
const IntervalInput = (props: { const IntervalInput = (props: {
disabled: boolean; disabled: boolean;
@ -62,25 +65,30 @@ const RunButton = ({ onClick }: { onClick: () => void }) => (
/** Button group for debugging controls */ /** Button group for debugging controls */
const DebugControls = (props: { const DebugControls = (props: {
paused: boolean; state: DebugControlsState;
onPause: () => void; onPause: () => void;
onResume: () => void; onResume: () => void;
onStep: () => void; onStep: () => void;
onStop: () => void; onStop: () => void;
}) => { }) => {
const paused = props.state === "paused" || props.state === "error";
const pauseDisabled = props.state === "error";
const stepDisabled = ["off", "running", "error"].includes(props.state);
return ( return (
<ButtonGroup> <ButtonGroup>
<Button <Button
small small
title={props.paused ? "Pause" : "Resume"} title={paused ? "Pause" : "Resume"}
onClick={props.paused ? props.onResume : props.onPause} disabled={pauseDisabled}
icon={<Icon icon={props.paused ? "play" : "pause"} intent="primary" />} onClick={paused ? props.onResume : props.onPause}
icon={<Icon icon={paused ? "play" : "pause"} intent="primary" />}
/> />
<Button <Button
small small
title="Step" title="Step"
onClick={props.onStep} onClick={props.onStep}
disabled={!props.paused} disabled={stepDisabled}
icon={<Icon icon="step-forward" intent="warning" />} icon={<Icon icon="step-forward" intent="warning" />}
/> />
<Button <Button
@ -94,7 +102,7 @@ const DebugControls = (props: {
}; };
type Props = { type Props = {
state: "off" | "running" | "paused"; state: DebugControlsState;
onRun: () => void; onRun: () => void;
onPause: () => void; onPause: () => void;
onResume: () => void; onResume: () => void;
@ -110,7 +118,7 @@ export const ExecutionControls = (props: Props) => {
<RunButton onClick={props.onRun} /> <RunButton onClick={props.onRun} />
) : ( ) : (
<DebugControls <DebugControls
paused={props.state === "paused"} state={props.state}
onPause={props.onPause} onPause={props.onPause}
onResume={props.onResume} onResume={props.onResume}
onStep={props.onStep} onStep={props.onStep}

View File

@ -1,18 +1,18 @@
import React from "react"; import React from "react";
import { TextArea } from "@blueprintjs/core"; import { Colors, Text } from "@blueprintjs/core";
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
/** /** Format a ParseError for displaying as output */
* For aesthetic reasons, we use readonly textarea for displaying output. const formatParseError = (error: WorkerParseError): string => {
* Textarea displays placeholder if value passed is empty string, which is undesired. const line = error.range.line + 1;
* This function is a fake-whitespace workaround. const start = error.range.charRange?.start;
* const end = error.range.charRange?.end;
* @param value Value received from parent. Placeholder shown on `null`. console.log(line, start, end);
* @returns Value to pass as prop to Blueprint TextArea let cols: string | null = null;
*/ if (start != null && end != null) cols = `col ${start + 1}-${end + 1}`;
const toTextareaValue = (value: string | null): string | undefined => { else if (start != null) cols = `col ${start + 1}`;
if (value == null) return undefined; // Placeholder shown else if (end != null) cols = `col ${end + 1}`;
if (value === "") return "\u0020"; // Fake whitespace to hide placeholder return `ParseError: line ${line}, ${cols}\n${error.message}`;
return value; // Non-empty output value
}; };
export interface OutputViewerRef { export interface OutputViewerRef {
@ -20,26 +20,36 @@ export interface OutputViewerRef {
reset: () => void; reset: () => void;
/** Append string to the displayed output */ /** Append string to the displayed output */
append: (str?: string) => void; append: (str?: string) => void;
/** Add error text below the output text */
setError: (error: WorkerRuntimeError | WorkerParseError | null) => void;
} }
const OutputViewerComponent = (_: {}, ref: React.Ref<OutputViewerRef>) => { const OutputViewerComponent = (_: {}, ref: React.Ref<OutputViewerRef>) => {
const [value, setValue] = React.useState<string | null>(null); const [value, setValue] = React.useState<string | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
reset: () => setValue(null), reset: () => {
setValue(null);
setError(null);
},
append: (s) => setValue((o) => (o || "") + (s || "")), append: (s) => setValue((o) => (o || "") + (s || "")),
setError: (error: WorkerRuntimeError | WorkerParseError | null) => {
if (!error) setError(null);
else if (error.name === "RuntimeError")
setError("RuntimeError: " + error.message);
else if (error.name === "ParseError") setError(formatParseError(error));
},
})); }));
return ( return (
<TextArea <div style={{ padding: 10, fontSize: 16 }}>
fill <Text style={{ fontFamily: "monospace" }}>{value}</Text>
large {value && <div style={{ height: 10 }} />}
readOnly <Text style={{ fontFamily: "monospace", color: Colors.RED3 }}>
growVertically {error}
value={toTextareaValue(value)} </Text>
placeholder="Run code to see output..." </div>
style={{ height: "100%", resize: "none", boxShadow: "none" }}
/>
); );
}; };

View File

@ -4,6 +4,7 @@ import {
WorkerRequestData, WorkerRequestData,
WorkerResponseData, WorkerResponseData,
} from "../engines/worker-constants"; } from "../engines/worker-constants";
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
/** Possible states for the worker to be in */ /** Possible states for the worker to be in */
type WorkerState = type WorkerState =
@ -12,6 +13,7 @@ type WorkerState =
| "ready" // Ready to start execution | "ready" // Ready to start execution
| "processing" // Executing code | "processing" // Executing code
| "paused" // Execution currently paused | "paused" // Execution currently paused
| "error" // Execution ended due to error
| "done"; // Program ended, reset now | "done"; // Program ended, reset now
/** /**
@ -40,10 +42,10 @@ export const useExecController = <RS>(langName: string) => {
*/ */
const requestWorker = ( const requestWorker = (
request: WorkerRequestData, request: WorkerRequestData,
onData?: (data: WorkerResponseData<RS>) => boolean onData?: (data: WorkerResponseData<RS, any>) => boolean
): Promise<WorkerResponseData<RS>> => { ): Promise<WorkerResponseData<RS, any>> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const handler = (ev: MessageEvent<WorkerResponseData<RS>>) => { const handler = (ev: MessageEvent<WorkerResponseData<RS, any>>) => {
if (!onData) { if (!onData) {
// Normal mode // Normal mode
workerRef.current!.removeEventListener("message", handler); workerRef.current!.removeEventListener("message", handler);
@ -65,7 +67,7 @@ export const useExecController = <RS>(langName: string) => {
/** Utility to throw error on unexpected response */ /** Utility to throw error on unexpected response */
const throwUnexpectedRes = ( const throwUnexpectedRes = (
fnName: string, fnName: string,
res: WorkerResponseData<RS> res: WorkerResponseData<RS, any>
): never => { ): never => {
throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`); throw new Error(`Unexpected response on ${fnName}: ${JSON.stringify(res)}`);
}; };
@ -75,6 +77,9 @@ export const useExecController = <RS>(langName: string) => {
(async () => { (async () => {
if (workerRef.current) throw new Error("Tried to reinitialize worker"); if (workerRef.current) throw new Error("Tried to reinitialize worker");
workerRef.current = new Worker(`../workers/${langName}.js`); workerRef.current = new Worker(`../workers/${langName}.js`);
workerRef.current!.addEventListener("error", (ev) =>
console.log("Ev: ", ev)
);
const res = await requestWorker({ type: "Init" }); const res = await requestWorker({ type: "Init" });
if (res.type === "ack" && res.data === "init") setWorkerState("empty"); if (res.type === "ack" && res.data === "init") setWorkerState("empty");
else throwUnexpectedRes("init", res); else throwUnexpectedRes("init", res);
@ -92,14 +97,19 @@ export const useExecController = <RS>(langName: string) => {
* @param code Code content * @param code Code content
* @param input User input * @param input User input
*/ */
const prepare = React.useCallback(async (code: string, input: string) => { const prepare = React.useCallback(
const res = await requestWorker({ async (code: string, input: string): Promise<WorkerParseError | void> => {
type: "Prepare", const res = await requestWorker({
params: { code, input }, type: "Prepare",
}); params: { code, input },
if (res.type === "ack" && res.data === "prepare") setWorkerState("ready"); });
else throwUnexpectedRes("loadCode", res); 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. * Update debugging breakpoints in the execution controller.
@ -142,14 +152,18 @@ export const useExecController = <RS>(langName: string) => {
* Run a single step of execution * Run a single step of execution
* @return Execution result * @return Execution result
*/ */
const executeStep = React.useCallback(async () => { const executeStep = React.useCallback(async (): Promise<{
result: StepExecutionResult<RS>;
error?: WorkerRuntimeError;
}> => {
const res = await requestWorker( const res = await requestWorker(
{ type: "ExecuteStep" }, { type: "ExecuteStep" },
(res) => res.type !== "result" (res) => res.type !== "result"
); );
if (res.type !== "result") throw new Error("Something unexpected happened"); if (res.type !== "result") throw new Error("Something unexpected happened");
if (!res.data.nextStepLocation) setWorkerState("done"); if (!res.data.nextStepLocation) setWorkerState("done");
return res.data;
return { result: res.data, error: res.error };
}, []); }, []);
/** /**
@ -158,20 +172,29 @@ export const useExecController = <RS>(langName: string) => {
*/ */
const execute = React.useCallback( const execute = React.useCallback(
async ( async (
onResult: (result: StepExecutionResult<RS>) => void, onResult: (
result: StepExecutionResult<RS>,
error?: WorkerRuntimeError
) => void,
interval: number interval: number
) => { ) => {
setWorkerState("processing"); setWorkerState("processing");
// Set up a streaming-response cycle with the worker // Set up a streaming-response cycle with the worker
await requestWorker({ type: "Execute", params: { interval } }, (res) => { await requestWorker({ type: "Execute", params: { interval } }, (res) => {
if (res.type !== "result") return true; if (res.type !== "result") return true;
onResult(res.data); onResult(res.data, res.error);
if (!res.data.nextStepLocation) { if (!res.data.nextStepLocation) {
// Program execution complete
setWorkerState("done"); setWorkerState("done");
return false; return false;
} else if (res.data.signal === "paused") { } else if (res.data.signal === "paused") {
// Execution paused by user or breakpoint
setWorkerState("paused"); setWorkerState("paused");
return false; return false;
} else if (res.error) {
// Runtime error occured
setWorkerState("error");
return false;
} else return true; } else return true;
}); });
}, },