11/**
22 * T-Ruby WASM Loader for Playground
33 *
4- * Handles lazy loading and initialization of Ruby WASM with T-Ruby compiler
4+ * Uses a sandboxed iframe to isolate WASM execution from browser extensions
5+ * that might interfere with FinalizationRegistry and other APIs.
56 */
67
7- // CDN URLs
8- const RUBY_WASM_CDN = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/browser/+esm' ;
9- const RUBY_WASM_BINARY = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/ruby+stdlib.wasm' ;
10- const T_RUBY_LIB_CDN = 'https://cdn.jsdelivr.net/npm/@t-ruby/wasm/dist/lib/' ;
11-
128// Types
139export interface CompileResult {
1410 success : boolean ;
@@ -18,7 +14,7 @@ export interface CompileResult {
1814}
1915
2016export interface TRubyCompiler {
21- compile ( code : string ) : CompileResult ;
17+ compile ( code : string ) : Promise < CompileResult > ;
2218 healthCheck ( ) : { loaded : boolean ; version : string ; ruby_version : string } ;
2319 getVersion ( ) : { t_ruby : string ; ruby : string } ;
2420}
@@ -32,68 +28,25 @@ export interface LoadingProgress {
3228}
3329
3430// Singleton state
35- let rubyVM : any = null ;
3631let compiler : TRubyCompiler | null = null ;
3732let loadingPromise : Promise < TRubyCompiler > | null = null ;
33+ let workerIframe : HTMLIFrameElement | null = null ;
34+ let healthData : { loaded : boolean ; version : string ; ruby_version : string } | null = null ;
35+
36+ // Pending compile requests
37+ const pendingRequests = new Map < string , {
38+ resolve : ( result : CompileResult ) => void ;
39+ reject : ( error : Error ) => void ;
40+ } > ( ) ;
3841
39- // Bootstrap code for T-Ruby compiler
40- const BOOTSTRAP_CODE = `
41- require "json"
42-
43- # Global compiler instance
44- $trb_compiler = nil
45-
46- def get_compiler
47- $trb_compiler ||= TRuby::Compiler.new
48- end
49-
50- def __trb_compile__(code)
51- compiler = get_compiler
52-
53- begin
54- result = compiler.compile_string(code)
55-
56- {
57- success: result[:errors].empty?,
58- ruby: result[:ruby] || "",
59- rbs: result[:rbs] || "",
60- errors: result[:errors] || []
61- }.to_json
62- rescue TRuby::ParseError => e
63- {
64- success: false,
65- ruby: "",
66- rbs: "",
67- errors: [e.message]
68- }.to_json
69- rescue StandardError => e
70- {
71- success: false,
72- ruby: "",
73- rbs: "",
74- errors: ["Compilation error: " + e.message]
75- }.to_json
76- end
77- end
78-
79- def __trb_health_check__
80- {
81- loaded: defined?(TRuby) == "constant",
82- version: defined?(TRuby::VERSION) ? TRuby::VERSION : "unknown",
83- ruby_version: RUBY_VERSION
84- }.to_json
85- end
86-
87- def __trb_version__
88- {
89- t_ruby: defined?(TRuby::VERSION) ? TRuby::VERSION : "unknown",
90- ruby: RUBY_VERSION
91- }.to_json
92- end
93- ` ;
42+ let requestIdCounter = 0 ;
43+
44+ function generateRequestId ( ) : string {
45+ return `req_${ ++ requestIdCounter } _${ Date . now ( ) } ` ;
46+ }
9447
9548/**
96- * Load the T-Ruby WASM compiler
49+ * Load the T-Ruby WASM compiler using sandboxed iframe
9750 * Returns cached instance if already loaded
9851 */
9952export async function loadTRubyCompiler (
@@ -125,101 +78,142 @@ export async function loadTRubyCompiler(
12578async function doLoadCompiler (
12679 onProgress ?: ( progress : LoadingProgress ) => void
12780) : Promise < TRubyCompiler > {
128- try {
129- // Step 1: Load Ruby WASM module
130- console . log ( '[T-Ruby] Step 1: Loading Ruby WASM module from' , RUBY_WASM_CDN ) ;
81+ return new Promise ( ( resolve , reject ) => {
82+ console . log ( '[T-Ruby] Creating sandboxed iframe for WASM execution...' ) ;
13183 onProgress ?.( {
13284 state : 'loading' ,
133- message : 'Loading Ruby runtime ...' ,
134- progress : 10
85+ message : 'Initializing compiler sandbox ...' ,
86+ progress : 5
13587 } ) ;
13688
137- const { DefaultRubyVM } = await import ( /* webpackIgnore: true */ RUBY_WASM_CDN ) ;
138- console . log ( '[T-Ruby] Step 1 complete: DefaultRubyVM loaded' ) ;
139-
140- onProgress ?.( {
141- state : 'loading' ,
142- message : 'Downloading Ruby WASM binary...' ,
143- progress : 30
144- } ) ;
145-
146- // Step 2: Fetch and compile WASM binary
147- console . log ( '[T-Ruby] Step 2: Fetching WASM binary from' , RUBY_WASM_BINARY ) ;
148- const response = await fetch ( RUBY_WASM_BINARY ) ;
149- console . log ( '[T-Ruby] Step 2: WASM fetch response status:' , response . status ) ;
150- const wasmModule = await WebAssembly . compileStreaming ( response ) ;
151- console . log ( '[T-Ruby] Step 2 complete: WASM module compiled' ) ;
152-
153- onProgress ?.( {
154- state : 'loading' ,
155- message : 'Initializing Ruby VM...' ,
156- progress : 60
157- } ) ;
158-
159- // Step 3: Initialize Ruby VM
160- console . log ( '[T-Ruby] Step 3: Initializing Ruby VM...' ) ;
161- const { vm } = await DefaultRubyVM ( wasmModule ) ;
162- rubyVM = vm ;
163- console . log ( '[T-Ruby] Step 3 complete: Ruby VM initialized' ) ;
164-
165- onProgress ?.( {
166- state : 'loading' ,
167- message : 'Loading T-Ruby compiler...' ,
168- progress : 80
169- } ) ;
170-
171- // Step 4: Load T-Ruby library
172- console . log ( '[T-Ruby] Step 4: Loading T-Ruby compiler bootstrap...' ) ;
173-
174- // Initialize the compiler bootstrap
175- vm . eval ( BOOTSTRAP_CODE ) ;
176- console . log ( '[T-Ruby] Step 4 complete: Bootstrap code evaluated' ) ;
89+ // Create sandboxed iframe
90+ const iframe = document . createElement ( 'iframe' ) ;
91+ iframe . style . display = 'none' ;
92+ iframe . sandbox . add ( 'allow-scripts' ) ;
93+ iframe . src = '/wasm-worker.html' ;
94+
95+ // Message handler
96+ const messageHandler = ( event : MessageEvent ) => {
97+ // Only accept messages from our iframe
98+ if ( event . source !== iframe . contentWindow ) return ;
99+
100+ const { type, data, requestId } = event . data || { } ;
101+ console . log ( '[T-Ruby] Received message from worker:' , type , data ) ;
102+
103+ switch ( type ) {
104+ case 'loaded' :
105+ console . log ( '[T-Ruby] Worker iframe loaded, sending init command...' ) ;
106+ iframe . contentWindow ?. postMessage ( { type : 'init' } , '*' ) ;
107+ break ;
108+
109+ case 'progress' :
110+ onProgress ?.( {
111+ state : 'loading' ,
112+ message : data . message ,
113+ progress : data . progress
114+ } ) ;
115+ break ;
116+
117+ case 'ready' :
118+ console . log ( '[T-Ruby] Worker ready, health:' , data . health ) ;
119+ healthData = data . health ;
120+ onProgress ?.( {
121+ state : 'ready' ,
122+ message : 'Compiler ready' ,
123+ progress : 100
124+ } ) ;
125+
126+ // Create compiler interface
127+ const compilerInstance : TRubyCompiler = {
128+ async compile ( code : string ) : Promise < CompileResult > {
129+ return new Promise ( ( res , rej ) => {
130+ const reqId = generateRequestId ( ) ;
131+ pendingRequests . set ( reqId , { resolve : res , reject : rej } ) ;
132+
133+ console . log ( '[T-Ruby] Sending compile request:' , reqId ) ;
134+ iframe . contentWindow ?. postMessage ( {
135+ type : 'compile' ,
136+ data : { code } ,
137+ requestId : reqId
138+ } , '*' ) ;
139+
140+ // Timeout after 30 seconds
141+ setTimeout ( ( ) => {
142+ if ( pendingRequests . has ( reqId ) ) {
143+ pendingRequests . delete ( reqId ) ;
144+ rej ( new Error ( 'Compile timeout' ) ) ;
145+ }
146+ } , 30000 ) ;
147+ } ) ;
148+ } ,
149+
150+ healthCheck ( ) {
151+ return healthData || { loaded : false , version : 'unknown' , ruby_version : 'unknown' } ;
152+ } ,
153+
154+ getVersion ( ) {
155+ return {
156+ t_ruby : healthData ?. version || 'unknown' ,
157+ ruby : healthData ?. ruby_version || 'unknown'
158+ } ;
159+ }
160+ } ;
161+
162+ workerIframe = iframe ;
163+ resolve ( compilerInstance ) ;
164+ break ;
165+
166+ case 'compile-result' :
167+ console . log ( '[T-Ruby] Compile result received for:' , requestId ) ;
168+ const pending = pendingRequests . get ( requestId ) ;
169+ if ( pending ) {
170+ pendingRequests . delete ( requestId ) ;
171+ pending . resolve ( data ) ;
172+ }
173+ break ;
174+
175+ case 'error' :
176+ console . error ( '[T-Ruby] Worker error:' , data . message ) ;
177+ onProgress ?.( {
178+ state : 'error' ,
179+ message : data . message
180+ } ) ;
181+ window . removeEventListener ( 'message' , messageHandler ) ;
182+ document . body . removeChild ( iframe ) ;
183+ reject ( new Error ( data . message ) ) ;
184+ break ;
185+ }
186+ } ;
177187
178- // Health check
179- const healthResult = vm . eval ( '__trb_health_check__' ) ;
180- console . log ( '[T-Ruby] Health check:' , healthResult . toString ( ) ) ;
188+ window . addEventListener ( 'message' , messageHandler ) ;
181189
182- onProgress ?.( {
183- state : 'ready' ,
184- message : 'Compiler ready' ,
185- progress : 100
186- } ) ;
190+ // Error handling for iframe load failure
191+ iframe . onerror = ( ) => {
192+ console . error ( '[T-Ruby] Failed to load worker iframe' ) ;
193+ window . removeEventListener ( 'message' , messageHandler ) ;
194+ reject ( new Error ( 'Failed to load WASM worker' ) ) ;
195+ } ;
187196
188- // Return compiler interface
189- return {
190- compile ( code : string ) : CompileResult {
191- console . log ( '[T-Ruby] Compiling code:' , code . substring ( 0 , 100 ) + ( code . length > 100 ? '...' : '' ) ) ;
192- try {
193- const resultJson = vm . eval ( `__trb_compile__(${ JSON . stringify ( code ) } )` ) ;
194- const resultStr = resultJson . toString ( ) ;
195- console . log ( '[T-Ruby] Raw compile result:' , resultStr ) ;
196- const result = JSON . parse ( resultStr ) ;
197- console . log ( '[T-Ruby] Parsed compile result:' , result ) ;
198- return result ;
199- } catch ( e ) {
200- console . error ( '[T-Ruby] Compile error:' , e ) ;
201- throw e ;
202- }
203- } ,
204-
205- healthCheck ( ) {
206- const resultJson = vm . eval ( '__trb_health_check__' ) ;
207- return JSON . parse ( resultJson . toString ( ) ) ;
208- } ,
209-
210- getVersion ( ) {
211- const resultJson = vm . eval ( '__trb_version__' ) ;
212- return JSON . parse ( resultJson . toString ( ) ) ;
197+ // Timeout for initial load
198+ const loadTimeout = setTimeout ( ( ) => {
199+ if ( ! compiler ) {
200+ console . error ( '[T-Ruby] Worker initialization timeout' ) ;
201+ window . removeEventListener ( 'message' , messageHandler ) ;
202+ document . body . removeChild ( iframe ) ;
203+ reject ( new Error ( 'WASM worker initialization timeout' ) ) ;
213204 }
205+ } , 60000 ) ; // 60 second timeout for initial load
206+
207+ // Clear timeout when ready
208+ const originalResolve = resolve ;
209+ resolve = ( value ) => {
210+ clearTimeout ( loadTimeout ) ;
211+ originalResolve ( value ) ;
214212 } ;
215- } catch ( error ) {
216- console . error ( '[T-Ruby] Loading error:' , error ) ;
217- onProgress ?.( {
218- state : 'error' ,
219- message : error instanceof Error ? error . message : 'Unknown error'
220- } ) ;
221- throw error ;
222- }
213+
214+ // Add iframe to document
215+ document . body . appendChild ( iframe ) ;
216+ } ) ;
223217}
224218
225219/**
0 commit comments