diff --git a/app/terminal/README.md b/app/terminal/README.md index 9e44cd4..f0044d1 100644 --- a/app/terminal/README.md +++ b/app/terminal/README.md @@ -14,8 +14,10 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ * ランタイムの初期化が完了したか、不要である場合true * mutex?: `MutexInterface` * ランタイムに排他制御が必要な場合、MutexInterfaceのインスタンスを返してください。 -* interrupt?: `() => Promise` - * 実行中のコマンドを中断します。呼び出し側でmutexのロックはされません +* interrupt?: `() => void` + * 実行中のコマンドを中断します。 + * 呼び出し側でmutexのロックはしません。interrupt()を呼ぶ際にはrunCommand()やrunFiles()が実行中であるためmutexはすでにロックされているはずです。 + * interrupt()内で実行中の処理のPromiseをrejectしたあと、runtimeを再開する際の処理に必要であればmutexをロックすることも可能です。 ### REPL用 @@ -25,7 +27,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ * checkSyntax?: `(code: string) => Promise` * コードの構文チェックを行います。行がコマンドとして完結していれば`complete`、次の行に続く場合(if文の条件式の途中など)は`incomplete`を返してください。 * REPLでEnterを押した際の動作に影響します。 - * 呼び出し側でmutexのロックはされません + * 呼び出し側でmutexのロックはせず、必要であればcheckSyntax()内でロックします。 * splitReplExamples?: `(code: string) => ReplCommands[]` * markdown内に記述されているREPLのサンプルコードをパースします。例えば ``` @@ -51,7 +53,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ * runFiles: `(filenames: string[]) => Promise` * 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。 - * 呼び出し側でmutexのロックはされません + * 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。 * getCommandlineStr: `(filenames: string[]) => string` * 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。 diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 0b8f932..4e81830 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -11,6 +11,7 @@ const AceEditor = dynamic( await import("ace-builds/src-min-noconflict/ext-language_tools"); await import("ace-builds/src-min-noconflict/ext-searchbox"); await import("ace-builds/src-min-noconflict/mode-python"); + await import("ace-builds/src-min-noconflict/mode-ruby"); await import("ace-builds/src-min-noconflict/mode-c_cpp"); await import("ace-builds/src-min-noconflict/mode-json"); await import("ace-builds/src-min-noconflict/mode-csv"); @@ -28,13 +29,16 @@ import { langConstants } from "./runtime"; // snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python"; // mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する -export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text"; +export type AceLang = "python" | "ruby" | "c_cpp" | "json" | "csv" | "text"; export function getAceLang(lang: string | undefined): AceLang { // Markdownで指定される可能性のある言語名からAceLangを取得 switch (lang) { case "python": case "py": return "python"; + case "ruby": + case "rb": + return "ruby"; case "cpp": case "c++": return "c_cpp"; diff --git a/app/terminal/highlight.ts b/app/terminal/highlight.ts index e5be201..f11202a 100644 --- a/app/terminal/highlight.ts +++ b/app/terminal/highlight.ts @@ -3,13 +3,17 @@ import chalk from "chalk"; import { RuntimeLang } from "./runtime"; // Python言語定義をインポート import "prismjs/components/prism-python"; +// Ruby言語定義をインポート +import "prismjs/components/prism-ruby"; -type PrismLang = "python"; +type PrismLang = "python" | "ruby"; function getPrismLanguage(language: RuntimeLang): PrismLang { switch (language) { case "python": return "python"; + case "ruby": + return "ruby"; case "cpp": throw new Error( `highlight for ${language} is disabled because it should not support REPL` diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index 857523b..4bfd2d2 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -4,6 +4,7 @@ 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"; @@ -11,10 +12,12 @@ import { defineTests } from "./tests"; export default function RuntimeTestPage() { const pyodide = usePyodide(); + const ruby = useRuby(); const wandboxCpp = useWandbox("cpp"); const runtimeRef = useRef>(null!); runtimeRef.current = { python: pyodide, + ruby: ruby, cpp: wandboxCpp, }; const { files, writeFile } = useEmbedContext(); diff --git a/app/terminal/ruby/page.tsx b/app/terminal/ruby/page.tsx new file mode 100644 index 0000000..a986738 --- /dev/null +++ b/app/terminal/ruby/page.tsx @@ -0,0 +1,23 @@ +"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 new file mode 100644 index 0000000..716b09b --- /dev/null +++ b/app/terminal/ruby/runtime.tsx @@ -0,0 +1,276 @@ +"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"; + +const RubyContext = createContext(null!); + +export function useRuby(): RuntimeContext { + const context = useContext(RubyContext); + if (!context) { + throw new Error("useRuby must be used within a RubyProvider"); + } + return context; +} + +type MessageToWorker = + | { + type: "init"; + payload: {}; + } + | { + type: "runRuby"; + payload: { code: string }; + } + | { + type: "checkSyntax"; + payload: { code: string }; + } + | { + type: "runFile"; + payload: { name: string; files: Record }; + }; + +type MessageFromWorker = + | { id: number; payload: unknown } + | { id: number; error: string }; + +type InitPayloadFromWorker = { success: boolean }; +type RunPayloadFromWorker = { + output: ReplOutput[]; + updatedFiles: [string, string][]; +}; +type StatusPayloadFromWorker = { status: SyntaxStatus }; + +export function RubyProvider({ 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); + const commandHistory = useRef([]); + + function postMessage({ type, payload }: MessageToWorker) { + const id = nextMessageId.current++; + return new Promise((resolve, reject) => { + messageCallbacks.current.set(id, [resolve, reject]); + workerRef.current?.postMessage({ id, type, payload }); + }); + } + + const initializeWorker = useCallback(() => { + const worker = new Worker("/ruby.worker.js"); + workerRef.current = worker; + + 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: {}, + }); + }, []); + + useEffect(() => { + initializeWorker().then(({ success }) => { + if (success) { + setReady(true); + } + }); + + return () => { + workerRef.current?.terminate(); + }; + }, [initializeWorker]); + + const interrupt = useCallback(() => { + // Terminate the current worker + if (workerRef.current) { + workerRef.current.terminate(); + } + + // reject all pending messages + for (const [, [, reject]] of messageCallbacks.current) { + reject("Execution interrupted"); + } + + // Mark as not ready during reinitialization + setReady(false); + + void mutex.current.runExclusive(async () => { + // Reinitialize the worker + const { success } = await initializeWorker(); + + if (success) { + // Re-execute all saved commands to restore state + for (const cmd of commandHistory.current) { + try { + await postMessage({ + type: "runRuby", + payload: { code: cmd }, + }); + } catch (e) { + console.error("Error restoring command:", cmd, e); + } + } + setReady(true); + } + }); + }, [initializeWorker]); + + const runCommand = useCallback( + async (code: string): Promise => { + if (!mutex.current.isLocked()) { + throw new Error("mutex of RubyContext must be locked for runCommand"); + } + if (!workerRef.current || !ready) { + return [{ type: "error", message: "Ruby VM is not ready yet." }]; + } + + const { output, updatedFiles } = await postMessage({ + type: "runRuby", + payload: { code }, + }).catch((error) => { + return { + output: [ + { type: "error", message: `Execution error: ${error}` }, + ] as ReplOutput[], + updatedFiles: [] as [string, string][], + }; + }); + + // Check if the command succeeded (no errors) + const hasError = output.some((o) => o.type === "error"); + if (!hasError) { + // Save successful command to history + commandHistory.current.push(code); + } + + for (const [name, content] of updatedFiles) { + writeFile(name, content); + } + return output; + }, + [ready, writeFile] + ); + + 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: "Ruby execution requires exactly one filename", + }, + ]; + } + if (!workerRef.current || !ready) { + return [{ type: "error", message: "Ruby VM is not ready yet." }]; + } + return mutex.current.runExclusive(async () => { + const { output, updatedFiles } = + await postMessage({ + type: "runFile", + payload: { name: filenames[0], files }, + }).catch((error) => { + return { + output: [ + { type: "error", message: `Execution error: ${error}` }, + ] as ReplOutput[], + updatedFiles: [] as [string, string][], + }; + }); + for (const [newName, content] of updatedFiles) { + writeFile(newName, content); + } + return output; + }); + }, + [files, ready, writeFile] + ); + + const splitReplExamples = useCallback((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; + }, []); + + const getCommandlineStr = useCallback( + (filenames: string[]) => `ruby ${filenames[0]}`, + [] + ); + + return ( + + {children} + + ); +} diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 7292c19..117754c 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -1,6 +1,7 @@ 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 { AceLang } from "./editor"; import { ReactNode } from "react"; @@ -28,7 +29,7 @@ export interface LangConstants { prompt?: string; promptMore?: string; } -export type RuntimeLang = "python" | "cpp"; +export type RuntimeLang = "python" | "ruby" | "cpp"; export function getRuntimeLang( lang: string | undefined @@ -38,6 +39,9 @@ export function getRuntimeLang( case "python": case "py": return "python"; + case "ruby": + case "rb": + return "ruby"; case "cpp": case "c++": return "cpp"; @@ -49,11 +53,14 @@ export function getRuntimeLang( export function useRuntime(language: RuntimeLang): RuntimeContext { // すべての言語のcontextをインスタンス化 const pyodide = usePyodide(); + const ruby = useRuby(); const wandboxCpp = useWandbox("cpp"); switch (language) { case "python": return pyodide; + case "ruby": + return ruby; case "cpp": return wandboxCpp; default: @@ -64,7 +71,9 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { export function RuntimeProvider({ children }: { children: ReactNode }) { return ( - {children} + + {children} + ); } @@ -77,6 +86,12 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { prompt: ">>> ", promptMore: "... ", }; + case "ruby": + return { + tabSize: 2, + prompt: ">> ", + promptMore: "?> ", + }; case "c_cpp": case "cpp": return { diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 82620ae..8cf6fae 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -12,6 +12,7 @@ export function defineTests( ( { python: 2000, + ruby: 5000, cpp: 10000, } as Record )[lang] @@ -30,6 +31,7 @@ export function defineTests( const printCode = ( { python: `print("${msg}")`, + ruby: `puts "${msg}"`, cpp: null, } satisfies Record )[lang]; @@ -54,6 +56,7 @@ export function defineTests( const [setIntVarCode, printIntVarCode] = ( { python: [`${varName} = ${value}`, `print(${varName})`], + ruby: [`${varName} = ${value}`, `puts ${varName}`], cpp: [null, null], } satisfies Record )[lang]; @@ -80,6 +83,7 @@ export function defineTests( const errorCode = ( { python: `raise Exception("${errorMsg}")`, + ruby: `raise "${errorMsg}"`, cpp: null, } satisfies Record )[lang]; @@ -99,23 +103,33 @@ export function defineTests( const [setIntVarCode, infLoopCode, printIntVarCode] = ( { python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`], + ruby: [`testVar = 42`, `loop do\nend`, `puts testVar`], cpp: [null, null, null], } satisfies Record )[lang]; if (!setIntVarCode || !infLoopCode || !printIntVarCode) { this.skip(); } - const result = await ( + const runPromise = ( runtimeRef.current[lang].mutex || emptyMutex ).runExclusive(async () => { await runtimeRef.current[lang].runCommand!(setIntVarCode); - const runPromise = runtimeRef.current[lang].runCommand!(infLoopCode); - // Wait a bit to ensure the infinite loop has started - await new Promise((resolve) => setTimeout(resolve, 1000)); - runtimeRef.current[lang].interrupt!(); - await runPromise; - return runtimeRef.current[lang].runCommand!(printIntVarCode); + return runtimeRef.current[lang].runCommand!(infLoopCode); }); + // Wait a bit to ensure the infinite loop has started + await new Promise((resolve) => setTimeout(resolve, 1000)); + runtimeRef.current[lang].interrupt!(); + await new Promise((resolve) => setTimeout(resolve, 100)); + await runPromise; + while (!runtimeRef.current[lang].ready) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + await new Promise((resolve) => setTimeout(resolve, 100)); + const result = await ( + runtimeRef.current[lang].mutex || emptyMutex + ).runExclusive(() => + runtimeRef.current[lang].runCommand!(printIntVarCode) + ); console.log(`${lang} REPL interrupt recovery test: `, result); expect(result).to.be.deep.equal([ { @@ -132,6 +146,7 @@ export function defineTests( const [filename, code] = ( { python: ["test.py", `print("${msg}")`], + ruby: ["test.rb", `puts "${msg}"`], cpp: [ "test.cpp", `#include \nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`, @@ -156,6 +171,7 @@ export function defineTests( const [filename, code] = ( { python: ["test_error.py", `raise Exception("${errorMsg}")\n`], + ruby: ["test_error.rb", `raise "${errorMsg}"\n`], cpp: [ "test_error.cpp", `#include \nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`, @@ -183,6 +199,14 @@ export function defineTests( }, ["test_multi_main.py"], ], + ruby: [ + { + "test_multi_main.rb": + "require_relative 'test_multi_sub'\nprint_message\n", + "test_multi_sub.rb": `def print_message\n puts "${msg}"\nend\n`, + }, + ["test_multi_main.rb"], + ], cpp: [ { "test_multi_main.cpp": diff --git a/public/_headers b/public/_headers index 2472c04..c88c393 100644 --- a/public/_headers +++ b/public/_headers @@ -3,3 +3,6 @@ /pyodide.worker.js Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp +/ruby.worker.js + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp diff --git a/public/ruby.worker.js b/public/ruby.worker.js new file mode 100644 index 0000000..e4d2522 --- /dev/null +++ b/public/ruby.worker.js @@ -0,0 +1,307 @@ +// Ruby.wasm web worker +let rubyVM = null; +let rubyOutput = []; +let stdoutBuffer = ""; +let stderrBuffer = ""; + +const RUBY_JS_URL = + "https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.7.2/dist/browser.umd.js"; +const RUBY_WASM_URL = + "https://cdn.jsdelivr.net/npm/@ruby/3.4-wasm-wasi@2.7.2/dist/ruby+stdlib.wasm"; + +globalThis.stdout = { + write(str) { + stdoutBuffer += str; + }, +}; +globalThis.stderr = { + write(str) { + stderrBuffer += str; + }, +}; + +async function init(id, payload) { + // const { } = payload; + + if (!rubyVM) { + try { + importScripts(RUBY_JS_URL); + + // Fetch and compile the Ruby WASM module + const rubyModule = await WebAssembly.compileStreaming( + await fetch(RUBY_WASM_URL) + ); + const { DefaultRubyVM } = globalThis["ruby-wasm-wasi"]; + const { vm } = await DefaultRubyVM(rubyModule); + rubyVM = vm; + + rubyVM.eval(` +$stdout = Object.new.tap do |obj| + def obj.write(str) + require "js" + JS.global[:stdout].write(str) + end +end +$stderr = Object.new.tap do |obj| + def obj.write(str) + require "js" + JS.global[:stderr].write(str) + end +end +`); + } catch (e) { + console.error("Failed to initialize Ruby VM:", e); + self.postMessage({ + id, + error: `Failed to initialize Ruby: ${e.message}`, + }); + return; + } + } + + self.postMessage({ id, payload: { success: true } }); +} + +function flushOutput() { + if (stdoutBuffer) { + const lines = stdoutBuffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + rubyOutput.push({ type: "stdout", message: lines[i] }); + } + stdoutBuffer = lines[lines.length - 1]; + } + // Final flush if there's remaining non-empty text + if (stdoutBuffer && stdoutBuffer.trim()) { + rubyOutput.push({ type: "stdout", message: stdoutBuffer }); + } + stdoutBuffer = ""; + + if (stderrBuffer) { + const lines = stderrBuffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + rubyOutput.push({ type: "stderr", message: lines[i] }); + } + stderrBuffer = lines[lines.length - 1]; + } + if (stderrBuffer && stderrBuffer.trim()) { + rubyOutput.push({ type: "stderr", message: stderrBuffer }); + } + stderrBuffer = ""; +} + +function formatRubyError(error) { + if (!(error instanceof Error)) { + return `予期せぬエラー: ${String(error).trim()}`; + } + + return error.message; +} + +async function runRuby(id, payload) { + const { code } = payload; + + if (!rubyVM) { + self.postMessage({ id, error: "Ruby VM not initialized" }); + return; + } + + try { + rubyOutput = []; + stdoutBuffer = ""; + stderrBuffer = ""; + + const result = rubyVM.eval(code); + + // Flush any buffered output + flushOutput(); + + const resultStr = result.toString(); + + // Add result to output if it's not nil and not empty + if (resultStr !== "" && resultStr !== "nil") { + rubyOutput.push({ + type: "return", + message: resultStr, + }); + } + } catch (e) { + console.log(e); + flushOutput(); + + rubyOutput.push({ + type: "error", + message: formatRubyError(e), + }); + } + + const updatedFiles = readAllFiles(); + const output = [...rubyOutput]; + rubyOutput = []; + + self.postMessage({ + id, + payload: { output, updatedFiles }, + }); +} + +async function runFile(id, payload) { + const { name, files } = payload; + + if (!rubyVM) { + self.postMessage({ id, error: "Ruby VM not initialized" }); + return; + } + + try { + rubyOutput = []; + stdoutBuffer = ""; + stderrBuffer = ""; + + // Write files to the virtual file system + for (const [filename, content] of Object.entries(files)) { + if (content) { + rubyVM.eval( + `File.write(${JSON.stringify(filename)}, ${JSON.stringify(content)})` + ); + } + } + + // Run the specified file + const fileContent = files[name]; + if (!fileContent) { + throw new Error(`File not found: ${name}`); + } + + rubyVM.eval(fileContent); + + // Flush any buffered output + flushOutput(); + } catch (e) { + console.log(e); + flushOutput(); + + rubyOutput.push({ + type: "error", + message: formatRubyError(e), + }); + } + + const updatedFiles = readAllFiles(); + const output = [...rubyOutput]; + rubyOutput = []; + + self.postMessage({ + id, + payload: { output, updatedFiles }, + }); +} + +async function checkSyntax(id, payload) { + const { code } = payload; + + if (!rubyVM) { + self.postMessage({ + id, + payload: { status: "invalid" }, + }); + return; + } + + try { + // Try to parse the code to check syntax + // Ruby doesn't have a built-in compile_command like Python + // We'll use a simple heuristic + const trimmed = code.trim(); + + // Check for incomplete syntax patterns + const incompletePatterns = [ + /\bif\b.*(? pattern.test(trimmed))) { + self.postMessage({ id, payload: { status: "incomplete" } }); + return; + } + + // Try to compile/evaluate in check mode + try { + rubyVM.eval(`BEGIN { raise "check" }; ${code}`); + } catch (e) { + // If it's our check exception, syntax is valid + if (e.message && e.message.includes("check")) { + self.postMessage({ id, payload: { status: "complete" } }); + return; + } + // Otherwise it's a syntax error + self.postMessage({ id, payload: { status: "invalid" } }); + return; + } + + self.postMessage({ id, payload: { status: "complete" } }); + } catch (e) { + console.error("Syntax check error:", e); + self.postMessage({ + id, + payload: { status: "invalid" }, + }); + } +} + +// Helper function to read all files from the virtual file system +function readAllFiles() { + if (!rubyVM) return []; + const updatedFiles = []; + + try { + // Get list of files in the home directory + const result = rubyVM.eval(` + require 'json' + files = {} + Dir.glob('*').each do |filename| + if File.file?(filename) + files[filename] = File.read(filename) + end + end + JSON.generate(files) + `); + const filesObj = JSON.parse(result.toString()); + for (const [filename, content] of Object.entries(filesObj)) { + updatedFiles.push([filename, content]); + } + } catch (e) { + console.error("Error reading files:", e); + } + + return updatedFiles; +} + +self.onmessage = async (event) => { + const { id, type, payload } = event.data; + + switch (type) { + case "init": + await init(id, payload); + return; + case "runRuby": + await runRuby(id, payload); + return; + case "runFile": + await runFile(id, payload); + return; + case "checkSyntax": + await checkSyntax(id, payload); + return; + default: + console.error(`Unknown message type: ${type}`); + return; + } +};