/** * Runtime Augmentation Loader * * Dynamically loads and registers augmentations at runtime * Supports CDN loading for browser environments and npm imports for Node.js */ import { BrainyAugmentation, AugmentationRegistry } from '../brainyAugmentation.js' import { AugmentationManifest } from '../manifest.js' export interface LoaderOptions { cdnUrl?: string allowUnsafe?: boolean sandbox?: boolean timeout?: number cache?: boolean } export interface LoadedAugmentation { id: string instance: BrainyAugmentation manifest: AugmentationManifest source: 'cdn' | 'npm' | 'local' loadTime: number } /** * Runtime Augmentation Loader * * Enables dynamic loading of augmentations from various sources */ export class RuntimeAugmentationLoader { private loaded: Map = new Map() private cdnCache: Map = new Map() private registry?: AugmentationRegistry constructor(private options: LoaderOptions = {}) { this.options = { cdnUrl: options.cdnUrl || 'https://cdn.soulcraft.com/augmentations', allowUnsafe: options.allowUnsafe || false, sandbox: options.sandbox || true, timeout: options.timeout || 30000, cache: options.cache ?? true } } /** * Set the augmentation registry */ setRegistry(registry: AugmentationRegistry): void { this.registry = registry } /** * Load augmentation from CDN (browser) */ async loadFromCDN( id: string, version: string = 'latest', config?: any ): Promise { // Check if already loaded if (this.loaded.has(id)) { return this.loaded.get(id)! } const url = `${this.options.cdnUrl}/${id}@${version}/index.js` const startTime = Date.now() try { // Load module from CDN const module = await this.loadCDNModule(url) // Extract augmentation class const AugmentationClass = module.default || module[Object.keys(module)[0]] if (!AugmentationClass) { throw new Error(`No augmentation class found in module ${id}`) } // Create instance const instance = new AugmentationClass(config) // Validate it's a proper augmentation if (!this.isValidAugmentation(instance)) { throw new Error(`Invalid augmentation: ${id}`) } // Get manifest const manifest = instance.getManifest ? instance.getManifest() : { id, name: id, version: version, description: `Dynamically loaded ${id}`, category: 'external' } const loaded: LoadedAugmentation = { id, instance, manifest, source: 'cdn', loadTime: Date.now() - startTime } // Cache this.loaded.set(id, loaded) // Auto-register if registry is set if (this.registry) { this.registry.register(instance) } return loaded } catch (error) { throw new Error(`Failed to load augmentation ${id} from CDN: ${error}`) } } /** * Load augmentation from NPM (Node.js) */ async loadFromNPM( packageName: string, config?: any ): Promise { // Check if already loaded const id = packageName.replace('@', '').replace('/', '-') if (this.loaded.has(id)) { return this.loaded.get(id)! } const startTime = Date.now() try { // Dynamic import const module = await import(packageName) // Extract augmentation class const AugmentationClass = module.default || module[Object.keys(module)[0]] if (!AugmentationClass) { throw new Error(`No augmentation class found in package ${packageName}`) } // Create instance const instance = new AugmentationClass(config) // Validate if (!this.isValidAugmentation(instance)) { throw new Error(`Invalid augmentation in package: ${packageName}`) } // Get manifest const manifest = instance.getManifest ? instance.getManifest() : { id, name: packageName, version: 'unknown', description: `Loaded from ${packageName}`, category: 'external' } const loaded: LoadedAugmentation = { id, instance, manifest, source: 'npm', loadTime: Date.now() - startTime } // Cache this.loaded.set(id, loaded) // Auto-register if (this.registry) { this.registry.register(instance) } return loaded } catch (error) { throw new Error(`Failed to load augmentation from NPM ${packageName}: ${error}`) } } /** * Load augmentation from local file */ async loadFromFile( path: string, config?: any ): Promise { const startTime = Date.now() try { // Dynamic import const module = await import(path) // Extract augmentation class const AugmentationClass = module.default || module[Object.keys(module)[0]] if (!AugmentationClass) { throw new Error(`No augmentation class found in file ${path}`) } // Create instance const instance = new AugmentationClass(config) // Validate if (!this.isValidAugmentation(instance)) { throw new Error(`Invalid augmentation in file: ${path}`) } // Extract ID from path const id = path.split('/').pop()?.replace(/\.(js|ts)$/, '') || 'unknown' // Get manifest const manifest = instance.getManifest ? instance.getManifest() : { id, name: id, version: 'local', description: `Loaded from ${path}`, category: 'local' } const loaded: LoadedAugmentation = { id, instance, manifest, source: 'local', loadTime: Date.now() - startTime } // Cache this.loaded.set(id, loaded) // Auto-register if (this.registry) { this.registry.register(instance) } return loaded } catch (error) { throw new Error(`Failed to load augmentation from file ${path}: ${error}`) } } /** * Load multiple augmentations */ async loadBatch( augmentations: Array<{ source: 'cdn' | 'npm' | 'local' id: string version?: string path?: string config?: any }> ): Promise { const results = await Promise.allSettled( augmentations.map(aug => { switch (aug.source) { case 'cdn': return this.loadFromCDN(aug.id, aug.version, aug.config) case 'npm': return this.loadFromNPM(aug.id, aug.config) case 'local': return this.loadFromFile(aug.path || aug.id, aug.config) default: return Promise.reject(new Error(`Unknown source: ${aug.source}`)) } }) ) const loaded: LoadedAugmentation[] = [] const errors: string[] = [] for (const result of results) { if (result.status === 'fulfilled') { loaded.push(result.value) } else { errors.push(result.reason.message) } } if (errors.length > 0) { console.warn('Some augmentations failed to load:', errors) } return loaded } /** * Unload augmentation */ unload(id: string): boolean { const loaded = this.loaded.get(id) if (!loaded) return false // Shutdown if possible if (loaded.instance.shutdown) { loaded.instance.shutdown() } // Remove from registry if set // Note: Registry doesn't have unregister yet, would need to add // Remove from cache this.loaded.delete(id) return true } /** * Get loaded augmentations */ getLoaded(): LoadedAugmentation[] { return Array.from(this.loaded.values()) } /** * Check if augmentation is loaded */ isLoaded(id: string): boolean { return this.loaded.has(id) } /** * Get loaded augmentation */ getAugmentation(id: string): BrainyAugmentation | null { return this.loaded.get(id)?.instance || null } /** * Load CDN module (browser-specific) */ private async loadCDNModule(url: string): Promise { // Check cache if (this.options.cache && this.cdnCache.has(url)) { return this.cdnCache.get(url) } // In browser environment if (typeof window !== 'undefined') { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Timeout loading ${url}`)) }, this.options.timeout!) // Create script element const script = document.createElement('script') script.type = 'module' script.src = url // Handle load script.onload = async () => { clearTimeout(timeout) // The module should register itself on window const moduleId = url.split('/').pop()?.split('@')[0] if (moduleId && (window as any)[moduleId]) { const module = (window as any)[moduleId] // Cache if (this.options.cache) { this.cdnCache.set(url, module) } resolve(module) } else { reject(new Error(`Module not found on window: ${moduleId}`)) } } // Handle error script.onerror = () => { clearTimeout(timeout) reject(new Error(`Failed to load script: ${url}`)) } // Add to document document.head.appendChild(script) }) } else { // In Node.js, use dynamic import const module = await import(url) // Cache if (this.options.cache) { this.cdnCache.set(url, module) } return module } } /** * Validate augmentation instance */ private isValidAugmentation(instance: any): boolean { // Check required properties return !!( instance.name && instance.timing && instance.operations && instance.priority !== undefined && typeof instance.execute === 'function' && typeof instance.initialize === 'function' ) } /** * Clear all caches */ clearCache(): void { this.cdnCache.clear() } /** * Get load statistics */ getStats(): { loaded: number totalLoadTime: number averageLoadTime: number sources: Record } { const loaded = Array.from(this.loaded.values()) const totalLoadTime = loaded.reduce((sum, aug) => sum + aug.loadTime, 0) const sources = loaded.reduce((acc, aug) => { acc[aug.source] = (acc[aug.source] || 0) + 1 return acc }, {} as Record) return { loaded: loaded.length, totalLoadTime, averageLoadTime: loaded.length > 0 ? totalLoadTime / loaded.length : 0, sources } } }