diff --git a/app/terminal/javascript/page.tsx b/app/terminal/javascript/page.tsx
deleted file mode 100644
index 7f9861c..0000000
--- a/app/terminal/javascript/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-"use client";
-
-import { EditorComponent } from "../editor";
-import { ReplTerminal } from "../repl";
-
-export default function JavaScriptPage() {
- return (
-
- console.log('hello, world!')\nhello, world!"}
- />
-
-
- );
-}
diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx
deleted file mode 100644
index 36c355c..0000000
--- a/app/terminal/javascript/runtime.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client';
-
-import { ReplCommand, ReplOutput } from '../repl';
-import { createWorkerRuntime } from '../worker-runtime';
-
-const config = {
- languageName: 'JavaScript',
- providerName: 'JavaScriptProvider',
- workerUrl: '/javascript.worker.js',
- splitReplExamples: (content: string): ReplCommand[] => {
- const initCommands: { command: string; output: ReplOutput[] }[] = [];
- for (const line of content.split('\n')) {
- if (line.startsWith('> ')) {
- // Remove the prompt from the command
- initCommands.push({ command: line.slice(2), output: [] });
- } else {
- // Lines without prompt are output from the previous command
- if (initCommands.length > 0) {
- initCommands[initCommands.length - 1].output.push({
- type: 'stdout',
- message: line,
- });
- }
- }
- }
- return initCommands;
- },
- getCommandlineStr: (filenames: string[]) => `node ${filenames[0]}`,
-};
-
-const { Provider, useRuntime } = createWorkerRuntime(config);
-
-export const JavaScriptProvider = Provider;
-export const useJavaScript = useRuntime;
\ No newline at end of file
diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx
index 93a51b8..fc6cb9a 100644
--- a/app/terminal/page.tsx
+++ b/app/terminal/page.tsx
@@ -3,18 +3,18 @@ import { Heading } from "@/[docs_id]/markdown";
import "mocha/mocha.js";
import "mocha/mocha.css";
import { useEffect, useRef, useState } from "react";
-import { usePyodide } from "./python/runtime";
-import { useRuby } from "./ruby/runtime";
import { useWandbox } from "./wandbox/runtime";
import { RuntimeContext, RuntimeLang } from "./runtime";
import { useEmbedContext } from "./embedContext";
import { defineTests } from "./tests";
-import { useJavaScript } from "./javascript/runtime";
+import { usePyodide } from "./worker/pyodide";
+import { useRuby } from "./worker/ruby";
+import { useJSEval } from "./worker/jsEval";
export default function RuntimeTestPage() {
const pyodide = usePyodide();
const ruby = useRuby();
- const javascript = useJavaScript();
+ const javascript = useJSEval();
const wandboxCpp = useWandbox("cpp");
const runtimeRef = useRef
>(null!);
runtimeRef.current = {
diff --git a/app/terminal/python/page.tsx b/app/terminal/python/page.tsx
deleted file mode 100644
index 093b100..0000000
--- a/app/terminal/python/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-"use client";
-
-import { EditorComponent } from "../editor";
-import { ExecFile } from "../exec";
-import { ReplTerminal } from "../repl";
-
-export default function PythonPage() {
- return (
-
- >> print('hello, world!')\nhello, world!"}
- />
-
-
-
- );
-}
diff --git a/app/terminal/python/runtime.tsx b/app/terminal/python/runtime.tsx
deleted file mode 100644
index af24c6b..0000000
--- a/app/terminal/python/runtime.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-'use client';
-
-import { ReplCommand, ReplOutput } from '../repl';
-import { createWorkerRuntime } from '../worker-runtime';
-
-const config = {
- languageName: 'Python',
- providerName: 'PyodideProvider',
- workerUrl: '/pyodide.worker.js',
- splitReplExamples: (content: string): ReplCommand[] => {
- const initCommands: { command: string; output: ReplOutput[] }[] = [];
- for (const line of content.split('\n')) {
- if (line.startsWith('>>> ')) {
- // Remove the prompt from the command
- initCommands.push({ command: line.slice(4), output: [] });
- } else if (line.startsWith('... ')) {
- if (initCommands.length > 0) {
- initCommands[initCommands.length - 1].command += '\n' + line.slice(4);
- }
- } else {
- // Lines without prompt are output from the previous command
- if (initCommands.length > 0) {
- initCommands[initCommands.length - 1].output.push({
- type: 'stdout',
- message: line,
- });
- }
- }
- }
- return initCommands;
- },
- getCommandlineStr: (filenames: string[]) => `python ${filenames[0]}`,
-};
-
-const { Provider, useRuntime } = createWorkerRuntime(config);
-
-export const PyodideProvider = Provider;
-export const usePyodide = useRuntime;
\ No newline at end of file
diff --git a/app/terminal/ruby/page.tsx b/app/terminal/ruby/page.tsx
deleted file mode 100644
index a986738..0000000
--- a/app/terminal/ruby/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-"use client";
-
-import { EditorComponent } from "../editor";
-import { ExecFile } from "../exec";
-import { ReplTerminal } from "../repl";
-
-export default function RubyPage() {
- return (
-
- > puts 'hello, world!'\nhello, world!"}
- />
-
-
-
- );
-}
diff --git a/app/terminal/ruby/runtime.tsx b/app/terminal/ruby/runtime.tsx
deleted file mode 100644
index d752644..0000000
--- a/app/terminal/ruby/runtime.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client';
-
-import { ReplCommand, ReplOutput } from '../repl';
-import { createWorkerRuntime } from '../worker-runtime';
-
-const config = {
- languageName: 'Ruby',
- providerName: 'RubyProvider',
- workerUrl: '/ruby.worker.js',
- splitReplExamples: (content: string): ReplCommand[] => {
- const initCommands: { command: string; output: ReplOutput[] }[] = [];
- for (const line of content.split('\n')) {
- if (line.startsWith('>> ')) {
- // Ruby IRB uses >> as the prompt
- initCommands.push({ command: line.slice(3), output: [] });
- } else if (line.startsWith('?> ')) {
- // Ruby IRB uses ?> for continuation
- if (initCommands.length > 0) {
- initCommands[initCommands.length - 1].command += '\n' + line.slice(3);
- }
- } else {
- // Lines without prompt are output from the previous command
- if (initCommands.length > 0) {
- initCommands[initCommands.length - 1].output.push({
- type: 'stdout',
- message: line,
- });
- }
- }
- }
- return initCommands;
- },
- getCommandlineStr: (filenames: string[]) => `ruby ${filenames[0]}`,
-};
-
-const { Provider, useRuntime } = createWorkerRuntime(config);
-
-export const RubyProvider = Provider;
-export const useRuby = useRuntime;
\ No newline at end of file
diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx
index 2f7d624..d72c7a0 100644
--- a/app/terminal/runtime.tsx
+++ b/app/terminal/runtime.tsx
@@ -1,11 +1,12 @@
import { MutexInterface } from "async-mutex";
import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl";
-import { PyodideProvider, usePyodide } from "./python/runtime";
-import { RubyProvider, useRuby } from "./ruby/runtime";
import { useWandbox, WandboxProvider } from "./wandbox/runtime";
-import { JavaScriptProvider, useJavaScript } from "./javascript/runtime";
import { AceLang } from "./editor";
import { ReactNode } from "react";
+import { PyodideContext, usePyodide } from "./worker/pyodide";
+import { RubyContext, useRuby } from "./worker/ruby";
+import { JSEvalContext, useJSEval } from "./worker/jsEval";
+import { WorkerProvider } from "./worker/runtime";
/**
* Common runtime context interface for different languages
@@ -23,7 +24,7 @@ export interface RuntimeContext {
splitReplExamples?: (content: string) => ReplCommand[];
// file
runFiles: (filenames: string[]) => Promise;
- getCommandlineStr: (filenames: string[]) => string;
+ getCommandlineStr?: (filenames: string[]) => string;
}
export interface LangConstants {
tabSize: number;
@@ -58,18 +59,18 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
// すべての言語のcontextをインスタンス化
const pyodide = usePyodide();
const ruby = useRuby();
+ const jsEval = useJSEval();
const wandboxCpp = useWandbox("cpp");
- const javascript = useJavaScript();
switch (language) {
case "python":
return pyodide;
case "ruby":
return ruby;
+ case "javascript":
+ return jsEval;
case "cpp":
return wandboxCpp;
- case "javascript":
- return javascript;
default:
language satisfies never;
throw new Error(`Runtime not implemented for language: ${language}`);
@@ -77,13 +78,13 @@ export function useRuntime(language: RuntimeLang): RuntimeContext {
}
export function RuntimeProvider({ children }: { children: ReactNode }) {
return (
-
-
-
- {children}
-
-
-
+
+
+
+ {children}
+
+
+
);
}
diff --git a/app/terminal/worker-runtime.tsx b/app/terminal/worker-runtime.tsx
deleted file mode 100644
index e6961be..0000000
--- a/app/terminal/worker-runtime.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-"use client";
-
-import {
- useState,
- useRef,
- useCallback,
- ReactNode,
- createContext,
- useContext,
- useEffect,
-} from "react";
-import { SyntaxStatus, ReplOutput, ReplCommand } from "./repl";
-import { Mutex, MutexInterface } from "async-mutex";
-import { useEmbedContext } from "./embedContext";
-import { RuntimeContext } from "./runtime";
-
-// --- Type Definitions ---
-
-type WorkerCapabilities = {
- interrupt: "buffer" | "restart";
-};
-
-type MessageToWorker =
- | { type: "init"; payload: { interruptBuffer: Uint8Array } }
- | { type: "runCode"; payload: { code: string } }
- | { type: "checkSyntax"; payload: { code: string } }
- | {
- type: "runFile";
- payload: { name: string; files: Record };
- }
- | { type: "restoreState"; payload: { commands: string[] } };
-
-type MessageFromWorker =
- | { id: number; payload: unknown }
- | { id: number; error: string };
-
-type InitPayloadFromWorker = {
- success: boolean;
- capabilities: WorkerCapabilities;
-};
-type RunPayloadFromWorker = {
- output: ReplOutput[];
- updatedFiles: [string, string][];
-};
-type StatusPayloadFromWorker = { status: SyntaxStatus };
-
-export interface WorkerRuntimeConfig {
- languageName: string;
- providerName: string;
- workerUrl: string;
- splitReplExamples: (content: string) => ReplCommand[];
- getCommandlineStr: (filenames: string[]) => string;
-}
-
-// --- Factory Function ---
-
-export function createWorkerRuntime(config: WorkerRuntimeConfig) {
- const RuntimeContextInternal = createContext(null!);
-
- const useRuntime = (): RuntimeContext => {
- const context = useContext(RuntimeContextInternal);
- if (!context) {
- throw new Error(
- `use${config.languageName} must be used within a ${config.providerName}`
- );
- }
- return context;
- };
-
- const Provider = ({ children }: { children: ReactNode }) => {
- const workerRef = useRef(null);
- const [ready, setReady] = useState(false);
- const mutex = useRef(new Mutex());
- const { files, writeFile } = useEmbedContext();
-
- const messageCallbacks = useRef<
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- Map void, (error: string) => void]>
- >(new Map());
- const nextMessageId = useRef(0);
-
- // Worker-specific state
- const interruptBuffer = useRef(null);
- const capabilities = useRef(null);
- const commandHistory = useRef([]);
-
- // Generic postMessage
- function postMessage(message: Omit & { payload: MessageToWorker['payload']}) {
- const id = nextMessageId.current++;
- return new Promise((resolve, reject) => {
- messageCallbacks.current.set(id, [resolve, reject]);
- workerRef.current?.postMessage({ id, ...message });
- });
- }
-
- const initializeWorker = useCallback(() => {
- const worker = new Worker(config.workerUrl);
- workerRef.current = worker;
-
- // Always create and provide the buffer
- interruptBuffer.current = new Uint8Array(new SharedArrayBuffer(1));
-
- worker.onmessage = (event) => {
- const data = event.data as MessageFromWorker;
- if (messageCallbacks.current.has(data.id)) {
- const [resolve, reject] = messageCallbacks.current.get(data.id)!;
- if ("error" in data) {
- reject(data.error);
- } else {
- resolve(data.payload);
- }
- messageCallbacks.current.delete(data.id);
- }
- };
-
- return postMessage({
- type: "init",
- payload: { interruptBuffer: interruptBuffer.current },
- }).then((payload) => {
- if (payload.success) {
- capabilities.current = payload.capabilities;
- setReady(true);
- }
- return worker;
- });
- }, []); // config.workerUrl is constant
-
- // Initialization effect
- useEffect(() => {
- let worker: Worker | null = null;
- initializeWorker().then((w) => {
- worker = w;
- });
- return () => {
- worker?.terminate();
- };
- }, [initializeWorker]);
-
- const interrupt = useCallback(() => {
- if (!capabilities.current) return;
-
- const method = capabilities.current.interrupt;
-
- if (method === "buffer" && interruptBuffer.current) {
- interruptBuffer.current[0] = 2;
- } else if (method === "restart") {
- // Reject all pending promises
- const error = "Worker interrupted";
- messageCallbacks.current.forEach(([, reject]) => reject(error));
- messageCallbacks.current.clear();
-
- workerRef.current?.terminate();
- setReady(false);
-
- void mutex.current.runExclusive(async () => {
- await initializeWorker();
- if (commandHistory.current.length > 0) {
- await postMessage<{ success: boolean }>({
- type: "restoreState",
- payload: { commands: commandHistory.current },
- });
- }
- });
- }
- }, [initializeWorker]);
-
- const runCommand = useCallback(
- async (code: string): Promise => {
- if (!mutex.current.isLocked()) {
- throw new Error(
- `mutex of ${config.providerName} must be locked for runCommand`
- );
- }
- if (!workerRef.current || !ready) {
- return [
- {
- type: "error",
- message: `${config.languageName} runtime is not ready yet.`,
- },
- ];
- }
-
- if (
- capabilities.current?.interrupt === "buffer" &&
- interruptBuffer.current
- ) {
- interruptBuffer.current[0] = 0;
- }
-
- try {
- const { output, updatedFiles } =
- await postMessage({
- type: "runCode",
- payload: { code },
- });
-
- for (const [name, content] of updatedFiles) {
- writeFile(name, content);
- }
-
- // Save command to history if interrupt method is 'restart'
- if (capabilities.current?.interrupt === "restart") {
- const hasError = output.some((o) => o.type === "error");
- if (!hasError) {
- commandHistory.current.push(code);
- }
- }
-
- return output;
- } catch (error) {
- if (error instanceof Error) {
- return [{ type: "error", message: error.message }];
- }
- return [{ type: "error", message: String(error) }];
- }
- },
- [ready, writeFile] // config is constant
- );
-
- const checkSyntax = useCallback(
- async (code: string): Promise => {
- if (!workerRef.current || !ready) return "invalid";
- const { status } = await mutex.current.runExclusive(() =>
- postMessage({
- type: "checkSyntax",
- payload: { code },
- })
- );
- return status;
- },
- [ready]
- );
-
- const runFiles = useCallback(
- async (filenames: string[]): Promise => {
- if (filenames.length !== 1) {
- return [
- {
- type: "error",
- message: `${config.languageName} execution requires exactly one filename.`,
- },
- ];
- }
- if (!workerRef.current || !ready) {
- return [
- {
- type: "error",
- message: `${config.languageName} runtime is not ready yet.`,
- },
- ];
- }
- if (
- capabilities.current?.interrupt === "buffer" &&
- interruptBuffer.current
- ) {
- interruptBuffer.current[0] = 0;
- }
- return mutex.current.runExclusive(async () => {
- const { output, updatedFiles } =
- await postMessage({
- type: "runFile",
- payload: { name: filenames[0], files },
- });
- for (const [newName, content] of updatedFiles) {
- writeFile(newName, content);
- }
- return output;
- });
- },
- [files, ready, writeFile] // config is constant
- );
-
- return (
-
- {children}
-
- );
- };
-
- return { Provider, useRuntime };
-}
diff --git a/app/terminal/worker/jsEval.ts b/app/terminal/worker/jsEval.ts
new file mode 100644
index 0000000..0023a9f
--- /dev/null
+++ b/app/terminal/worker/jsEval.ts
@@ -0,0 +1,38 @@
+"use client";
+
+import { createContext, useContext } from "react";
+import { RuntimeContext } from "../runtime";
+import { ReplCommand, ReplOutput } from "../repl";
+
+export const JSEvalContext = createContext(null!);
+
+export function useJSEval() {
+ const context = useContext(JSEvalContext);
+ if (!context) {
+ throw new Error("useJSEval must be used within a JSEvalProvider");
+ }
+ return {
+ ...context,
+ splitReplExamples,
+ // getCommandlineStr,
+ };
+}
+
+function splitReplExamples(content: string): ReplCommand[] {
+ const initCommands: { command: string; output: ReplOutput[] }[] = [];
+ for (const line of content.split("\n")) {
+ if (line.startsWith("> ")) {
+ // Remove the prompt from the command
+ initCommands.push({ command: line.slice(2), output: [] });
+ } else {
+ // Lines without prompt are output from the previous command
+ if (initCommands.length > 0) {
+ initCommands[initCommands.length - 1].output.push({
+ type: "stdout",
+ message: line,
+ });
+ }
+ }
+ }
+ return initCommands;
+}
diff --git a/app/terminal/worker/pyodide.ts b/app/terminal/worker/pyodide.ts
new file mode 100644
index 0000000..eb616b7
--- /dev/null
+++ b/app/terminal/worker/pyodide.ts
@@ -0,0 +1,46 @@
+"use client";
+
+import { createContext, useContext } from "react";
+import { RuntimeContext } from "../runtime";
+import { ReplCommand, ReplOutput } from "../repl";
+
+export const PyodideContext = createContext(null!);
+
+export function usePyodide() {
+ const context = useContext(PyodideContext);
+ if (!context) {
+ throw new Error("usePyodide must be used within a PyodideProvider");
+ }
+ return {
+ ...context,
+ splitReplExamples,
+ getCommandlineStr,
+ };
+}
+
+function splitReplExamples(content: string): ReplCommand[] {
+ const initCommands: { command: string; output: ReplOutput[] }[] = [];
+ for (const line of content.split("\n")) {
+ if (line.startsWith(">>> ")) {
+ // Remove the prompt from the command
+ initCommands.push({ command: line.slice(4), output: [] });
+ } else if (line.startsWith("... ")) {
+ if (initCommands.length > 0) {
+ initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
+ }
+ } else {
+ // Lines without prompt are output from the previous command
+ if (initCommands.length > 0) {
+ initCommands[initCommands.length - 1].output.push({
+ type: "stdout",
+ message: line,
+ });
+ }
+ }
+ }
+ return initCommands;
+}
+
+function getCommandlineStr(filenames: string[]) {
+ return `python ${filenames[0]}`;
+}
diff --git a/app/terminal/worker/ruby.ts b/app/terminal/worker/ruby.ts
new file mode 100644
index 0000000..a8e1ff2
--- /dev/null
+++ b/app/terminal/worker/ruby.ts
@@ -0,0 +1,47 @@
+"use client";
+
+import { createContext, useContext } from "react";
+import { RuntimeContext } from "../runtime";
+import { ReplCommand, ReplOutput } from "../repl";
+
+export const RubyContext = createContext(null!);
+
+export function useRuby() {
+ const context = useContext(RubyContext);
+ if (!context) {
+ throw new Error("useRuby must be used within a RubyProvider");
+ }
+ return {
+ ...context,
+ splitReplExamples,
+ getCommandlineStr,
+ };
+}
+
+function splitReplExamples(content: string): ReplCommand[] {
+ const initCommands: { command: string; output: ReplOutput[] }[] = [];
+ for (const line of content.split("\n")) {
+ if (line.startsWith(">> ")) {
+ // Ruby IRB uses >> as the prompt
+ initCommands.push({ command: line.slice(3), output: [] });
+ } else if (line.startsWith("?> ")) {
+ // Ruby IRB uses ?> for continuation
+ if (initCommands.length > 0) {
+ initCommands[initCommands.length - 1].command += "\n" + line.slice(3);
+ }
+ } else {
+ // Lines without prompt are output from the previous command
+ if (initCommands.length > 0) {
+ initCommands[initCommands.length - 1].output.push({
+ type: "stdout",
+ message: line,
+ });
+ }
+ }
+ }
+ return initCommands;
+}
+
+function getCommandlineStr(filenames: string[]) {
+ return `ruby ${filenames[0]}`;
+}
diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx
new file mode 100644
index 0000000..78ed53f
--- /dev/null
+++ b/app/terminal/worker/runtime.tsx
@@ -0,0 +1,266 @@
+"use client";
+
+import {
+ Context,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { RuntimeContext } from "../runtime";
+import { ReplOutput, SyntaxStatus } from "../repl";
+import { Mutex, MutexInterface } from "async-mutex";
+import { useEmbedContext } from "../embedContext";
+
+type MessageToWorker =
+ | { type: "init"; payload: { interruptBuffer: Uint8Array } }
+ | { type: "runCode"; payload: { code: string } }
+ | { type: "checkSyntax"; payload: { code: string } }
+ | {
+ type: "runFile";
+ payload: { name: string; files: Record };
+ }
+ | { type: "restoreState"; payload: { commands: string[] } };
+
+type MessageFromWorker =
+ | { id: number; payload: unknown }
+ | { id: number; error: string };
+
+type WorkerCapabilities = {
+ interrupt: "buffer" | "restart";
+};
+type InitPayloadFromWorker = {
+ capabilities: WorkerCapabilities;
+};
+type RunPayloadFromWorker = {
+ output: ReplOutput[];
+ updatedFiles: [string, string][];
+};
+type StatusPayloadFromWorker = { status: SyntaxStatus };
+
+export function WorkerProvider({
+ children,
+ context,
+ script,
+}: {
+ children: ReactNode;
+ context: Context;
+ script: string;
+}) {
+ const workerRef = useRef(null);
+ const [ready, setReady] = useState(false);
+ const mutex = useRef(new Mutex());
+ const { files, writeFile } = useEmbedContext();
+
+ const messageCallbacks = useRef<
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ Map void, (error: string) => void]>
+ >(new Map());
+ const nextMessageId = useRef(0);
+
+ // Worker-specific state
+ const interruptBuffer = useRef(null);
+ const capabilities = useRef(null);
+ const commandHistory = useRef([]);
+
+ // Generic postMessage
+ function postMessage(message: MessageToWorker) {
+ const id = nextMessageId.current++;
+ return new Promise((resolve, reject) => {
+ messageCallbacks.current.set(id, [resolve, reject]);
+ workerRef.current?.postMessage({ id, ...message });
+ });
+ }
+
+ const initializeWorker = useCallback(async () => {
+ const worker = new Worker(script);
+ workerRef.current = worker;
+
+ // Always create and provide the buffer
+ interruptBuffer.current = new Uint8Array(new SharedArrayBuffer(1));
+
+ worker.onmessage = (event) => {
+ const data = event.data as MessageFromWorker;
+ if (messageCallbacks.current.has(data.id)) {
+ const [resolve, reject] = messageCallbacks.current.get(data.id)!;
+ if ("error" in data) {
+ reject(data.error);
+ } else {
+ resolve(data.payload);
+ }
+ messageCallbacks.current.delete(data.id);
+ }
+ };
+
+ return postMessage({
+ type: "init",
+ payload: { interruptBuffer: interruptBuffer.current },
+ }).then((payload) => {
+ capabilities.current = payload.capabilities;
+ });
+ }, [script]);
+
+ // Initialization effect
+ useEffect(() => {
+ initializeWorker().then(() => setReady(true));
+ return () => {
+ workerRef.current?.terminate();
+ };
+ }, [initializeWorker]);
+
+ const interrupt = useCallback(() => {
+ if (!capabilities.current) return;
+
+ switch (capabilities.current?.interrupt) {
+ case "buffer":
+ if (interruptBuffer.current) {
+ interruptBuffer.current[0] = 2;
+ }
+ break;
+ case "restart": {
+ // Reject all pending promises
+ const error = "Worker interrupted";
+ messageCallbacks.current.forEach(([, reject]) => reject(error));
+ messageCallbacks.current.clear();
+
+ workerRef.current?.terminate();
+ setReady(false);
+
+ void mutex.current.runExclusive(async () => {
+ await initializeWorker();
+ if (commandHistory.current.length > 0) {
+ await postMessage