diff --git a/app/terminal/README.md b/app/terminal/README.md index ec89b6e..7f8f40c 100644 --- a/app/terminal/README.md +++ b/app/terminal/README.md @@ -10,6 +10,11 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ ### 共通 +* init?: `() => void` + * useRuntime() 内のuseEffectなどで呼び出されます。ランタイムを使う側では通常呼び出す必要はないです。 + * ランタイムの初期化にコストがかかるものは、init()で初期化フラグがトリガーされたときだけ初期化するようにします。 + * useRuntime() が複数回使われた場合はinitも複数回呼ばれます。 + * init()はフラグを立てるだけにし、完了する前にreturnしてよいです。初期化とcleanupはuseEffect()で非同期に行うのがよいと思います。 * ready: `boolean` * ランタイムの初期化が完了したか、不要である場合true * mutex?: `MutexInterface` diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index 4f57ac9..e2fb8fc 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -208,6 +208,7 @@ function MochaTest() { mocha.setup("bdd"); for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) { + runtimeRef.current[lang].init?.(); defineTests(lang, runtimeRef, filesRef); } diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 8789dcf..4d2d0a0 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -1,8 +1,10 @@ +"use client"; + import { MutexInterface } from "async-mutex"; import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl"; import { useWandbox, WandboxProvider } from "./wandbox/runtime"; import { AceLang } from "./editor"; -import { ReactNode } from "react"; +import { ReactNode, useEffect } from "react"; import { PyodideContext, usePyodide } from "./worker/pyodide"; import { RubyContext, useRuby } from "./worker/ruby"; import { JSEvalContext, useJSEval } from "./worker/jsEval"; @@ -17,6 +19,7 @@ import { MarkdownLang } from "@/[docs_id]/styledSyntaxHighlighter"; * */ export interface RuntimeContext { + init?: () => void; ready: boolean; mutex?: MutexInterface; interrupt?: () => void; @@ -87,21 +90,32 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); + let runtime: RuntimeContext; switch (language) { case "python": - return pyodide; + runtime = pyodide; + break; case "ruby": - return ruby; + runtime = ruby; + break; case "javascript": - return jsEval; + runtime = jsEval; + break; case "typescript": - return typescript; + runtime = typescript; + break; case "cpp": - return wandboxCpp; + runtime = wandboxCpp; + break; default: language satisfies never; throw new Error(`Runtime not implemented for language: ${language}`); } + const { init } = runtime; + useEffect(() => { + init?.(); + }, [init]); + return runtime; } export function RuntimeProvider({ children }: { children: ReactNode }) { return ( diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index 917da82..5f87f5c 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -16,17 +16,19 @@ import { RuntimeContext } from "../runtime"; export const compilerOptions: CompilerOptions = {}; -const TypeScriptContext = createContext( - null -); +const TypeScriptContext = createContext<{ + init: () => void; + tsEnv: VirtualTypeScriptEnvironment | null; +}>({ init: () => undefined, tsEnv: null }); export function TypeScriptProvider({ children }: { children: ReactNode }) { const [tsEnv, setTSEnv] = useState(null); - + const [doInit, setDoInit] = useState(false); + const init = useCallback(() => setDoInit(true), []); useEffect(() => { // useEffectはサーバーサイドでは実行されないが、 // typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を // サーバーサイドでのインポート対象とみなしてしまう。 - if (tsEnv === null && typeof window !== "undefined") { + if (doInit && tsEnv === null && typeof window !== "undefined") { const abortController = new AbortController(); (async () => { const ts = await import("typescript"); @@ -67,16 +69,16 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { abortController.abort(); }; } - }, [tsEnv, setTSEnv]); + }, [tsEnv, setTSEnv, doInit]); return ( - + {children} ); } export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { - const tsEnv = useContext(TypeScriptContext); + const { init, tsEnv } = useContext(TypeScriptContext); const { writeFile } = useEmbedContext(); const runFiles = useCallback( @@ -139,6 +141,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext { [tsEnv, writeFile, jsEval] ); return { + init, ready: tsEnv !== null, runFiles, getCommandlineStr, diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index 3ecd2a4..b8c18cd 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -5,6 +5,7 @@ import { ReactNode, useCallback, useEffect, + useMemo, useRef, useState, } from "react"; @@ -69,7 +70,7 @@ export function WorkerProvider({ }) { const workerRef = useRef(null); const [ready, setReady] = useState(false); - const mutex = useRef(new Mutex()); + const mutex = useMemo(() => new Mutex(), []); const { writeFile } = useEmbedContext(); const messageCallbacks = useRef< @@ -96,6 +97,13 @@ export function WorkerProvider({ } const initializeWorker = useCallback(async () => { + if (!mutex.isLocked()) { + throw new Error(`mutex of context must be locked for initializeWorker`); + } + if (workerRef.current) { + return; + } + let worker: Worker; lang satisfies RuntimeLang; switch (lang) { @@ -135,15 +143,27 @@ export function WorkerProvider({ }).then((payload) => { capabilities.current = payload.capabilities; }); - }, [lang]); + }, [lang, mutex]); + + const [doInit, setDoInit] = useState(false); + const init = useCallback(() => setDoInit(true), []); // Initialization effect useEffect(() => { - initializeWorker().then(() => setReady(true)); - return () => { - workerRef.current?.terminate(); - }; - }, [initializeWorker]); + if (doInit) { + void mutex.runExclusive(async () => { + await initializeWorker(); + setReady(true); + }); + return () => { + void mutex.runExclusive(async () => { + workerRef.current?.terminate(); + workerRef.current = null; + setReady(false); + }); + }; + } + }, [doInit, initializeWorker, mutex]); const interrupt = useCallback(() => { if (!capabilities.current) return; @@ -161,9 +181,10 @@ export function WorkerProvider({ messageCallbacks.current.clear(); workerRef.current?.terminate(); + workerRef.current = null; setReady(false); - void mutex.current.runExclusive(async () => { + void mutex.runExclusive(async () => { await initializeWorker(); if (commandHistory.current.length > 0) { await postMessage("restoreState", { @@ -178,11 +199,11 @@ export function WorkerProvider({ capabilities.current?.interrupt satisfies never; break; } - }, [initializeWorker]); + }, [initializeWorker, mutex]); const runCommand = useCallback( async (code: string): Promise => { - if (!mutex.current.isLocked()) { + if (!mutex.isLocked()) { throw new Error(`mutex of context must be locked for runCommand`); } if (!workerRef.current || !ready) { @@ -222,18 +243,18 @@ export function WorkerProvider({ return [{ type: "error", message: String(error) }]; } }, - [ready, writeFile] + [ready, writeFile, mutex] ); const checkSyntax = useCallback( async (code: string): Promise => { if (!workerRef.current || !ready) return "invalid"; - const { status } = await mutex.current.runExclusive(() => + const { status } = await mutex.runExclusive(() => postMessage("checkSyntax", { code }) ); return status; }, - [ready] + [ready, mutex] ); const runFiles = useCallback( @@ -263,7 +284,7 @@ export function WorkerProvider({ ) { interruptBuffer.current[0] = 0; } - return mutex.current.runExclusive(async () => { + return mutex.runExclusive(async () => { const { output, updatedFiles } = await postMessage("runFile", { name: filenames[0], files, @@ -272,16 +293,17 @@ export function WorkerProvider({ return output; }); }, - [ready, writeFile] + [ready, writeFile, mutex] ); return (