diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index d266282..1377410 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -1,7 +1,8 @@ /// +import { expose } from "comlink"; import type { ReplOutput } from "../repl"; -import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; +import type { WorkerCapabilities } from "./runtime"; import inspect from "object-inspect"; function format(...args: unknown[]): string { @@ -29,12 +30,12 @@ self.console = { }, }; -async function init({ id }: WorkerRequest["init"]) { +async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ + capabilities: WorkerCapabilities; +}> { // Initialize the worker and report capabilities - self.postMessage({ - id, - payload: { capabilities: { interrupt: "restart" } }, - } satisfies WorkerResponse["init"]); + // interruptBuffer is not used for JavaScript (restart-based interruption) + return { capabilities: { interrupt: "restart" } }; } async function replLikeEval(code: string): Promise { @@ -72,8 +73,10 @@ async function replLikeEval(code: string): Promise { } } -async function runCode({ id, payload }: WorkerRequest["runCode"]) { - const { code } = payload; +async function runCode(code: string): Promise<{ + output: ReplOutput[]; + updatedFiles: Record; +}> { try { const result = await replLikeEval(code); jsOutput.push({ @@ -99,14 +102,13 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { const output = [...jsOutput]; jsOutput = []; // Clear output - self.postMessage({ - id, - payload: { output, updatedFiles: [] }, - } satisfies WorkerResponse["runCode"]); + return { output, updatedFiles: {} as Record }; } -function runFile({ id, payload }: WorkerRequest["runFile"]) { - const { name, files } = payload; +function runFile( + name: string, + files: Record +): { output: ReplOutput[]; updatedFiles: Record } { // pyodide worker などと異なり、複数ファイルを読み込んでimportのようなことをするのには対応していません。 try { self.eval(files[name]); @@ -129,23 +131,17 @@ function runFile({ id, payload }: WorkerRequest["runFile"]) { const output = [...jsOutput]; jsOutput = []; // Clear output - self.postMessage({ - id, - payload: { output, updatedFiles: [] }, - } satisfies WorkerResponse["runFile"]); + return { output, updatedFiles: {} as Record }; } -async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { - const { code } = payload; - +async function checkSyntax( + code: string +): Promise<{ status: "complete" | "incomplete" | "invalid" }> { try { // Try to create a Function to check syntax // new Function(code); // <- not working self.eval(`() => {${code}}`); - self.postMessage({ - id, - payload: { status: "complete" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "complete" }; } catch (e) { // Check if it's a syntax error or if more input is expected if (e instanceof SyntaxError) { @@ -154,28 +150,18 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { e.message.includes("Unexpected token '}'") || e.message.includes("Unexpected end of input") ) { - self.postMessage({ - id, - payload: { status: "incomplete" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "incomplete" }; } else { - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "invalid" }; } } else { - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "invalid" }; } } } -async function restoreState({ id, payload }: WorkerRequest["restoreState"]) { +async function restoreState(commands: string[]): Promise { // Re-execute all previously successful commands to restore state - const { commands } = payload; jsOutput = []; // Clear output for restoration for (const command of commands) { @@ -188,32 +174,15 @@ async function restoreState({ id, payload }: WorkerRequest["restoreState"]) { } jsOutput = []; // Clear any output from restoration - self.postMessage({ - id, - payload: {}, - } satisfies WorkerResponse["restoreState"]); + return {}; } -self.onmessage = async (event: MessageEvent) => { - switch (event.data.type) { - case "init": - await init(event.data); - return; - case "runCode": - await runCode(event.data); - return; - case "runFile": - runFile(event.data); - return; - case "checkSyntax": - await checkSyntax(event.data); - return; - case "restoreState": - await restoreState(event.data); - return; - default: - event.data satisfies never; - originalConsole.error(`Unknown message: ${event.data}`); - return; - } +const api = { + init, + runCode, + runFile, + checkSyntax, + restoreState, }; + +expose(api); diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index 1594de3..e6cb985 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -1,11 +1,12 @@ /// /// +import { expose } from "comlink"; import type { PyodideInterface } from "pyodide"; // import { loadPyodide } from "pyodide"; -> Reading from "node:child_process" is not handled by plugins import { version as pyodideVersion } from "pyodide/package.json"; import type { PyCallable } from "pyodide/ffi"; -import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; +import type { WorkerCapabilities } from "./runtime"; import type { ReplOutput } from "../repl"; import execfile_py from "./pyodide/execfile.py?raw"; @@ -36,8 +37,9 @@ function readAllFiles(): Record { return updatedFiles; } -async function init({ id, payload }: WorkerRequest["init"]) { - const { interruptBuffer } = payload; +async function init( + interruptBuffer: Uint8Array +): Promise<{ capabilities: WorkerCapabilities }> { if (!pyodide) { self.importScripts(`${PYODIDE_CDN}pyodide.js`); @@ -57,20 +59,15 @@ async function init({ id, payload }: WorkerRequest["init"]) { pyodide.setInterruptBuffer(interruptBuffer); } - self.postMessage({ - id, - payload: { capabilities: { interrupt: "buffer" } }, - } satisfies WorkerResponse["init"]); + return { capabilities: { interrupt: "buffer" } }; } -async function runCode({ id, payload }: WorkerRequest["runCode"]) { - const { code } = payload; +async function runCode(code: string): Promise<{ + output: ReplOutput[]; + updatedFiles: Record; +}> { if (!pyodide) { - self.postMessage({ - id, - error: "Pyodide not initialized", - } satisfies WorkerResponse["runCode"]); - return; + throw new Error("Pyodide not initialized"); } try { const result = await pyodide.runPythonAsync(code); @@ -115,20 +112,15 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { const output = [...pyodideOutput]; pyodideOutput = []; // 出力をクリア - self.postMessage({ - id, - payload: { output, updatedFiles }, - } satisfies WorkerResponse["runCode"]); + return { output, updatedFiles }; } -async function runFile({ id, payload }: WorkerRequest["runFile"]) { - const { name, files } = payload; +async function runFile( + name: string, + files: Record +): Promise<{ output: ReplOutput[]; updatedFiles: Record }> { if (!pyodide) { - self.postMessage({ - id, - error: "Pyodide not initialized", - } satisfies WorkerResponse["runFile"]); - return; + throw new Error("Pyodide not initialized"); } try { // Use Pyodide FS API to write files to the file system @@ -175,70 +167,41 @@ async function runFile({ id, payload }: WorkerRequest["runFile"]) { const updatedFiles = readAllFiles(); const output = [...pyodideOutput]; pyodideOutput = []; // 出力をクリア - self.postMessage({ - id, - payload: { output, updatedFiles }, - } satisfies WorkerResponse["runFile"]); + return { output, updatedFiles }; } -async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { - const { code } = payload; +async function checkSyntax( + code: string +): Promise<{ status: "complete" | "incomplete" | "invalid" }> { if (!pyodide) { - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "invalid" }; } // 複数行コマンドは最後に空行を入れないと完了しないものとする if (code.includes("\n") && code.split("\n").at(-1) !== "") { - self.postMessage({ - id, - payload: { status: "incomplete" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "incomplete" }; } try { // Pythonのコードを実行して結果を受け取る const status = (pyodide.runPython(check_syntax_py) as PyCallable)(code); - self.postMessage({ - id, - payload: { status }, - } satisfies WorkerResponse["checkSyntax"]); + return { status }; } catch (e) { console.error("Syntax check error:", e); - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "invalid" }; } } -self.onmessage = async (event: MessageEvent) => { - switch (event.data.type) { - case "init": - await init(event.data); - return; - case "runCode": - await runCode(event.data); - return; - case "runFile": - await runFile(event.data); - return; - case "checkSyntax": - await checkSyntax(event.data); - return; - case "restoreState": - self.postMessage({ - id: event.data.id, - error: "not implemented", - } satisfies WorkerResponse["restoreState"]); - return; - default: - event.data satisfies never; - console.error(`Unknown message: ${event.data}`); - return; - } +async function restoreState(): Promise { + throw new Error("not implemented"); +} + +const api = { + init, + runCode, + runFile, + checkSyntax, + restoreState, }; + +expose(api); diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index 0bdb918..ccc4a99 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -1,9 +1,10 @@ /// /// +import { expose } from "comlink"; import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser"; import type { RubyVM } from "@ruby/wasm-wasi/dist/vm"; -import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; +import type { WorkerCapabilities } from "./runtime"; import type { ReplOutput } from "../repl"; import init_rb from "./ruby/init.rb?raw"; @@ -28,7 +29,10 @@ self.stderr = { }, }; -async function init({ id }: WorkerRequest["init"]) { +async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ + capabilities: WorkerCapabilities; +}> { + // interruptBuffer is not used for Ruby (restart-based interruption) if (!rubyVM) { try { // Fetch and compile the Ruby WASM module @@ -41,18 +45,13 @@ async function init({ id }: WorkerRequest["init"]) { rubyVM.eval(init_rb); - self.postMessage({ - id, - payload: { capabilities: { interrupt: "restart" } }, - } satisfies WorkerResponse["init"]); + return { capabilities: { interrupt: "restart" } }; } catch (e: unknown) { console.error("Failed to initialize Ruby VM:", e); - self.postMessage({ - id, - error: `Failed to initialize Ruby: ${e}`, - } satisfies WorkerResponse["init"]); + throw new Error(`Failed to initialize Ruby: ${e}`); } } + return { capabilities: { interrupt: "restart" } }; } function flushOutput() { @@ -102,15 +101,12 @@ function formatRubyError(error: unknown, isFile: boolean): string { return errorMessage; } -async function runCode({ id, payload }: WorkerRequest["runCode"]) { - const { code } = payload; - +async function runCode(code: string): Promise<{ + output: ReplOutput[]; + updatedFiles: Record; +}> { if (!rubyVM) { - self.postMessage({ - id, - error: "Ruby VM not initialized", - } satisfies WorkerResponse["runCode"]); - return; + throw new Error("Ruby VM not initialized"); } try { @@ -144,21 +140,15 @@ async function runCode({ id, payload }: WorkerRequest["runCode"]) { const output = [...rubyOutput]; rubyOutput = []; - self.postMessage({ - id, - payload: { output, updatedFiles }, - } satisfies WorkerResponse["runCode"]); + return { output, updatedFiles }; } -async function runFile({ id, payload }: WorkerRequest["runFile"]) { - const { name, files } = payload; - +async function runFile( + name: string, + files: Record +): Promise<{ output: ReplOutput[]; updatedFiles: Record }> { if (!rubyVM) { - self.postMessage({ - id, - error: "Ruby VM not initialized", - } satisfies WorkerResponse["runFile"]); - return; + throw new Error("Ruby VM not initialized"); } try { @@ -199,21 +189,14 @@ async function runFile({ id, payload }: WorkerRequest["runFile"]) { const output = [...rubyOutput]; rubyOutput = []; - self.postMessage({ - id, - payload: { output, updatedFiles }, - } satisfies WorkerResponse["runFile"]); + return { output, updatedFiles }; } -async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { - const { code } = payload; - +async function checkSyntax( + code: string +): Promise<{ status: "complete" | "incomplete" | "invalid" }> { if (!rubyVM) { - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "invalid" }; } try { @@ -231,37 +214,19 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { e.message.includes("expected a `}` to close the hash literal") || e.message.includes("unterminated string meets end of file")) ) { - self.postMessage({ - id, - payload: { status: "incomplete" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "incomplete" }; } // If it's our check exception, syntax is valid if (e instanceof Error && e.message && e.message.includes("check")) { - self.postMessage({ - id, - payload: { status: "complete" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "complete" }; } // Otherwise it's a syntax error - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); - return; + return { status: "invalid" }; } - self.postMessage({ - id, - payload: { status: "complete" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "complete" }; } catch (e) { console.error("Syntax check error:", e); - self.postMessage({ - id, - payload: { status: "invalid" }, - } satisfies WorkerResponse["checkSyntax"]); + return { status: "invalid" }; } } @@ -295,15 +260,10 @@ function readAllFiles(): Record { return updatedFiles; } -async function restoreState({ id, payload }: WorkerRequest["restoreState"]) { +async function restoreState(commands: string[]): Promise { // Re-execute all previously successful commands to restore state - const { commands } = payload; if (!rubyVM) { - self.postMessage({ - id, - error: "Ruby VM not initialized", - } satisfies WorkerResponse["restoreState"]); - return; + throw new Error("Ruby VM not initialized"); } rubyOutput = []; // Clear output for restoration @@ -323,32 +283,15 @@ async function restoreState({ id, payload }: WorkerRequest["restoreState"]) { flushOutput(); rubyOutput = []; - self.postMessage({ - id, - payload: {}, - } satisfies WorkerResponse["restoreState"]); + return {}; } -self.onmessage = async (event: MessageEvent) => { - switch (event.data.type) { - case "init": - await init(event.data); - return; - case "runCode": - await runCode(event.data); - return; - case "runFile": - await runFile(event.data); - return; - case "checkSyntax": - await checkSyntax(event.data); - return; - case "restoreState": - await restoreState(event.data); - return; - default: - event.data satisfies never; - console.error(`Unknown message: ${event.data}`); - return; - } +const api = { + init, + runCode, + runFile, + checkSyntax, + restoreState, }; + +expose(api); diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index b8c18cd..99c2bf0 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -9,55 +9,32 @@ import { useRef, useState, } from "react"; +import { wrap, Remote } from "comlink"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput, SyntaxStatus } from "../repl"; import { Mutex, MutexInterface } from "async-mutex"; import { useEmbedContext } from "../embedContext"; type WorkerLang = "python" | "ruby" | "javascript"; -type WorkerCapabilities = { +export type WorkerCapabilities = { interrupt: "buffer" | "restart"; }; -export type MessageType = keyof MessagePayload; -export type MessagePayload = { - init: { - req: { interruptBuffer: Uint8Array }; - res: { capabilities: WorkerCapabilities }; - }; - runCode: { - req: { code: string }; - res: { output: ReplOutput[]; updatedFiles: Record }; - }; - runFile: { - req: { name: string; files: Record }; - res: { output: ReplOutput[]; updatedFiles: Record }; - }; - checkSyntax: { - req: { code: string }; - res: { status: SyntaxStatus }; - }; - restoreState: { - req: { commands: string[] }; - res: object; - }; -}; -// export type WorkerRequest = { id: number; type: "init"; payload: MessagePayload["init"]["req"]; } | ... と同じ -export type WorkerRequest = { - [K in MessageType]: { - id: number; - type: K; - payload: MessagePayload[K]["req"]; - }; -}; -export type WorkerResponse = { - [K in MessageType]: - | { - id: number; - payload: MessagePayload[MessageType]["res"]; - } - | { id: number; error: string }; -}; +// Define the worker API interface +export interface WorkerAPI { + init( + interruptBuffer: Uint8Array + ): Promise<{ capabilities: WorkerCapabilities }>; + runCode( + code: string + ): Promise<{ output: ReplOutput[]; updatedFiles: Record }>; + runFile( + name: string, + files: Record + ): Promise<{ output: ReplOutput[]; updatedFiles: Record }>; + checkSyntax(code: string): Promise<{ status: SyntaxStatus }>; + restoreState(commands: string[]): Promise; +} export function WorkerProvider({ children, @@ -69,32 +46,18 @@ export function WorkerProvider({ lang: WorkerLang; }) { const workerRef = useRef(null); + const workerApiRef = useRef | null>(null); const [ready, setReady] = useState(false); const mutex = useMemo(() => new Mutex(), []); const { 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( - type: K, - payload: MessagePayload[K]["req"] - ) { - const id = nextMessageId.current++; - return new Promise((resolve, reject) => { - messageCallbacks.current.set(id, [resolve, reject]); - workerRef.current?.postMessage({ id, type, payload } as WorkerRequest[K]); - }); - } + // Track pending promises for restart-based interruption + const pendingPromises = useRef void>>(new Set()); const initializeWorker = useCallback(async () => { if (!mutex.isLocked()) { @@ -122,32 +85,34 @@ export function WorkerProvider({ } workerRef.current = worker; - // Always create and provide the buffer - interruptBuffer.current = new Uint8Array(new SharedArrayBuffer(1)); + // Wrap the worker with Comlink + const workerApi = wrap(worker); + workerApiRef.current = workerApi; - worker.onmessage = (event) => { - const data = event.data as WorkerResponse[MessageType]; - 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); - } - }; + // Always create and provide the buffer for Python (others ignore it) + interruptBuffer.current = new Uint8Array(new SharedArrayBuffer(1)); - return postMessage("init", { - interruptBuffer: interruptBuffer.current, - }).then((payload) => { - capabilities.current = payload.capabilities; - }); + const payload = await workerApi.init(interruptBuffer.current); + capabilities.current = payload.capabilities; }, [lang, mutex]); const [doInit, setDoInit] = useState(false); const init = useCallback(() => setDoInit(true), []); + // Helper function to wrap worker API calls and track pending promises + // This ensures promises are rejected when the worker is terminated + const trackPromise = useCallback((promise: Promise): Promise => { + return new Promise((resolve, reject) => { + // Store the reject function + pendingPromises.current.add(reject); + + promise.then(resolve, reject).finally(() => { + // Remove reject function after promise settles + pendingPromises.current.delete(reject); + }); + }); + }, []); + // Initialization effect useEffect(() => { if (doInit) { @@ -175,21 +140,24 @@ export function WorkerProvider({ } break; case "restart": { - // Reject all pending promises - const error = "Worker interrupted"; - messageCallbacks.current.forEach(([, reject]) => reject(error)); - messageCallbacks.current.clear(); + // Reject all pending promises by calling their reject handlers + const error = new Error("Worker interrupted"); + pendingPromises.current.forEach((reject) => reject(error)); + pendingPromises.current.clear(); + // Terminate the worker workerRef.current?.terminate(); workerRef.current = null; + workerApiRef.current = null; setReady(false); void mutex.runExclusive(async () => { await initializeWorker(); - if (commandHistory.current.length > 0) { - await postMessage("restoreState", { - commands: commandHistory.current, - }); + if ( + commandHistory.current.length > 0 && + workerApiRef.current !== null + ) { + await workerApiRef.current.restoreState(commandHistory.current); } setReady(true); }); @@ -206,7 +174,7 @@ export function WorkerProvider({ if (!mutex.isLocked()) { throw new Error(`mutex of context must be locked for runCommand`); } - if (!workerRef.current || !ready) { + if (!workerApiRef.current || !ready) { return [ { type: "error", @@ -223,7 +191,9 @@ export function WorkerProvider({ } try { - const { output, updatedFiles } = await postMessage("runCode", { code }); + const { output, updatedFiles } = await trackPromise( + workerApiRef.current.runCode(code) + ); writeFile(updatedFiles); @@ -243,18 +213,18 @@ export function WorkerProvider({ return [{ type: "error", message: String(error) }]; } }, - [ready, writeFile, mutex] + [ready, writeFile, mutex, trackPromise] ); const checkSyntax = useCallback( async (code: string): Promise => { - if (!workerRef.current || !ready) return "invalid"; + if (!workerApiRef.current || !ready) return "invalid"; const { status } = await mutex.runExclusive(() => - postMessage("checkSyntax", { code }) + trackPromise(workerApiRef.current!.checkSyntax(code)) ); return status; }, - [ready, mutex] + [ready, mutex, trackPromise] ); const runFiles = useCallback( @@ -270,7 +240,7 @@ export function WorkerProvider({ }, ]; } - if (!workerRef.current || !ready) { + if (!workerApiRef.current || !ready) { return [ { type: "error", @@ -285,15 +255,14 @@ export function WorkerProvider({ interruptBuffer.current[0] = 0; } return mutex.runExclusive(async () => { - const { output, updatedFiles } = await postMessage("runFile", { - name: filenames[0], - files, - }); + const { output, updatedFiles } = await trackPromise( + workerApiRef.current!.runFile(filenames[0], files) + ); writeFile(updatedFiles); return output; }); }, - [ready, writeFile, mutex] + [ready, writeFile, mutex, trackPromise] ); return ( diff --git a/package-lock.json b/package-lock.json index 9f84d36..94e3088 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "chai": "^6.2.0", "chalk": "^5.5.0", "clsx": "^2.1.1", + "comlink": "^4.4.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "mocha": "^11.7.4", @@ -13767,6 +13768,12 @@ "node": ">= 0.8" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", diff --git a/package.json b/package.json index 71a9bc3..383b17c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "chai": "^6.2.0", "chalk": "^5.5.0", "clsx": "^2.1.1", + "comlink": "^4.4.2", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "mocha": "^11.7.4",