From 1496dab4c05d09fed7994ad33f7c985ec8ab5782 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 18 Nov 2025 01:27:11 +0900 Subject: [PATCH 1/4] =?UTF-8?q?runtime=E3=81=ABinit()=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81=E5=BF=85=E8=A6=81=E3=81=AA=E6=99=82?= =?UTF-8?q?=E3=81=A0=E3=81=91=E5=88=9D=E6=9C=9F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/README.md | 5 +++++ app/terminal/page.tsx | 1 + app/terminal/runtime.tsx | 31 ++++++++++++++++++++++------- app/terminal/typescript/runtime.tsx | 29 ++++++++++++++------------- app/terminal/worker/runtime.tsx | 21 +++++++++++++------ 5 files changed, 60 insertions(+), 27 deletions(-) diff --git a/app/terminal/README.md b/app/terminal/README.md index ec89b6e..0e5c288 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も複数回呼ばれます。 + * 初期化は非同期に行い、完了する前にreturnしてよいです。 * ready: `boolean` * ランタイムの初期化が完了したか、不要である場合true * mutex?: `MutexInterface` diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index 3125790..55946e8 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -163,6 +163,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 2778ca6..93d7b17 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"; @@ -16,6 +18,7 @@ import { TypeScriptProvider, useTypeScript } from "./typescript/runtime"; * */ export interface RuntimeContext { + init?: () => void; ready: boolean; mutex?: MutexInterface; interrupt?: () => void; @@ -24,7 +27,10 @@ export interface RuntimeContext { checkSyntax?: (code: string) => Promise; splitReplExamples?: (content: string) => ReplCommand[]; // file - runFiles: (filenames: string[], files: Readonly>) => Promise; + runFiles: ( + filenames: string[], + files: Readonly> + ) => Promise; getCommandlineStr?: (filenames: string[]) => string; } export interface LangConstants { @@ -73,21 +79,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..54c0414 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -8,6 +8,7 @@ import { useCallback, useContext, useEffect, + useRef, useState, } from "react"; import { useEmbedContext } from "../embedContext"; @@ -16,18 +17,21 @@ 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); - - useEffect(() => { - // useEffectはサーバーサイドでは実行されないが、 + const initializing = useRef(false); + const init = useCallback(() => { + if (initializing.current) { + return; + } + initializing.current = true; // typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を // サーバーサイドでのインポート対象とみなしてしまう。 if (tsEnv === null && typeof window !== "undefined") { - const abortController = new AbortController(); (async () => { const ts = await import("typescript"); const vfs = await import("@typescript/vfs"); @@ -39,8 +43,7 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { const libFileContents = await Promise.all( libFiles.map(async (libFile) => { const response = await fetch( - `/typescript/${ts.version}/${libFile}`, - { signal: abortController.signal } + `/typescript/${ts.version}/${libFile}` ); if (response.ok) { return response.text(); @@ -63,20 +66,17 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { ); setTSEnv(env); })(); - return () => { - abortController.abort(); - }; } }, [tsEnv, setTSEnv]); 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 +139,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..7582b1a 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -96,6 +96,13 @@ export function WorkerProvider({ } const initializeWorker = useCallback(async () => { + if (!mutex.current.isLocked()) { + throw new Error(`mutex of context must be locked for initializeWorker`); + } + if (workerRef.current) { + return; + } + let worker: Worker; lang satisfies RuntimeLang; switch (lang) { @@ -137,12 +144,12 @@ export function WorkerProvider({ }); }, [lang]); - // Initialization effect - useEffect(() => { - initializeWorker().then(() => setReady(true)); - return () => { - workerRef.current?.terminate(); - }; + // First initialization + const init = useCallback(() => { + // すでに初期化済みだった場合initializeWorker()がreturnしなにもしない + void mutex.current.runExclusive(() => + initializeWorker().then(() => setReady(true)) + ); }, [initializeWorker]); const interrupt = useCallback(() => { @@ -161,6 +168,7 @@ export function WorkerProvider({ messageCallbacks.current.clear(); workerRef.current?.terminate(); + workerRef.current = null; setReady(false); void mutex.current.runExclusive(async () => { @@ -278,6 +286,7 @@ export function WorkerProvider({ return ( Date: Tue, 18 Nov 2025 16:38:37 +0900 Subject: [PATCH 2/4] =?UTF-8?q?lint=E3=82=A8=E3=83=A9=E3=83=BC=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/typescript/runtime.tsx | 1 - app/terminal/worker/runtime.tsx | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index 54c0414..eb247a0 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -7,7 +7,6 @@ import { ReactNode, useCallback, useContext, - useEffect, useRef, useState, } from "react"; diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index 7582b1a..8a2b745 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -1,13 +1,6 @@ "use client"; -import { - Context, - ReactNode, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { Context, ReactNode, useCallback, useRef, useState } from "react"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput, SyntaxStatus } from "../repl"; import { Mutex, MutexInterface } from "async-mutex"; From a9d8b923203c08618c552c4886565081bb1d13f8 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:12:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?runtime=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96?= =?UTF-8?q?=E3=81=AFeffect=E3=81=A7=E8=A1=8C=E3=81=84=E3=80=81cleanup?= =?UTF-8?q?=E3=82=82=E3=81=99=E3=82=8B=E3=80=82init=E3=81=AF=E3=81=9D?= =?UTF-8?q?=E3=82=8C=E3=82=92=E3=83=88=E3=83=AA=E3=82=AC=E3=83=BC=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=A0=E3=81=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/README.md | 6 +-- app/terminal/typescript/runtime.tsx | 23 ++++++----- app/terminal/worker/runtime.tsx | 59 +++++++++++++++++++---------- 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/app/terminal/README.md b/app/terminal/README.md index 0e5c288..7f8f40c 100644 --- a/app/terminal/README.md +++ b/app/terminal/README.md @@ -11,10 +11,10 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ ### 共通 * init?: `() => void` - * useRuntime() 内のuseEffectで呼び出されます。 - * ランタイムの初期化にコストがかかるものは、init()されたときにだけ初期化するようにします。 + * useRuntime() 内のuseEffectなどで呼び出されます。ランタイムを使う側では通常呼び出す必要はないです。 + * ランタイムの初期化にコストがかかるものは、init()で初期化フラグがトリガーされたときだけ初期化するようにします。 * useRuntime() が複数回使われた場合はinitも複数回呼ばれます。 - * 初期化は非同期に行い、完了する前にreturnしてよいです。 + * init()はフラグを立てるだけにし、完了する前にreturnしてよいです。初期化とcleanupはuseEffect()で非同期に行うのがよいと思います。 * ready: `boolean` * ランタイムの初期化が完了したか、不要である場合true * mutex?: `MutexInterface` diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx index eb247a0..5f87f5c 100644 --- a/app/terminal/typescript/runtime.tsx +++ b/app/terminal/typescript/runtime.tsx @@ -7,7 +7,7 @@ import { ReactNode, useCallback, useContext, - useRef, + useEffect, useState, } from "react"; import { useEmbedContext } from "../embedContext"; @@ -22,15 +22,14 @@ const TypeScriptContext = createContext<{ }>({ init: () => undefined, tsEnv: null }); export function TypeScriptProvider({ children }: { children: ReactNode }) { const [tsEnv, setTSEnv] = useState(null); - const initializing = useRef(false); - const init = useCallback(() => { - if (initializing.current) { - return; - } - initializing.current = true; + 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"); const vfs = await import("@typescript/vfs"); @@ -42,7 +41,8 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { const libFileContents = await Promise.all( libFiles.map(async (libFile) => { const response = await fetch( - `/typescript/${ts.version}/${libFile}` + `/typescript/${ts.version}/${libFile}`, + { signal: abortController.signal } ); if (response.ok) { return response.text(); @@ -65,8 +65,11 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) { ); setTSEnv(env); })(); + return () => { + abortController.abort(); + }; } - }, [tsEnv, setTSEnv]); + }, [tsEnv, setTSEnv, doInit]); return ( {children} diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index 8a2b745..dd1ac11 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -1,6 +1,14 @@ "use client"; -import { Context, ReactNode, useCallback, useRef, useState } from "react"; +import { + Context, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput, SyntaxStatus } from "../repl"; import { Mutex, MutexInterface } from "async-mutex"; @@ -62,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< @@ -89,7 +97,7 @@ export function WorkerProvider({ } const initializeWorker = useCallback(async () => { - if (!mutex.current.isLocked()) { + if (!mutex.isLocked()) { throw new Error(`mutex of context must be locked for initializeWorker`); } if (workerRef.current) { @@ -135,15 +143,26 @@ export function WorkerProvider({ }).then((payload) => { capabilities.current = payload.capabilities; }); - }, [lang]); + }, [lang, mutex]); - // First initialization - const init = useCallback(() => { - // すでに初期化済みだった場合initializeWorker()がreturnしなにもしない - void mutex.current.runExclusive(() => - initializeWorker().then(() => setReady(true)) - ); - }, [initializeWorker]); + const [doInit, setDoInit] = useState(false); + const init = useCallback(() => setDoInit(true), []); + + // Initialization effect + useEffect(() => { + if (doInit) { + void mutex.runExclusive(async () => { + await initializeWorker(); + setReady(true); + }); + return () => { + void mutex.runExclusive(async () => { + workerRef.current?.terminate(); + workerRef.current = null; + }); + }; + } + }, [doInit, initializeWorker, mutex]); const interrupt = useCallback(() => { if (!capabilities.current) return; @@ -164,7 +183,7 @@ export function WorkerProvider({ workerRef.current = null; setReady(false); - void mutex.current.runExclusive(async () => { + void mutex.runExclusive(async () => { await initializeWorker(); if (commandHistory.current.length > 0) { await postMessage("restoreState", { @@ -179,11 +198,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) { @@ -223,18 +242,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( @@ -264,7 +283,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, @@ -273,7 +292,7 @@ export function WorkerProvider({ return output; }); }, - [ready, writeFile] + [ready, writeFile, mutex] ); return ( @@ -283,7 +302,7 @@ export function WorkerProvider({ ready, runCommand, checkSyntax, - mutex: mutex.current, + mutex, runFiles, interrupt, }} From 7ab2c46c75d7a969e3fd81608c505863101ee221 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:42:49 +0900 Subject: [PATCH 4/4] setReady(false) --- app/terminal/worker/runtime.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index dd1ac11..b8c18cd 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -159,6 +159,7 @@ export function WorkerProvider({ void mutex.runExclusive(async () => { workerRef.current?.terminate(); workerRef.current = null; + setReady(false); }); }; }