Add error handling logic
This commit is contained in:
@ -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";
|
||||
}
|
||||
}
|
@ -1,4 +1,9 @@
|
||||
import { LanguageEngine, StepExecutionResult } from "./types";
|
||||
import {
|
||||
isRuntimeError,
|
||||
serializeRuntimeError,
|
||||
WorkerRuntimeError,
|
||||
} from "./worker-errors";
|
||||
|
||||
type ExecuteAllArgs<RS> = {
|
||||
/** Interval between two execution steps, in milliseconds */
|
||||
@ -94,30 +99,53 @@ class ExecutionController<RS> {
|
||||
* Run a single step of execution
|
||||
* @returns Result of execution
|
||||
*/
|
||||
executeStep(): StepExecutionResult<RS> {
|
||||
this._result = this._engine.executeStep();
|
||||
this._result.signal = "paused";
|
||||
return this._result;
|
||||
executeStep(): {
|
||||
result: StepExecutionResult<RS>;
|
||||
error?: WorkerRuntimeError;
|
||||
} {
|
||||
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.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
|
||||
this._isPaused = false;
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
while (true) {
|
||||
const doBreak = this.runExecLoopIteration();
|
||||
onResult(this._result!);
|
||||
if (doBreak) break;
|
||||
await this.sleep(interval);
|
||||
try {
|
||||
const doBreak = this.runExecLoopIteration();
|
||||
onResult(this._result!);
|
||||
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! });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,33 @@
|
||||
import ExecutionController from "./execution-controller";
|
||||
import { LanguageEngine, StepExecutionResult } from "./types";
|
||||
import {
|
||||
WorkerAckType,
|
||||
WorkerRequestData,
|
||||
WorkerResponseData,
|
||||
} from "./worker-constants";
|
||||
import * as E from "./worker-errors";
|
||||
import * as C from "./worker-constants";
|
||||
|
||||
/** Create a worker response for update acknowledgement */
|
||||
const ackMessage = <RS>(state: WorkerAckType): WorkerResponseData<RS> => ({
|
||||
/** Create a worker response for acknowledgement */
|
||||
const ackMessage = <RS, A extends C.WorkerAckType>(
|
||||
ackType: A,
|
||||
error?: C.WorkerAckError[A]
|
||||
): C.WorkerResponseData<RS, A> => ({
|
||||
type: "ack",
|
||||
data: state,
|
||||
data: ackType,
|
||||
error,
|
||||
});
|
||||
|
||||
/** Create a worker response for execution result */
|
||||
const resultMessage = <RS>(
|
||||
result: StepExecutionResult<RS>
|
||||
): WorkerResponseData<RS> => ({
|
||||
const resultMessage = <RS, A extends C.WorkerAckType>(
|
||||
result: StepExecutionResult<RS>,
|
||||
error?: E.WorkerRuntimeError
|
||||
): C.WorkerResponseData<RS, A> => ({
|
||||
type: "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 */
|
||||
const initController = () => {
|
||||
postMessage(ackMessage("init"));
|
||||
@ -42,8 +50,14 @@ const prepare = <RS>(
|
||||
controller: ExecutionController<RS>,
|
||||
{ code, input }: { code: string; input: string }
|
||||
) => {
|
||||
controller.prepare(code, input);
|
||||
postMessage(ackMessage("prepare"));
|
||||
try {
|
||||
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,
|
||||
* and return result of execution.
|
||||
*/
|
||||
const execute = <RS>(controller: ExecutionController<RS>, interval: number) => {
|
||||
controller.executeAll({
|
||||
const execute = async <RS>(
|
||||
controller: ExecutionController<RS>,
|
||||
interval: number
|
||||
) => {
|
||||
const { result, error } = await controller.executeAll({
|
||||
interval,
|
||||
onResult: (res) => postMessage(resultMessage(res)),
|
||||
});
|
||||
if (error) postMessage(resultMessage(result, error));
|
||||
};
|
||||
|
||||
/** Trigger pause in program execution */
|
||||
@ -77,8 +95,8 @@ const pauseExecution = async <RS>(controller: ExecutionController<RS>) => {
|
||||
|
||||
/** Run a single execution step */
|
||||
const executeStep = <RS>(controller: ExecutionController<RS>) => {
|
||||
const result = controller.executeStep();
|
||||
postMessage(resultMessage(result));
|
||||
const { result, error } = controller.executeStep();
|
||||
postMessage(resultMessage(result, error));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -88,16 +106,23 @@ const executeStep = <RS>(controller: ExecutionController<RS>) => {
|
||||
export const setupWorker = <RS>(engine: LanguageEngine<RS>) => {
|
||||
const controller = new ExecutionController(engine);
|
||||
|
||||
addEventListener("message", async (ev: MessageEvent<WorkerRequestData>) => {
|
||||
if (ev.data.type === "Init") return initController();
|
||||
if (ev.data.type === "Reset") return resetController(controller);
|
||||
if (ev.data.type === "Prepare") return prepare(controller, ev.data.params);
|
||||
if (ev.data.type === "Execute")
|
||||
return execute(controller, ev.data.params.interval);
|
||||
if (ev.data.type === "Pause") return await pauseExecution(controller);
|
||||
if (ev.data.type === "ExecuteStep") return executeStep(controller);
|
||||
if (ev.data.type === "UpdateBreakpoints")
|
||||
return updateBreakpoints(controller, ev.data.params.points);
|
||||
addEventListener("message", async (ev: MessageEvent<C.WorkerRequestData>) => {
|
||||
try {
|
||||
if (ev.data.type === "Init") return initController();
|
||||
if (ev.data.type === "Reset") return resetController(controller);
|
||||
if (ev.data.type === "Prepare")
|
||||
return prepare(controller, ev.data.params);
|
||||
if (ev.data.type === "Execute")
|
||||
return execute(controller, ev.data.params.interval);
|
||||
if (ev.data.type === "Pause") return await pauseExecution(controller);
|
||||
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");
|
||||
});
|
||||
};
|
||||
|
@ -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 */
|
||||
export type WorkerRequestData =
|
||||
@ -39,7 +40,28 @@ export type WorkerAckType =
|
||||
| "prepare" // on preparing for 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 */
|
||||
export type WorkerResponseData<RS> =
|
||||
| { type: "ack"; data: WorkerAckType }
|
||||
| { type: "result"; data: StepExecutionResult<RS> };
|
||||
export type WorkerResponseData<RS, A extends WorkerAckType> =
|
||||
/** Ack for one-off requests, optionally containing error occured (if any) */
|
||||
| {
|
||||
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
71
engines/worker-errors.ts
Normal 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 };
|
||||
};
|
Reference in New Issue
Block a user