Skip to content

Commit c162274

Browse files
petrbrzekclaude
andcommitted
Add optional Web Worker support for runtime execution
- Add IRuntime interface for polymorphism between sync/async runtimes - Add VFS snapshot/restore for transferring file system to worker - Add VFS event emitter for change/delete notifications - Add WorkerRuntime class that executes code in a Web Worker - Add createRuntime() factory with useWorker option - Maintain full backward compatibility (Runtime.execute() stays sync) - Add comlink dependency for worker communication - Add comprehensive tests for worker runtime and VFS snapshots Usage: // Main thread (default, backward compatible) const runtime = await createRuntime(vfs, { useWorker: false }); // Worker mode (opt-in for better UI responsiveness) const runtime = await createRuntime(vfs, { useWorker: true }); Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 57b8a7e commit c162274

9 files changed

Lines changed: 1012 additions & 23 deletions

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@webcontainer/api": "^1.6.1",
4848
"brotli": "^1.3.3",
4949
"brotli-wasm": "^3.0.1",
50+
"comlink": "^4.4.2",
5051
"just-bash": "^2.7.0",
5152
"pako": "^2.1.0",
5253
"resolve.exports": "^2.0.3",

src/create-runtime.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Runtime Factory - Create main-thread or worker runtime based on configuration
3+
*
4+
* Usage:
5+
* // Main thread (default)
6+
* const runtime = await createRuntime(vfs, { useWorker: false });
7+
*
8+
* // Worker mode
9+
* const runtime = await createRuntime(vfs, { useWorker: true });
10+
*
11+
* // Auto-detect
12+
* const runtime = await createRuntime(vfs, { useWorker: 'auto' });
13+
*/
14+
15+
import { Runtime } from './runtime';
16+
import { WorkerRuntime } from './worker-runtime';
17+
import type { VirtualFS } from './virtual-fs';
18+
import type { IRuntime, IExecuteResult, CreateRuntimeOptions, IRuntimeOptions } from './runtime-interface';
19+
20+
/**
21+
* Check if Web Workers are available in the current environment
22+
*/
23+
function isWorkerAvailable(): boolean {
24+
return typeof Worker !== 'undefined';
25+
}
26+
27+
/**
28+
* Wrapper that makes the synchronous Runtime conform to the async IRuntime interface
29+
*/
30+
class AsyncRuntimeWrapper implements IRuntime {
31+
private runtime: Runtime;
32+
33+
constructor(vfs: VirtualFS, options: IRuntimeOptions = {}) {
34+
this.runtime = new Runtime(vfs, options);
35+
}
36+
37+
async execute(code: string, filename?: string): Promise<IExecuteResult> {
38+
return Promise.resolve(this.runtime.execute(code, filename));
39+
}
40+
41+
async runFile(filename: string): Promise<IExecuteResult> {
42+
return Promise.resolve(this.runtime.runFile(filename));
43+
}
44+
45+
clearCache(): void {
46+
this.runtime.clearCache();
47+
}
48+
49+
getVFS(): VirtualFS {
50+
return this.runtime.getVFS();
51+
}
52+
53+
/**
54+
* Get the underlying sync Runtime for direct access to sync methods
55+
*/
56+
getSyncRuntime(): Runtime {
57+
return this.runtime;
58+
}
59+
}
60+
61+
/**
62+
* Create a runtime instance based on configuration
63+
*
64+
* @param vfs - Virtual file system instance
65+
* @param options - Runtime options including useWorker flag
66+
* @returns Promise resolving to IRuntime instance
67+
*/
68+
export async function createRuntime(
69+
vfs: VirtualFS,
70+
options: CreateRuntimeOptions = {}
71+
): Promise<IRuntime> {
72+
const { useWorker = false, ...runtimeOptions } = options;
73+
74+
// Determine if we should use a worker
75+
let shouldUseWorker = false;
76+
77+
if (useWorker === true) {
78+
shouldUseWorker = isWorkerAvailable();
79+
if (!shouldUseWorker) {
80+
console.warn('[createRuntime] Worker requested but not available, falling back to main thread');
81+
}
82+
} else if (useWorker === 'auto') {
83+
shouldUseWorker = isWorkerAvailable();
84+
console.log(`[createRuntime] Auto mode: using ${shouldUseWorker ? 'worker' : 'main thread'}`);
85+
}
86+
87+
if (shouldUseWorker) {
88+
console.log('[createRuntime] Creating WorkerRuntime');
89+
const workerRuntime = new WorkerRuntime(vfs, runtimeOptions);
90+
// Wait for worker to be ready by executing a simple command
91+
await workerRuntime.execute('/* worker ready check */', '/__worker_init__.js');
92+
return workerRuntime;
93+
}
94+
95+
console.log('[createRuntime] Creating main-thread Runtime');
96+
return new AsyncRuntimeWrapper(vfs, runtimeOptions);
97+
}
98+
99+
// Re-export types and classes for convenience
100+
export { Runtime } from './runtime';
101+
export { WorkerRuntime } from './worker-runtime';
102+
export type {
103+
IRuntime,
104+
IExecuteResult,
105+
IRuntimeOptions,
106+
CreateRuntimeOptions,
107+
VFSSnapshot,
108+
} from './runtime-interface';

