greed.js
Version:
Run Python libraries in the browser with WebGPU acceleration - PyTorch, NumPy, and more. Modular architecture with full backward compatibility.
266 lines (224 loc) • 6.97 kB
JavaScript
/**
* RuntimeManager - Handles Pyodide initialization and Python package management
* Extracted from monolithic Greed class for better separation of concerns
*/
import EventEmitter from './event-emitter.js';
class RuntimeManager extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
pyodideIndexURL: config.pyodideIndexURL || 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/',
preloadPackages: config.preloadPackages || ['numpy'],
timeout: config.initTimeout || 30000,
...config
};
this.pyodide = null;
this.isReady = false;
this.installedPackages = new Set();
this.initPromise = null;
}
/**
* Initialize Pyodide runtime with error handling and progress tracking
*/
async initialize() {
if (this.initPromise) {
return this.initPromise;
}
this.initPromise = this._initializeInternal();
return this.initPromise;
}
async _initializeInternal() {
try {
this.emit('init:start', { stage: 'pyodide' });
// Validate Pyodide availability
if (typeof loadPyodide === 'undefined') {
throw new Error('Pyodide not loaded. Please include pyodide.js in your HTML.');
}
// Initialize with timeout
const pyodidePromise = loadPyodide({
indexURL: this.config.pyodideIndexURL
});
this.pyodide = await Promise.race([
pyodidePromise,
this._createTimeoutPromise(this.config.timeout, 'Pyodide initialization timeout')
]);
this.emit('init:progress', { stage: 'pyodide', status: 'loaded' });
// Pre-load essential packages
if (this.config.preloadPackages.length > 0) {
this.emit('init:progress', { stage: 'packages', packages: this.config.preloadPackages });
await this._loadPackages(this.config.preloadPackages);
}
this.isReady = true;
this.emit('init:complete', { installedPackages: Array.from(this.installedPackages) });
return true;
} catch (error) {
this.emit('init:error', { error, stage: 'initialization' });
throw error;
}
}
/**
* Load Python packages with progress tracking
*/
async loadPackages(packages) {
if (!this.isReady) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
return this._loadPackages(packages);
}
async _loadPackages(packages) {
const packagesToLoad = packages.filter(pkg => !this.installedPackages.has(pkg));
if (packagesToLoad.length === 0) {
return Array.from(this.installedPackages);
}
try {
this.emit('packages:loading', { packages: packagesToLoad });
await this.pyodide.loadPackage(packagesToLoad);
packagesToLoad.forEach(pkg => this.installedPackages.add(pkg));
this.emit('packages:loaded', {
loaded: packagesToLoad,
total: Array.from(this.installedPackages)
});
return Array.from(this.installedPackages);
} catch (error) {
this.emit('packages:error', { error, packages: packagesToLoad });
throw new Error(`Failed to load packages [${packagesToLoad.join(', ')}]: ${error.message}`);
}
}
/**
* Execute Python code with error handling and context isolation
*/
async runPython(code, options = {}) {
if (!this.isReady) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
const {
captureOutput = false,
timeout = 10000,
globals = {},
validateInput = true
} = options;
if (validateInput && this._containsDangerousPatterns(code)) {
throw new SecurityError('Potentially dangerous code patterns detected');
}
try {
// Set globals if provided
for (const [key, value] of Object.entries(globals)) {
this.pyodide.globals.set(key, value);
}
let result;
if (captureOutput) {
// Capture stdout for print statements
const outputCode = `
import sys
from io import StringIO
_output_buffer = StringIO()
_original_stdout = sys.stdout
sys.stdout = _output_buffer
try:
${code.split('\n').map(line => ' ' + line).join('\n')}
finally:
sys.stdout = _original_stdout
_captured_output = _output_buffer.getvalue()
`;
const executePromise = this.pyodide.runPython(outputCode);
await Promise.race([
executePromise,
this._createTimeoutPromise(timeout, 'Python execution timeout')
]);
// Get captured output
const capturedOutput = this.pyodide.globals.get('_captured_output');
result = { output: capturedOutput };
} else {
const executePromise = this.pyodide.runPythonAsync(code);
result = await Promise.race([
executePromise,
this._createTimeoutPromise(timeout, 'Python execution timeout')
]);
}
return result;
} catch (error) {
this.emit('execution:error', { error, code: code.substring(0, 100) });
throw error;
}
}
/**
* Get Python global variable
*/
getGlobal(name) {
if (!this.isReady) {
throw new Error('Runtime not initialized');
}
return this.pyodide.globals.get(name);
}
/**
* Set Python global variable
*/
setGlobal(name, value) {
if (!this.isReady) {
throw new Error('Runtime not initialized');
}
this.pyodide.globals.set(name, value);
}
/**
* Check if package is installed
*/
hasPackage(packageName) {
return this.installedPackages.has(packageName);
}
/**
* Get runtime status
*/
getStatus() {
return {
isReady: this.isReady,
installedPackages: Array.from(this.installedPackages),
pyodideVersion: this.pyodide?.version || null,
config: this.config
};
}
/**
* Cleanup runtime resources
*/
cleanup() {
try {
if (this.pyodide) {
this.pyodide.globals.clear();
this.pyodide = null;
}
this.isReady = false;
this.installedPackages.clear();
this.initPromise = null;
this.emit('cleanup:complete');
} catch (error) {
this.emit('cleanup:error', { error });
// Error already emitted for handling by parent components
}
}
// Private helper methods
_createTimeoutPromise(timeout, message) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(message)), timeout);
});
}
_containsDangerousPatterns(code) {
const dangerousPatterns = [
/\beval\s*\(/,
/\bexec\s*\(/,
/\b__import__\s*\(/,
/\bsubprocess\./,
/\bos\.system\s*\(/,
/\bopen\s*\(/,
/\bfile\s*\(/
];
return dangerousPatterns.some(pattern => pattern.test(code));
}
}
// Custom error for security violations
class SecurityError extends Error {
constructor(message) {
super(message);
this.name = 'SecurityError';
}
}
export default RuntimeManager;
export { SecurityError };