Skip to content

Commit a9d8b92

Browse files
committed
runtimeの初期化はeffectで行い、cleanupもする。initはそれをトリガーするだけ
1 parent 7cd60b2 commit a9d8b92

File tree

3 files changed

+55
-33
lines changed

3 files changed

+55
-33
lines changed

app/terminal/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
1111
### 共通
1212

1313
* init?: `() => void`
14-
* useRuntime() 内のuseEffectで呼び出されます
15-
* ランタイムの初期化にコストがかかるものは、init()されたときにだけ初期化するようにします
14+
* useRuntime() 内のuseEffectなどで呼び出されます。ランタイムを使う側では通常呼び出す必要はないです
15+
* ランタイムの初期化にコストがかかるものは、init()で初期化フラグがトリガーされたときだけ初期化するようにします
1616
* useRuntime() が複数回使われた場合はinitも複数回呼ばれます。
17-
* 初期化は非同期に行い、完了する前にreturnしてよいです。
17+
* init()はフラグを立てるだけにし、完了する前にreturnしてよいです。初期化とcleanupはuseEffect()で非同期に行うのがよいと思います
1818
* ready: `boolean`
1919
* ランタイムの初期化が完了したか、不要である場合true
2020
* mutex?: `MutexInterface`

app/terminal/typescript/runtime.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ReactNode,
88
useCallback,
99
useContext,
10-
useRef,
10+
useEffect,
1111
useState,
1212
} from "react";
1313
import { useEmbedContext } from "../embedContext";
@@ -22,15 +22,14 @@ const TypeScriptContext = createContext<{
2222
}>({ init: () => undefined, tsEnv: null });
2323
export function TypeScriptProvider({ children }: { children: ReactNode }) {
2424
const [tsEnv, setTSEnv] = useState<VirtualTypeScriptEnvironment | null>(null);
25-
const initializing = useRef(false);
26-
const init = useCallback(() => {
27-
if (initializing.current) {
28-
return;
29-
}
30-
initializing.current = true;
25+
const [doInit, setDoInit] = useState(false);
26+
const init = useCallback(() => setDoInit(true), []);
27+
useEffect(() => {
28+
// useEffectはサーバーサイドでは実行されないが、
3129
// typeof window !== "undefined" でガードしないとなぜかesbuildが"typescript"を
3230
// サーバーサイドでのインポート対象とみなしてしまう。
33-
if (tsEnv === null && typeof window !== "undefined") {
31+
if (doInit && tsEnv === null && typeof window !== "undefined") {
32+
const abortController = new AbortController();
3433
(async () => {
3534
const ts = await import("typescript");
3635
const vfs = await import("@typescript/vfs");
@@ -42,7 +41,8 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
4241
const libFileContents = await Promise.all(
4342
libFiles.map(async (libFile) => {
4443
const response = await fetch(
45-
`/typescript/${ts.version}/${libFile}`
44+
`/typescript/${ts.version}/${libFile}`,
45+
{ signal: abortController.signal }
4646
);
4747
if (response.ok) {
4848
return response.text();
@@ -65,8 +65,11 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
6565
);
6666
setTSEnv(env);
6767
})();
68+
return () => {
69+
abortController.abort();
70+
};
6871
}
69-
}, [tsEnv, setTSEnv]);
72+
}, [tsEnv, setTSEnv, doInit]);
7073
return (
7174
<TypeScriptContext.Provider value={{ init, tsEnv }}>
7275
{children}

app/terminal/worker/runtime.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"use client";
22

3-
import { Context, ReactNode, useCallback, useRef, useState } from "react";
3+
import {
4+
Context,
5+
ReactNode,
6+
useCallback,
7+
useEffect,
8+
useMemo,
9+
useRef,
10+
useState,
11+
} from "react";
412
import { RuntimeContext, RuntimeLang } from "../runtime";
513
import { ReplOutput, SyntaxStatus } from "../repl";
614
import { Mutex, MutexInterface } from "async-mutex";
@@ -62,7 +70,7 @@ export function WorkerProvider({
6270
}) {
6371
const workerRef = useRef<Worker | null>(null);
6472
const [ready, setReady] = useState<boolean>(false);
65-
const mutex = useRef<MutexInterface>(new Mutex());
73+
const mutex = useMemo<MutexInterface>(() => new Mutex(), []);
6674
const { writeFile } = useEmbedContext();
6775

6876
const messageCallbacks = useRef<
@@ -89,7 +97,7 @@ export function WorkerProvider({
8997
}
9098

9199
const initializeWorker = useCallback(async () => {
92-
if (!mutex.current.isLocked()) {
100+
if (!mutex.isLocked()) {
93101
throw new Error(`mutex of context must be locked for initializeWorker`);
94102
}
95103
if (workerRef.current) {
@@ -135,15 +143,26 @@ export function WorkerProvider({
135143
}).then((payload) => {
136144
capabilities.current = payload.capabilities;
137145
});
138-
}, [lang]);
146+
}, [lang, mutex]);
139147

140-
// First initialization
141-
const init = useCallback(() => {
142-
// すでに初期化済みだった場合initializeWorker()がreturnしなにもしない
143-
void mutex.current.runExclusive(() =>
144-
initializeWorker().then(() => setReady(true))
145-
);
146-
}, [initializeWorker]);
148+
const [doInit, setDoInit] = useState(false);
149+
const init = useCallback(() => setDoInit(true), []);
150+
151+
// Initialization effect
152+
useEffect(() => {
153+
if (doInit) {
154+
void mutex.runExclusive(async () => {
155+
await initializeWorker();
156+
setReady(true);
157+
});
158+
return () => {
159+
void mutex.runExclusive(async () => {
160+
workerRef.current?.terminate();
161+
workerRef.current = null;
162+
});
163+
};
164+
}
165+
}, [doInit, initializeWorker, mutex]);
147166

148167
const interrupt = useCallback(() => {
149168
if (!capabilities.current) return;
@@ -164,7 +183,7 @@ export function WorkerProvider({
164183
workerRef.current = null;
165184
setReady(false);
166185

167-
void mutex.current.runExclusive(async () => {
186+
void mutex.runExclusive(async () => {
168187
await initializeWorker();
169188
if (commandHistory.current.length > 0) {
170189
await postMessage("restoreState", {
@@ -179,11 +198,11 @@ export function WorkerProvider({
179198
capabilities.current?.interrupt satisfies never;
180199
break;
181200
}
182-
}, [initializeWorker]);
201+
}, [initializeWorker, mutex]);
183202

184203
const runCommand = useCallback(
185204
async (code: string): Promise<ReplOutput[]> => {
186-
if (!mutex.current.isLocked()) {
205+
if (!mutex.isLocked()) {
187206
throw new Error(`mutex of context must be locked for runCommand`);
188207
}
189208
if (!workerRef.current || !ready) {
@@ -223,18 +242,18 @@ export function WorkerProvider({
223242
return [{ type: "error", message: String(error) }];
224243
}
225244
},
226-
[ready, writeFile]
245+
[ready, writeFile, mutex]
227246
);
228247

229248
const checkSyntax = useCallback(
230249
async (code: string): Promise<SyntaxStatus> => {
231250
if (!workerRef.current || !ready) return "invalid";
232-
const { status } = await mutex.current.runExclusive(() =>
251+
const { status } = await mutex.runExclusive(() =>
233252
postMessage("checkSyntax", { code })
234253
);
235254
return status;
236255
},
237-
[ready]
256+
[ready, mutex]
238257
);
239258

240259
const runFiles = useCallback(
@@ -264,7 +283,7 @@ export function WorkerProvider({
264283
) {
265284
interruptBuffer.current[0] = 0;
266285
}
267-
return mutex.current.runExclusive(async () => {
286+
return mutex.runExclusive(async () => {
268287
const { output, updatedFiles } = await postMessage("runFile", {
269288
name: filenames[0],
270289
files,
@@ -273,7 +292,7 @@ export function WorkerProvider({
273292
return output;
274293
});
275294
},
276-
[ready, writeFile]
295+
[ready, writeFile, mutex]
277296
);
278297

279298
return (
@@ -283,7 +302,7 @@ export function WorkerProvider({
283302
ready,
284303
runCommand,
285304
checkSyntax,
286-
mutex: mutex.current,
305+
mutex,
287306
runFiles,
288307
interrupt,
289308
}}

0 commit comments

Comments
 (0)