UNPKG

gowm

Version:

Go Wasm Manager - Loader system for Go WebAssembly modules with GitHub repository support. Features intelligent auto-detection and enhanced memory management.

487 lines (423 loc) 15.7 kB
/** * Unified WASM Loader * Supports loading from local files, HTTP URLs, and GitHub repositories * Works in both Node.js and browser environments */ class UnifiedWasmLoader { constructor() { this.modules = new Map(); this.isNode = typeof window === 'undefined'; this.nodeModulesLoaded = false; // Node.js modules will be imported dynamically when needed this.fs = null; this.path = null; } /** * Dynamically load Node.js modules to avoid bundling issues */ async ensureNodeModules() { if (this.isNode && !this.nodeModulesLoaded) { try { this.fs = this.dynamicRequire('fs'); this.path = this.dynamicRequire('path'); this.nodeModulesLoaded = true; } catch (error) { console.warn('Failed to load Node.js modules:', error.message); } } } /** * Dynamic require to avoid bundler issues */ dynamicRequire(module) { if (typeof require !== 'undefined') { return require(module); } throw new Error(`Module ${module} not available`); } /** * Main module loading method - supports file paths, URLs, and GitHub repos * @param {string} source - Can be file path, HTTP URL, or GitHub repo (owner/repo) * @param {object} options - Loading options * @returns {Promise<object>} Loaded WASM module */ async loadModule(source, options = {}) { await this.ensureNodeModules(); const moduleId = options.name || this.extractModuleId(source); if (this.modules.has(moduleId)) { return this.modules.get(moduleId); } try { // Load wasm_exec.js if necessary if (!globalThis.Go) { await this.loadGoRuntime(options.goRuntimePath); } const go = new globalThis.Go(); const wasmBytes = await this.loadWasmBytes(source, options); const result = await WebAssembly.instantiate(wasmBytes, go.importObject); // Pre-initialization to optimize performance if (options.preInit !== false) { // Start Go in the background to avoid blocking go.run(result.instance); // Wait for the module to be ready await this.waitForReady(moduleId, options.timeout); } const module = { instance: result.instance, go: go, exports: result.instance.exports, ready: true, source: source, loadedAt: new Date().toISOString() }; this.modules.set(moduleId, module); return module; } catch (error) { throw new Error(`Failed to load WASM module ${moduleId}: ${error.message}`); } } /** * Load WASM bytes from various sources * @param {string} source - Source identifier * @param {object} options - Loading options * @returns {Promise<ArrayBuffer>} WASM bytes */ async loadWasmBytes(source, options = {}) { // GitHub repository loading if (this.isGitHubRepo(source)) { return this.loadFromGitHub(source, options); } // HTTP/HTTPS URL loading if (this.isHttpUrl(source)) { return this.loadFromHttp(source); } // Local file loading (Node.js only) if (this.isNode) { return this.loadFromFile(source); } // Browser: treat as relative URL return this.loadFromHttp(source); } /** * Load from local file system (Node.js) * @param {string} filePath - Path to WASM file * @returns {Promise<ArrayBuffer>} WASM bytes */ async loadFromFile(filePath) { if (!this.fs) { throw new Error('File system not available'); } if (!this.fs.existsSync(filePath)) { throw new Error(`WASM file not found: ${filePath}`); } const buffer = this.fs.readFileSync(filePath); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } /** * Load from HTTP/HTTPS URL * @param {string} url - URL to WASM file * @returns {Promise<ArrayBuffer>} WASM bytes */ async loadFromHttp(url) { // Ensure fetch is available await this.ensureFetch(); const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch WASM file: ${response.status} ${response.statusText}`); } return response.arrayBuffer(); } /** * Load from GitHub repository * @param {string} repoIdentifier - GitHub repo in format "owner/repo" * @param {object} options - Loading options * @returns {Promise<ArrayBuffer>} WASM bytes */ async loadFromGitHub(repoIdentifier, options = {}) { const repoInfo = this.parseGitHubRepo(repoIdentifier); const wasmUrl = await this.buildGitHubWasmUrl(repoInfo, options); console.log(`🔄 Loading WASM from GitHub: ${wasmUrl}`); return this.loadFromHttp(wasmUrl); } /** * Parse GitHub repository identifier * @param {string} repo - Repository identifier * @returns {object} Parsed repository information */ parseGitHubRepo(repo) { // Handle full GitHub URLs if (repo.includes('github.com')) { const match = repo.match(/github\.com\/([^\/]+)\/([^\/\?#]+)/); if (!match) { throw new Error('Invalid GitHub URL format'); } return { owner: match[1], repo: match[2].replace(/\.git$/, ''), fullRepo: `${match[1]}/${match[2].replace(/\.git$/, '')}` }; } // Handle "owner/repo" format const parts = repo.split('/'); if (parts.length !== 2) { throw new Error('GitHub repository must be in format "owner/repo"'); } return { owner: parts[0], repo: parts[1], fullRepo: repo }; } /** * Build GitHub raw URL for WASM file with fallback strategy * @param {object} repoInfo - Repository information * @param {object} options - Loading options * @returns {Promise<string>} URL to WASM file */ async buildGitHubWasmUrl(repoInfo, options) { const { owner, repo } = repoInfo; const branch = options.tag || options.branch || 'main'; const basePath = options.path ? `${options.path}/` : ''; // If specific filename is provided if (options.filename) { return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${basePath}${options.filename}`; } // Try different common WASM filenames const possibleFilenames = [ 'main.wasm', 'index.wasm', `${repo}.wasm`, 'module.wasm', 'wasm/main.wasm', 'wasm/index.wasm', 'dist/main.wasm', 'dist/index.wasm', 'build/main.wasm', 'build/index.wasm' ]; // Try each possible filename for (const filename of possibleFilenames) { const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${basePath}${filename}`; try { const exists = await this.checkUrlExists(url); if (exists) { return url; } } catch (e) { continue; } } throw new Error(`Could not find WASM file in repository ${repoInfo.fullRepo}`); } /** * Check if URL exists * @param {string} url - URL to check * @returns {Promise<boolean>} Whether URL exists */ async checkUrlExists(url) { await this.ensureFetch(); try { const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch (error) { return false; } } /** * Ensure fetch is available (for Node.js environments) */ async ensureFetch() { if (this.isNode && typeof fetch === 'undefined') { try { const nodeFetch = this.dynamicRequire('node-fetch'); global.fetch = nodeFetch; } catch (e) { if (typeof fetch === 'undefined') { throw new Error('fetch is not available. Please install node-fetch: npm install node-fetch'); } } } } /** * Wait for module to be ready * @param {string} moduleId - Module identifier * @param {number} timeout - Timeout in milliseconds * @returns {Promise<void>} */ async waitForReady(moduleId, timeout = 15000) { const startTime = Date.now(); return new Promise((resolve, reject) => { let checkCount = 0; const maxChecks = timeout / 50; const checkReady = () => { checkCount++; const signals = { gowmReady: globalThis.__gowm_ready === true, goInitialized: globalThis.Go && globalThis.Go._initialized, functionsAvailable: typeof globalThis.getAvailableFunctions === 'function', moduleSpecificFunctions: typeof globalThis.add === 'function' || typeof globalThis.get === 'function' || typeof globalThis.post === 'function', moduleReady: globalThis[`__${moduleId}_ready`] === true }; const isReady = signals.gowmReady || signals.goInitialized || signals.functionsAvailable || signals.moduleSpecificFunctions || signals.moduleReady; if (checkCount % 20 === 0) { console.log(`🔍 GoWM: Checking readiness for ${moduleId} (attempt ${checkCount}/${maxChecks}):`, signals); } const timeElapsed = Date.now() - startTime; if (isReady) { console.log(`✅ GoWM: Module ${moduleId} is ready after ${timeElapsed}ms`); if (globalThis.__gowm_ready) { globalThis.__gowm_ready = false; } resolve(); } else if (timeElapsed > timeout) { console.error(`❌ GoWM: Module ${moduleId} timeout after ${timeElapsed}ms. Signals:`, signals); reject(new Error(`Module ${moduleId} failed to initialize within ${timeout}ms`)); } else { setTimeout(checkReady, 50); } }; setTimeout(checkReady, 100); }); } /** * Load Go runtime (wasm_exec.js) * @param {string} customPath - Custom path to runtime */ async loadGoRuntime(customPath) { await this.ensureNodeModules(); const runtimePath = customPath || this.getDefaultRuntimePath(); if (this.isNode) { if (!this.fs || !this.fs.existsSync(runtimePath)) { console.warn(`Go runtime not found at ${runtimePath}, using fallback`); const fallbackPath = this.path ? this.path.join(__dirname, '../../runtime/wasm_exec.js') : null; if (fallbackPath && this.fs && this.fs.existsSync(fallbackPath)) { this.dynamicRequire(fallbackPath); } else { throw new Error('Go runtime not found. Please ensure wasm_exec.js is available'); } } else { this.dynamicRequire(runtimePath); } } else { await this.loadScript(runtimePath); } } /** * Get default runtime path * @returns {string} Default runtime path */ getDefaultRuntimePath() { if (this.isNode && this.path) { return this.path.join(__dirname, '../../runtime/wasm_exec.js'); } return '/wasm_exec.js'; } /** * Load script in browser * @param {string} src - Script source * @returns {Promise<void>} */ loadScript(src) { return new Promise((resolve, reject) => { if (typeof document === 'undefined') { reject(new Error('Cannot load script in non-browser environment')); return; } const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /** * Extract module ID from source * @param {string} source - Source identifier * @returns {string} Module ID */ extractModuleId(source) { if (this.isGitHubRepo(source)) { return source.split('/')[1]; // repo name } if (this.isNode && this.path) { return this.path.basename(source, '.wasm'); } return source.split('/').pop().replace('.wasm', ''); } /** * Check if source is GitHub repository * @param {string} source - Source identifier * @returns {boolean} */ isGitHubRepo(source) { return (source.includes('/') && !source.includes('.') && !source.startsWith('http')) || source.includes('github.com'); } /** * Check if source is HTTP URL * @param {string} source - Source identifier * @returns {boolean} */ isHttpUrl(source) { return source.startsWith('http://') || source.startsWith('https://'); } /** * Get loaded module * @param {string} name - Module name * @returns {object|null} Module or null */ getModule(name) { return this.modules.get(name); } /** * Unload module * @param {string} name - Module name * @returns {boolean} Success */ unloadModule(name) { const module = this.modules.get(name); if (module && module.go) { try { module.go.exit(0); } catch (e) { // Ignore cleanup errors } } return this.modules.delete(name); } /** * List all loaded modules * @returns {Array<string>} Module names */ listModules() { return Array.from(this.modules.keys()); } /** * Get loader statistics * @returns {object} Statistics */ getStats() { const modules = Array.from(this.modules.entries()).map(([name, module]) => ({ name, ready: module.ready, source: module.source, loadedAt: module.loadedAt, memoryUsage: module.go ? module.go.mem.buffer.byteLength : 0 })); return { totalModules: this.modules.size, environment: this.isNode ? 'Node.js' : 'Browser', modules }; } } // Export for both CommonJS and ES modules if (typeof module !== 'undefined' && module.exports) { module.exports = UnifiedWasmLoader; } else { window.UnifiedWasmLoader = UnifiedWasmLoader; }