Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ

### 共通

* init?: `() => void`
* useRuntime() 内のuseEffectなどで呼び出されます。ランタイムを使う側では通常呼び出す必要はないです。
* ランタイムの初期化にコストがかかるものは、init()で初期化フラグがトリガーされたときだけ初期化するようにします。
* useRuntime() が複数回使われた場合はinitも複数回呼ばれます。
* init()はフラグを立てるだけにし、完了する前にreturnしてよいです。初期化とcleanupはuseEffect()で非同期に行うのがよいと思います。
* ready: `boolean`
* ランタイムの初期化が完了したか、不要である場合true
* mutex?: `MutexInterface`
Expand Down
1 change: 1 addition & 0 deletions app/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
26 changes: 20 additions & 6 deletions app/terminal/runtime.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -17,6 +19,7 @@ import { MarkdownLang } from "@/[docs_id]/styledSyntaxHighlighter";
*
*/
export interface RuntimeContext {
init?: () => void;
ready: boolean;
mutex?: MutexInterface;
interrupt?: () => void;
Expand Down Expand Up @@ -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]);
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The useEffect has init in its dependency array, but init is a callback that may change on every render depending on its dependencies. This could cause unnecessary re-initialization. Since the README documentation states that "useRuntime() が複数回使われた場合はinitも複数回呼ばれます", this behavior might be intentional, but consider whether this effect should only run once by using an empty dependency array instead.

Suggested change
}, [init]);
}, [language]);

Copilot uses AI. Check for mistakes.
return runtime;
}
export function RuntimeProvider({ children }: { children: ReactNode }) {
return (
Expand Down
19 changes: 11 additions & 8 deletions app/terminal/typescript/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ import { RuntimeContext } from "../runtime";

export const compilerOptions: CompilerOptions = {};

const TypeScriptContext = createContext<VirtualTypeScriptEnvironment | null>(
null
);
const TypeScriptContext = createContext<{
init: () => void;
tsEnv: VirtualTypeScriptEnvironment | null;
}>({ init: () => undefined, tsEnv: null });
export function TypeScriptProvider({ children }: { children: ReactNode }) {
const [tsEnv, setTSEnv] = useState<VirtualTypeScriptEnvironment | null>(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");
Expand Down Expand Up @@ -67,16 +69,16 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
abortController.abort();
};
}
}, [tsEnv, setTSEnv]);
}, [tsEnv, setTSEnv, doInit]);
return (
<TypeScriptContext.Provider value={tsEnv}>
<TypeScriptContext.Provider value={{ init, tsEnv }}>
{children}
</TypeScriptContext.Provider>
);
}

export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
const tsEnv = useContext(TypeScriptContext);
const { init, tsEnv } = useContext(TypeScriptContext);

const { writeFile } = useEmbedContext();
const runFiles = useCallback(
Expand Down Expand Up @@ -139,6 +141,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
[tsEnv, writeFile, jsEval]
);
return {
init,
ready: tsEnv !== null,
runFiles,
getCommandlineStr,
Expand Down
54 changes: 38 additions & 16 deletions app/terminal/worker/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -69,7 +70,7 @@ export function WorkerProvider({
}) {
const workerRef = useRef<Worker | null>(null);
const [ready, setReady] = useState<boolean>(false);
const mutex = useRef<MutexInterface>(new Mutex());
const mutex = useMemo<MutexInterface>(() => new Mutex(), []);
const { writeFile } = useEmbedContext();

const messageCallbacks = useRef<
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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", {
Expand All @@ -178,11 +199,11 @@ export function WorkerProvider({
capabilities.current?.interrupt satisfies never;
break;
}
}, [initializeWorker]);
}, [initializeWorker, mutex]);

const runCommand = useCallback(
async (code: string): Promise<ReplOutput[]> => {
if (!mutex.current.isLocked()) {
if (!mutex.isLocked()) {
throw new Error(`mutex of context must be locked for runCommand`);
}
if (!workerRef.current || !ready) {
Expand Down Expand Up @@ -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<SyntaxStatus> => {
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(
Expand Down Expand Up @@ -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,
Expand All @@ -272,16 +293,17 @@ export function WorkerProvider({
return output;
});
},
[ready, writeFile]
[ready, writeFile, mutex]
);

return (
<context.Provider
value={{
init,
ready,
runCommand,
checkSyntax,
mutex: mutex.current,
mutex,
runFiles,
interrupt,
}}
Expand Down