Add UI and logic for handling worker errors
This commit is contained in:
parent
35bebf045f
commit
31b0d7b7b8
@ -30,7 +30,7 @@ const resultMessage = <RS, A extends C.WorkerAckType>(
|
|||||||
|
|
||||||
/** Create a worker response for unexpected errors */
|
/** Create a worker response for unexpected errors */
|
||||||
const errorMessage = <RS, A extends C.WorkerAckType>(
|
const errorMessage = <RS, A extends C.WorkerAckType>(
|
||||||
error: Error
|
error: E.WorkerError
|
||||||
): C.WorkerResponseData<RS, A> => ({ type: "error", error });
|
): C.WorkerResponseData<RS, A> => ({ type: "error", error });
|
||||||
|
|
||||||
/** Initialize the execution controller */
|
/** Initialize the execution controller */
|
||||||
@ -129,15 +129,16 @@ export const setupWorker = <RS>(engine: LanguageEngine<RS>) => {
|
|||||||
if (ev.data.type === "ValidateCode")
|
if (ev.data.type === "ValidateCode")
|
||||||
return validateCode(controller, ev.data.params.code);
|
return validateCode(controller, ev.data.params.code);
|
||||||
if (ev.data.type === "Execute")
|
if (ev.data.type === "Execute")
|
||||||
return execute(controller, ev.data.params.interval);
|
return await execute(controller, ev.data.params.interval);
|
||||||
if (ev.data.type === "Pause") return await pauseExecution(controller);
|
if (ev.data.type === "Pause") return await pauseExecution(controller);
|
||||||
if (ev.data.type === "ExecuteStep") return executeStep(controller);
|
if (ev.data.type === "ExecuteStep") return executeStep(controller);
|
||||||
if (ev.data.type === "UpdateBreakpoints")
|
if (ev.data.type === "UpdateBreakpoints")
|
||||||
return updateBreakpoints(controller, ev.data.params.points);
|
return updateBreakpoints(controller, ev.data.params.points);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error here indicates an implementation bug
|
// Error here indicates an implementation bug
|
||||||
console.error(error);
|
const err = error as Error;
|
||||||
postMessage(errorMessage(error as Error));
|
postMessage(errorMessage(E.serializeError(err)));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("Invalid worker message type");
|
throw new Error("Invalid worker message type");
|
||||||
});
|
});
|
||||||
|
@ -73,4 +73,4 @@ export type WorkerResponseData<RS, A extends WorkerAckType> =
|
|||||||
error?: E.WorkerRuntimeError;
|
error?: E.WorkerRuntimeError;
|
||||||
}
|
}
|
||||||
/** Response indicating a bug in worker/engine logic */
|
/** Response indicating a bug in worker/engine logic */
|
||||||
| { type: "error"; error: Error };
|
| { type: "error"; error: E.WorkerError };
|
||||||
|
@ -58,6 +58,13 @@ export type WorkerRuntimeError = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Error sent by worker indicating an implementation bug */
|
||||||
|
export type WorkerError = {
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/** Serialize a RuntimeError instance into a plain object */
|
/** Serialize a RuntimeError instance into a plain object */
|
||||||
export const serializeRuntimeError = (
|
export const serializeRuntimeError = (
|
||||||
error: RuntimeError
|
error: RuntimeError
|
||||||
@ -69,3 +76,8 @@ export const serializeRuntimeError = (
|
|||||||
export const serializeParseError = (error: ParseError): WorkerParseError => {
|
export const serializeParseError = (error: ParseError): WorkerParseError => {
|
||||||
return { name: "ParseError", message: error.message, range: error.range };
|
return { name: "ParseError", message: error.message, range: error.range };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Serialize an arbitrary error into a plain object */
|
||||||
|
export const serializeError = (error: Error): WorkerError => {
|
||||||
|
return { name: error.name, message: error.message, stack: error.stack };
|
||||||
|
};
|
||||||
|
@ -5,12 +5,15 @@ import "@blueprintjs/icons/lib/css/blueprint-icons.css";
|
|||||||
import "react-mosaic-component/react-mosaic-component.css";
|
import "react-mosaic-component/react-mosaic-component.css";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { DarkModeProvider } from "../ui/providers/dark-mode-provider";
|
import { DarkModeProvider } from "../ui/providers/dark-mode-provider";
|
||||||
|
import { ErrorBoundaryProvider } from "../ui/providers/error-boundary-provider";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DarkModeProvider>
|
<ErrorBoundaryProvider>
|
||||||
<Component {...pageProps} />
|
<DarkModeProvider>
|
||||||
</DarkModeProvider>
|
<Component {...pageProps} />
|
||||||
|
</DarkModeProvider>
|
||||||
|
</ErrorBoundaryProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
68
ui/providers/error-boundary-provider.tsx
Normal file
68
ui/providers/error-boundary-provider.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Colors, Text, Toast, Toaster } from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const CREATE_ISSUE_URL = "https://github.com/nilaymaj/esolang-park/issues/new";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
errorTitle: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
whiteSpace: "pre-wrap" as "pre-wrap",
|
||||||
|
},
|
||||||
|
callStack: {
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
background: Colors.RED1,
|
||||||
|
whiteSpace: "pre-wrap" as "pre-wrap",
|
||||||
|
border: "1px solid " + Colors.RED4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorBoundaryContext = React.createContext<{
|
||||||
|
throwError: (error: Error) => void;
|
||||||
|
}>({ throwError: () => {} });
|
||||||
|
|
||||||
|
/** Context provider for error handling */
|
||||||
|
export const ErrorBoundaryProvider = (props: { children: React.ReactNode }) => {
|
||||||
|
const [error, setError] = React.useState<Error | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundaryContext.Provider value={{ throwError: setError }}>
|
||||||
|
<Toaster>
|
||||||
|
{error && (
|
||||||
|
<Toast
|
||||||
|
icon="error"
|
||||||
|
intent="danger"
|
||||||
|
onDismiss={() => setError(null)}
|
||||||
|
message={
|
||||||
|
<Text>
|
||||||
|
<p>An unexpected error occurred:</p>
|
||||||
|
<pre style={styles.errorTitle}>{error.message}</pre>
|
||||||
|
<pre style={styles.callStack}>{error.stack}</pre>
|
||||||
|
<p>
|
||||||
|
Please{" "}
|
||||||
|
<a href={CREATE_ISSUE_URL} target="_blank">
|
||||||
|
create an issue on GitHub
|
||||||
|
</a>{" "}
|
||||||
|
with:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>The language you were using</li>
|
||||||
|
<li>The code you tried to run</li>
|
||||||
|
<li>The error details and call stack above</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Once done, refresh the page to reload the IDE. If needed,{" "}
|
||||||
|
<b>copy your code before refreshing the page</b>.
|
||||||
|
</p>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Toaster>
|
||||||
|
{props.children}
|
||||||
|
</ErrorBoundaryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Utility hook to access error boundary */
|
||||||
|
export const useErrorBoundary = () => React.useContext(ErrorBoundaryContext);
|
@ -5,6 +5,7 @@ import {
|
|||||||
WorkerResponseData,
|
WorkerResponseData,
|
||||||
} from "../engines/worker-constants";
|
} from "../engines/worker-constants";
|
||||||
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
|
import { WorkerParseError, WorkerRuntimeError } from "../engines/worker-errors";
|
||||||
|
import { useErrorBoundary } from "./providers/error-boundary-provider";
|
||||||
|
|
||||||
/** Possible states for the worker to be in */
|
/** Possible states for the worker to be in */
|
||||||
type WorkerState =
|
type WorkerState =
|
||||||
@ -26,6 +27,7 @@ type WorkerState =
|
|||||||
export const useExecController = <RS>(langName: string) => {
|
export const useExecController = <RS>(langName: string) => {
|
||||||
const workerRef = React.useRef<Worker | null>(null);
|
const workerRef = React.useRef<Worker | null>(null);
|
||||||
const [workerState, setWorkerState] = React.useState<WorkerState>("loading");
|
const [workerState, setWorkerState] = React.useState<WorkerState>("loading");
|
||||||
|
const errorBoundary = useErrorBoundary();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type-safe wrapper to abstract request-response cycle into
|
* Type-safe wrapper to abstract request-response cycle into
|
||||||
@ -77,6 +79,20 @@ 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`);
|
||||||
|
// 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" });
|
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user