Skip to content

Commit d04c4af

Browse files
committed
fix: use Web Worker for WASM isolation from extensions
Web Workers run in a separate thread and browser extensions don't inject content scripts into them, avoiding the FinalizationRegistry override issue.
1 parent eb19284 commit d04c4af

File tree

1 file changed

+47
-72
lines changed

1 file changed

+47
-72
lines changed

src/lib/ruby-wasm.ts

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* T-Ruby WASM Loader for Playground
33
*
4-
* Uses a sandboxed iframe with srcdoc to isolate WASM execution from browser extensions
5-
* that might interfere with FinalizationRegistry and other APIs.
4+
* Uses a Web Worker to isolate WASM execution from browser extensions.
5+
* Web Workers run in a separate thread and extensions don't inject content scripts into them.
66
*/
77

88
// Types
@@ -30,7 +30,7 @@ export interface LoadingProgress {
3030
// Singleton state
3131
let compiler: TRubyCompiler | null = null;
3232
let loadingPromise: Promise<TRubyCompiler> | null = null;
33-
let workerIframe: HTMLIFrameElement | null = null;
33+
let wasmWorker: Worker | null = null;
3434
let healthData: { loaded: boolean; version: string; ruby_version: string } | null = null;
3535

3636
// Pending compile requests
@@ -45,22 +45,14 @@ function generateRequestId(): string {
4545
return `req_${++requestIdCounter}_${Date.now()}`;
4646
}
4747

48-
// Worker HTML content - embedded as string to use srcdoc
49-
// This creates an about:srcdoc origin iframe which most extensions don't target
50-
const WORKER_HTML = `<!DOCTYPE html>
51-
<html>
52-
<head>
53-
<meta charset="UTF-8">
54-
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: https://cdn.jsdelivr.net;">
55-
<title>T-Ruby WASM Worker</title>
56-
</head>
57-
<body>
58-
<script type="module">
59-
// CDN URLs
48+
// Web Worker code as a string - runs in isolated thread
49+
const WORKER_CODE = `
50+
// Web Worker for T-Ruby WASM compilation
51+
// This runs in a separate thread, isolated from browser extensions
52+
6053
const RUBY_WASM_CDN = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/browser/+esm';
6154
const RUBY_WASM_BINARY = 'https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.7.0/dist/ruby+stdlib.wasm';
6255
63-
// Bootstrap code for T-Ruby compiler
6456
const BOOTSTRAP_CODE = \`
6557
require "json"
6658
@@ -111,36 +103,36 @@ end
111103
let vm = null;
112104
let isReady = false;
113105
114-
// Send message to parent
115-
function sendToParent(type, data, requestId) {
116-
parent.postMessage({ type, data, requestId }, '*');
106+
// Send message to main thread
107+
function sendToMain(type, data, requestId) {
108+
self.postMessage({ type, data, requestId });
117109
}
118110
119111
// Initialize Ruby VM
120112
async function initialize() {
121113
try {
122114
console.log('[WASM Worker] Step 1: Loading Ruby WASM module...');
123-
sendToParent('progress', { message: 'Loading Ruby runtime...', progress: 10 });
115+
sendToMain('progress', { message: 'Loading Ruby runtime...', progress: 10 });
124116
125117
const { DefaultRubyVM } = await import(RUBY_WASM_CDN);
126118
console.log('[WASM Worker] Step 1 complete');
127119
128120
console.log('[WASM Worker] Step 2: Fetching WASM binary...');
129-
sendToParent('progress', { message: 'Downloading Ruby WASM binary...', progress: 30 });
121+
sendToMain('progress', { message: 'Downloading Ruby WASM binary...', progress: 30 });
130122
131123
const response = await fetch(RUBY_WASM_BINARY);
132124
const wasmModule = await WebAssembly.compileStreaming(response);
133125
console.log('[WASM Worker] Step 2 complete');
134126
135127
console.log('[WASM Worker] Step 3: Initializing Ruby VM...');
136-
sendToParent('progress', { message: 'Initializing Ruby VM...', progress: 60 });
128+
sendToMain('progress', { message: 'Initializing Ruby VM...', progress: 60 });
137129
138130
const result = await DefaultRubyVM(wasmModule);
139131
vm = result.vm;
140132
console.log('[WASM Worker] Step 3 complete');
141133
142134
console.log('[WASM Worker] Step 4: Loading T-Ruby bootstrap...');
143-
sendToParent('progress', { message: 'Loading T-Ruby compiler...', progress: 80 });
135+
sendToMain('progress', { message: 'Loading T-Ruby compiler...', progress: 80 });
144136
145137
vm.eval(BOOTSTRAP_CODE);
146138
console.log('[WASM Worker] Step 4 complete');
@@ -150,19 +142,21 @@ async function initialize() {
150142
console.log('[WASM Worker] Health check:', healthResult.toString());
151143
152144
isReady = true;
153-
sendToParent('ready', { health: JSON.parse(healthResult.toString()) });
145+
sendToMain('ready', { health: JSON.parse(healthResult.toString()) });
154146
155147
} catch (error) {
156148
console.error('[WASM Worker] Init error:', error);
157-
sendToParent('error', { message: error.message });
149+
sendToMain('error', { message: error.message });
158150
}
159151
}
160152
161153
// Compile code
162154
function compile(code, requestId) {
163155
if (!isReady || !vm) {
164-
sendToParent('compile-result', {
156+
sendToMain('compile-result', {
165157
success: false,
158+
ruby: '',
159+
rbs: '',
166160
errors: ['Compiler not ready']
167161
}, requestId);
168162
return;
@@ -173,10 +167,10 @@ function compile(code, requestId) {
173167
const resultJson = vm.eval('__trb_compile__(' + JSON.stringify(code) + ')');
174168
const result = JSON.parse(resultJson.toString());
175169
console.log('[WASM Worker] Compile result:', result);
176-
sendToParent('compile-result', result, requestId);
170+
sendToMain('compile-result', result, requestId);
177171
} catch (error) {
178172
console.error('[WASM Worker] Compile error:', error);
179-
sendToParent('compile-result', {
173+
sendToMain('compile-result', {
180174
success: false,
181175
ruby: '',
182176
rbs: '',
@@ -185,8 +179,8 @@ function compile(code, requestId) {
185179
}
186180
}
187181
188-
// Listen for messages from parent
189-
window.addEventListener('message', (event) => {
182+
// Listen for messages from main thread
183+
self.addEventListener('message', (event) => {
190184
const { type, data, requestId } = event.data || {};
191185
192186
switch (type) {
@@ -200,13 +194,11 @@ window.addEventListener('message', (event) => {
200194
});
201195
202196
// Signal that worker is loaded
203-
sendToParent('loaded', {});
204-
<\/script>
205-
</body>
206-
</html>`;
197+
sendToMain('loaded', {});
198+
`;
207199

208200
/**
209-
* Load the T-Ruby WASM compiler using sandboxed iframe with srcdoc
201+
* Load the T-Ruby WASM compiler using Web Worker
210202
* Returns cached instance if already loaded
211203
*/
212204
export async function loadTRubyCompiler(
@@ -239,40 +231,31 @@ async function doLoadCompiler(
239231
onProgress?: (progress: LoadingProgress) => void
240232
): Promise<TRubyCompiler> {
241233
return new Promise((resolve, reject) => {
242-
console.log('[T-Ruby] Creating sandboxed srcdoc iframe for WASM execution...');
234+
console.log('[T-Ruby] Creating Web Worker for WASM execution...');
243235
onProgress?.({
244236
state: 'loading',
245-
message: 'Initializing compiler sandbox...',
237+
message: 'Initializing compiler worker...',
246238
progress: 5
247239
});
248240

249-
// Create iframe with blob URL
250-
// Using blob: URL creates a unique origin that extensions typically don't target
251-
const iframe = document.createElement('iframe');
252-
iframe.style.display = 'none';
253-
254-
// Create blob URL - this gives us a blob: origin
255-
const blob = new Blob([WORKER_HTML], { type: 'text/html' });
241+
// Create Web Worker from blob URL
242+
// Web Workers run in a separate thread, isolated from extension content scripts
243+
const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
256244
const blobUrl = URL.createObjectURL(blob);
257-
iframe.src = blobUrl;
245+
const worker = new Worker(blobUrl, { type: 'module' });
258246

259-
// Clean up blob URL after iframe loads
260-
iframe.onload = () => {
261-
URL.revokeObjectURL(blobUrl);
262-
};
247+
// Clean up blob URL
248+
URL.revokeObjectURL(blobUrl);
263249

264250
// Message handler
265-
const messageHandler = (event: MessageEvent) => {
266-
// Only accept messages from our iframe
267-
if (event.source !== iframe.contentWindow) return;
268-
251+
worker.onmessage = (event: MessageEvent) => {
269252
const { type, data, requestId } = event.data || {};
270253
console.log('[T-Ruby] Received message from worker:', type, data);
271254

272255
switch (type) {
273256
case 'loaded':
274-
console.log('[T-Ruby] Worker iframe loaded, sending init command...');
275-
iframe.contentWindow?.postMessage({ type: 'init' }, '*');
257+
console.log('[T-Ruby] Worker loaded, sending init command...');
258+
worker.postMessage({ type: 'init' });
276259
break;
277260

278261
case 'progress':
@@ -300,11 +283,11 @@ async function doLoadCompiler(
300283
pendingRequests.set(reqId, { resolve: res, reject: rej });
301284

302285
console.log('[T-Ruby] Sending compile request:', reqId);
303-
iframe.contentWindow?.postMessage({
286+
worker.postMessage({
304287
type: 'compile',
305288
data: { code },
306289
requestId: reqId
307-
}, '*');
290+
});
308291

309292
// Timeout after 30 seconds
310293
setTimeout(() => {
@@ -328,7 +311,7 @@ async function doLoadCompiler(
328311
}
329312
};
330313

331-
workerIframe = iframe;
314+
wasmWorker = worker;
332315
resolve(compilerInstance);
333316
break;
334317

@@ -347,28 +330,23 @@ async function doLoadCompiler(
347330
state: 'error',
348331
message: data.message
349332
});
350-
window.removeEventListener('message', messageHandler);
351-
document.body.removeChild(iframe);
333+
worker.terminate();
352334
reject(new Error(data.message));
353335
break;
354336
}
355337
};
356338

357-
window.addEventListener('message', messageHandler);
358-
359-
// Error handling for iframe load failure
360-
iframe.onerror = () => {
361-
console.error('[T-Ruby] Failed to load worker iframe');
362-
window.removeEventListener('message', messageHandler);
363-
reject(new Error('Failed to load WASM worker'));
339+
// Error handling
340+
worker.onerror = (error) => {
341+
console.error('[T-Ruby] Worker error:', error);
342+
reject(new Error('Failed to initialize WASM worker'));
364343
};
365344

366345
// Timeout for initial load
367346
const loadTimeout = setTimeout(() => {
368347
if (!compiler) {
369348
console.error('[T-Ruby] Worker initialization timeout');
370-
window.removeEventListener('message', messageHandler);
371-
document.body.removeChild(iframe);
349+
worker.terminate();
372350
reject(new Error('WASM worker initialization timeout'));
373351
}
374352
}, 60000); // 60 second timeout for initial load
@@ -379,9 +357,6 @@ async function doLoadCompiler(
379357
clearTimeout(loadTimeout);
380358
originalResolve(value);
381359
};
382-
383-
// Add iframe to document
384-
document.body.appendChild(iframe);
385360
});
386361
}
387362

0 commit comments

Comments
 (0)