Skip to content

Commit 23cccbd

Browse files
committed
workerを使うruntime各種をリファクタ
1 parent 2f11ed3 commit 23cccbd

File tree

8 files changed

+431
-713
lines changed

8 files changed

+431
-713
lines changed
Lines changed: 18 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,238 +1,35 @@
1-
"use client";
1+
'use client';
22

3-
import {
4-
useState,
5-
useRef,
6-
useCallback,
7-
ReactNode,
8-
createContext,
9-
useContext,
10-
useEffect,
11-
} from "react";
12-
import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl";
13-
import { Mutex, MutexInterface } from "async-mutex";
14-
import { RuntimeContext } from "../runtime";
3+
import { ReplCommand, ReplOutput } from '../repl';
4+
import { createWorkerRuntime } from '../worker-runtime';
155

16-
const JavaScriptContext = createContext<RuntimeContext>(null!);
17-
18-
export function useJavaScript(): RuntimeContext {
19-
const context = useContext(JavaScriptContext);
20-
if (!context) {
21-
throw new Error("useJavaScript must be used within a JavaScriptProvider");
22-
}
23-
return context;
24-
}
25-
26-
type MessageToWorker =
27-
| {
28-
type: "init";
29-
payload?: undefined;
30-
}
31-
| {
32-
type: "runJavaScript";
33-
payload: { code: string };
34-
}
35-
| {
36-
type: "checkSyntax";
37-
payload: { code: string };
38-
}
39-
| {
40-
type: "restoreState";
41-
payload: { commands: string[] };
42-
};
43-
44-
type MessageFromWorker =
45-
| { id: number; payload: unknown }
46-
| { id: number; error: string };
47-
48-
type InitPayloadFromWorker = { success: boolean };
49-
type RunPayloadFromWorker = {
50-
output: ReplOutput[];
51-
updatedFiles: [string, string][];
52-
};
53-
type StatusPayloadFromWorker = { status: SyntaxStatus };
54-
55-
export function JavaScriptProvider({ children }: { children: ReactNode }) {
56-
const workerRef = useRef<Worker | null>(null);
57-
const [ready, setReady] = useState<boolean>(false);
58-
const mutex = useRef<MutexInterface>(new Mutex());
59-
const messageCallbacks = useRef<
60-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
61-
Map<number, [(payload: any) => void, (error: string) => void]>
62-
>(new Map());
63-
const nextMessageId = useRef<number>(0);
64-
const executedCommands = useRef<string[]>([]);
65-
66-
function postMessage<T>({ type, payload }: MessageToWorker) {
67-
const id = nextMessageId.current++;
68-
return new Promise<T>((resolve, reject) => {
69-
messageCallbacks.current.set(id, [resolve, reject]);
70-
workerRef.current?.postMessage({ id, type, payload });
71-
});
72-
}
73-
74-
const initializeWorker = useCallback(() => {
75-
const worker = new Worker("/javascript.worker.js");
76-
workerRef.current = worker;
77-
78-
worker.onmessage = (event) => {
79-
const data = event.data as MessageFromWorker;
80-
if (messageCallbacks.current.has(data.id)) {
81-
const [resolve, reject] = messageCallbacks.current.get(data.id)!;
82-
if ("error" in data) {
83-
reject(data.error);
84-
} else {
85-
resolve(data.payload);
86-
}
87-
messageCallbacks.current.delete(data.id);
88-
}
89-
};
90-
91-
return postMessage<InitPayloadFromWorker>({
92-
type: "init",
93-
}).then(({ success }) => {
94-
if (success) {
95-
setReady(true);
96-
}
97-
return worker;
98-
});
99-
}, []);
100-
101-
useEffect(() => {
102-
let worker: Worker | null = null;
103-
initializeWorker().then((w) => {
104-
worker = w;
105-
});
106-
107-
return () => {
108-
worker?.terminate();
109-
};
110-
}, [initializeWorker]);
111-
112-
const interrupt = useCallback(() => {
113-
// Since we can't interrupt JavaScript execution directly,
114-
// we terminate the worker and restart it, then restore state
115-
116-
// Reject all pending callbacks before terminating
117-
const error = "Worker interrupted";
118-
messageCallbacks.current.forEach(([, reject]) => reject(error));
119-
messageCallbacks.current.clear();
120-
121-
// Terminate the current worker
122-
workerRef.current?.terminate();
123-
124-
// Reset ready state
125-
setReady(false);
126-
127-
mutex.current.runExclusive(async () => {
128-
// Create a new worker and wait for it to be ready
129-
await initializeWorker();
130-
131-
// Restore state by re-executing previous commands
132-
if (executedCommands.current.length > 0) {
133-
await postMessage<{ success: boolean }>({
134-
type: "restoreState",
135-
payload: { commands: executedCommands.current },
136-
});
137-
}
138-
});
139-
}, [initializeWorker]);
140-
141-
const runCommand = useCallback(
142-
async (code: string): Promise<ReplOutput[]> => {
143-
if (!mutex.current.isLocked()) {
144-
throw new Error(
145-
"mutex of JavaScriptContext must be locked for runCommand"
146-
);
147-
}
148-
if (!workerRef.current || !ready) {
149-
return [{ type: "error", message: "JavaScript runtime is not ready yet." }];
150-
}
151-
152-
try {
153-
const { output } = await postMessage<RunPayloadFromWorker>({
154-
type: "runJavaScript",
155-
payload: { code },
156-
});
157-
// Save successfully executed command
158-
executedCommands.current.push(code);
159-
return output;
160-
} catch (error) {
161-
// Handle errors (including "Worker interrupted")
162-
if (error instanceof Error) {
163-
return [{ type: "error", message: error.message }];
164-
}
165-
return [{ type: "error", message: String(error) }];
166-
}
167-
},
168-
[ready]
169-
);
170-
171-
const checkSyntax = useCallback(
172-
async (code: string): Promise<SyntaxStatus> => {
173-
if (!workerRef.current || !ready) return "invalid";
174-
const { status } = await mutex.current.runExclusive(() =>
175-
postMessage<StatusPayloadFromWorker>({
176-
type: "checkSyntax",
177-
payload: { code },
178-
})
179-
);
180-
return status;
181-
},
182-
[ready]
183-
);
184-
185-
const runFiles = useCallback(
186-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
187-
async (_filenames: string[]): Promise<ReplOutput[]> => {
188-
return [
189-
{
190-
type: "error",
191-
message: "JavaScript file execution is not supported in this runtime",
192-
},
193-
];
194-
},
195-
[]
196-
);
197-
198-
const splitReplExamples = useCallback((content: string): ReplCommand[] => {
6+
const config = {
7+
languageName: 'JavaScript',
8+
providerName: 'JavaScriptProvider',
9+
workerUrl: '/javascript.worker.js',
10+
useFiles: false, // JavaScript runtime doesn't support file execution
11+
splitReplExamples: (content: string): ReplCommand[] => {
19912
const initCommands: { command: string; output: ReplOutput[] }[] = [];
200-
for (const line of content.split("\n")) {
201-
if (line.startsWith("> ")) {
13+
for (const line of content.split('\n')) {
14+
if (line.startsWith('> ')) {
20215
// Remove the prompt from the command
20316
initCommands.push({ command: line.slice(2), output: [] });
20417
} else {
20518
// Lines without prompt are output from the previous command
20619
if (initCommands.length > 0) {
20720
initCommands[initCommands.length - 1].output.push({
208-
type: "stdout",
21+
type: 'stdout',
20922
message: line,
21023
});
21124
}
21225
}
21326
}
21427
return initCommands;
215-
}, []);
28+
},
29+
getCommandlineStr: (filenames: string[]) => `node ${filenames[0]}`,
30+
};
21631

217-
const getCommandlineStr = useCallback(
218-
(filenames: string[]) => `node ${filenames[0]}`,
219-
[]
220-
);
32+
const { Provider, useRuntime } = createWorkerRuntime(config);
22133

222-
return (
223-
<JavaScriptContext.Provider
224-
value={{
225-
ready,
226-
runCommand,
227-
checkSyntax,
228-
mutex: mutex.current,
229-
runFiles,
230-
interrupt,
231-
splitReplExamples,
232-
getCommandlineStr,
233-
}}
234-
>
235-
{children}
236-
</JavaScriptContext.Provider>
237-
);
238-
}
34+
export const JavaScriptProvider = Provider;
35+
export const useJavaScript = useRuntime;

app/terminal/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ import { useWandbox } from "./wandbox/runtime";
99
import { RuntimeContext, RuntimeLang } from "./runtime";
1010
import { useEmbedContext } from "./embedContext";
1111
import { defineTests } from "./tests";
12+
import { useJavaScript } from "./javascript/runtime";
1213

1314
export default function RuntimeTestPage() {
1415
const pyodide = usePyodide();
1516
const ruby = useRuby();
17+
const javascript = useJavaScript();
1618
const wandboxCpp = useWandbox("cpp");
1719
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
1820
runtimeRef.current = {
1921
python: pyodide,
2022
ruby: ruby,
23+
javascript: javascript,
2124
cpp: wandboxCpp,
2225
};
2326
const { files, writeFile } = useEmbedContext();

0 commit comments

Comments
 (0)