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 ;
0 commit comments