src/runtime-interface.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Runtime Interface - Common interface for main-thread and worker runtimes
3+
*/
4+
5+
import type { VirtualFS } from './virtual-fs';
6+
7+
export interface IRuntimeOptions {
8+
cwd?: string;
9+
env?: Record<string, string>;
10+
onConsole?: (method: string, args: unknown[]) => void;
11+
}
12+
13+
export interface IModule {
14+
id: string;
15+
filename: string;
16+
exports: unknown;
17+
loaded: boolean;
18+
children: IModule[];
19+
paths: string[];
20+
}
21+
22+
export interface IExecuteResult {
23+
exports: unknown;
24+
module: IModule;
25+
}
26+
27+
/**
28+
* Common runtime interface implemented by both MainThreadRuntime and WorkerRuntime
29+
*/
30+
export interface IRuntime {
31+
/**
32+
* Execute code as a module
33+
*/
34+
execute(code: string, filename?: string): Promise<IExecuteResult>;
35+
36+
/**
37+
* Run a file from the virtual file system
38+
*/
39+
runFile(filename: string): Promise<IExecuteResult>;
40+
41+
/**
42+
* Clear the module cache
43+
*/
44+
clearCache(): void;
45+
46+
/**
47+
* Get the virtual file system (only available on main thread runtime)
48+
*/
49+
getVFS?(): VirtualFS;
50+
51+
/**
52+
* Terminate the runtime (only applicable to worker runtime)
53+
*/
54+
terminate?(): void;
55+
}
56+
57+
/**
58+
* Options for creating a runtime
59+
*/
60+
export interface CreateRuntimeOptions extends IRuntimeOptions {
61+
/**
62+
* Whether to use a Web Worker for code execution
63+
* - false (default): Execute on main thread
64+
* - true: Execute in a Web Worker
65+
* - 'auto': Use worker if available, fallback to main thread
66+
*/
67+
useWorker?: boolean | 'auto';
68+
}
69+
70+
/**
71+
* VFS snapshot for transferring to worker
72+
*/
73+
export interface VFSSnapshot {
74+
files: VFSFileEntry[];
75+
}
76+
77+
export interface VFSFileEntry {
78+
path: string;
79+
type: 'file' | 'directory';
80+
content?: string; // base64 encoded for binary files
81+
}

src/runtime.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { VirtualFS } from './virtual-fs';
9+
import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
910
import { createFsShim, FsShim } from './shims/fs';
1011
import * as pathShim from './shims/path';
1112
import { createProcess, Process } from './shims/process';
@@ -769,6 +770,8 @@ function createConsoleWrapper(
769770

770771
/**
771772
* Runtime class for executing code in virtual environment
773+
* Note: This class has sync methods for backward compatibility.
774+
* Use createRuntime() factory for IRuntime interface compliance.
772775
*/
773776
export class Runtime {
774777
private vfs: VirtualFS;
@@ -886,7 +889,7 @@ export class Runtime {
886889
}
887890

888891
/**
889-
* Execute code as a module
892+
* Execute code as a module (synchronous - backward compatible)
890893
*/
891894
execute(
892895
code: string,
@@ -968,13 +971,41 @@ ${code}
968971
}
969972

970973
/**
971-
* Run a file from the virtual file system
974+
* Execute code as a module (async version for IRuntime interface)
975+
* Alias: executeSync() is the same as execute() for backward compatibility
976+
*/
977+
executeSync = this.execute;
978+
979+
/**
980+
* Execute code as a module (async - for IRuntime interface)
981+
*/
982+
async executeAsync(
983+
code: string,
984+
filename: string = '/index.js'
985+
): Promise<IExecuteResult> {
986+
return Promise.resolve(this.execute(code, filename));
987+
}
988+
989+
/**
990+
* Run a file from the virtual file system (synchronous - backward compatible)
972991
*/
973992
runFile(filename: string): { exports: unknown; module: Module } {
974993
const code = this.vfs.readFileSync(filename, 'utf8');
975994
return this.execute(code, filename);
976995
}
977996

997+
/**
998+
* Alias for runFile (backward compatibility)
999+
*/
1000+
runFileSync = this.runFile;
1001+
1002+
/**
1003+
* Run a file from the virtual file system (async - for IRuntime interface)
1004+
*/
1005+
async runFileAsync(filename: string): Promise<IExecuteResult> {
1006+
return Promise.resolve(this.runFile(filename));
1007+
}
1008+
9781009
/**
9791010
* Clear the module cache
9801011
*/
@@ -998,7 +1029,7 @@ ${code}
9981029
}
9991030

10001031
/**
1001-
* Create and execute code in a new runtime
1032+
* Create and execute code in a new runtime (synchronous - backward compatible)
10021033
*/
10031034
export function execute(
10041035
code: string,
@@ -1009,4 +1040,7 @@ export function execute(
10091040
return runtime.execute(code);
10101041
}
10111042

1043+
// Re-export types
1044+
export type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
1045+
10121046
export default Runtime;

0 commit comments

Comments
 (0)