UNPKG

@memberjunction/react-runtime

Version:

Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.

503 lines 21.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentManager = void 0; const core_1 = require("@memberjunction/core"); const core_entities_1 = require("@memberjunction/core-entities"); class ComponentManager { constructor(compiler, registry, runtimeContext, config = {}) { this.fetchCache = new Map(); this.registryNotifications = new Set(); this.loadingPromises = new Map(); this.componentEngine = core_entities_1.ComponentMetadataEngine.Instance; this.graphQLClient = null; this.compiler = compiler; this.registry = registry; this.runtimeContext = runtimeContext; this.config = { debug: false, maxCacheSize: 100, cacheTTL: 3600000, enableUsageTracking: true, dependencyBatchSize: 5, fetchTimeout: 30000, ...config }; this.log('ComponentManager initialized', { debug: this.config.debug, cacheTTL: this.config.cacheTTL, usageTracking: this.config.enableUsageTracking }); } async loadComponent(spec, options = {}) { const startTime = Date.now(); const componentKey = this.getComponentKey(spec, options); this.log(`Loading component: ${spec.name}`, { key: componentKey, location: spec.location, registry: spec.registry, forceRefresh: options.forceRefresh }); const existingPromise = this.loadingPromises.get(componentKey); if (existingPromise && !options.forceRefresh) { this.log(`Component already loading: ${spec.name}, waiting...`); return existingPromise; } const loadPromise = this.doLoadComponent(spec, options, componentKey, startTime); this.loadingPromises.set(componentKey, loadPromise); try { const result = await loadPromise; return result; } finally { this.loadingPromises.delete(componentKey); } } async doLoadComponent(spec, options, componentKey, startTime) { const errors = []; try { const namespace = spec.namespace || options.defaultNamespace || 'Global'; const version = spec.version || options.defaultVersion || 'latest'; const existing = this.registry.get(spec.name, namespace, version); if (existing && !options.forceRefresh && !options.forceRecompile) { this.log(`Component found in registry: ${spec.name}`); if (options.trackUsage !== false) { await this.notifyRegistryUsageIfNeeded(spec, componentKey); } const cachedEntry = this.fetchCache.get(componentKey); return { success: true, component: existing, spec: cachedEntry?.spec || spec, fromCache: true }; } let fullSpec = spec; if (this.needsFetch(spec)) { this.log(`Fetching component spec: ${spec.name}`); try { fullSpec = await this.fetchComponentSpec(spec, options.contextUser, { resolutionMode: options.resolutionMode }); this.fetchCache.set(componentKey, { spec: fullSpec, fetchedAt: new Date(), hash: await this.calculateHash(fullSpec), usageNotified: false }); } catch (error) { errors.push({ message: `Failed to fetch component: ${error instanceof Error ? error.message : String(error)}`, phase: 'fetch', componentName: spec.name }); throw error; } } else { if (spec.location === 'registry' && spec.code) { this.log(`Skipping fetch for registry component: ${spec.name} (code already provided)`, { location: spec.location, registry: spec.registry }); } if (spec.code && !this.fetchCache.has(componentKey)) { this.fetchCache.set(componentKey, { spec: fullSpec, fetchedAt: new Date(), hash: await this.calculateHash(fullSpec), usageNotified: false }); } } if (options.trackUsage !== false) { await this.notifyRegistryUsageIfNeeded(fullSpec, componentKey); } let compiledComponent = existing; if (!compiledComponent || options.forceRecompile) { this.log(`Compiling component: ${spec.name}`); try { compiledComponent = await this.compileComponent(fullSpec, options); } catch (error) { errors.push({ message: `Failed to compile component: ${error instanceof Error ? error.message : String(error)}`, phase: 'compile', componentName: spec.name }); throw error; } } if (!existing || options.forceRefresh || options.forceRecompile) { this.log(`Registering component: ${spec.name}`); this.registry.register(fullSpec.name, compiledComponent, namespace, version); } const dependencies = {}; if (fullSpec.dependencies && fullSpec.dependencies.length > 0) { this.log(`Loading ${fullSpec.dependencies.length} dependencies for ${spec.name}`); const depResults = await this.loadDependenciesBatched(fullSpec.dependencies, { ...options, isDependent: true }); for (const result of depResults) { if (result.success && result.component) { const depSpec = fullSpec.dependencies.find(d => d.name === (result.spec?.name || '')); if (depSpec) { dependencies[depSpec.name] = result.component; } } else if (result.errors) { errors.push(...result.errors); } } } const elapsed = Date.now() - startTime; this.log(`Component loaded successfully: ${spec.name} (${elapsed}ms)`, { fromCache: false, dependencyCount: Object.keys(dependencies).length }); return { success: errors.length === 0, component: compiledComponent, spec: fullSpec, fromCache: false, dependencies, errors: errors.length > 0 ? errors : undefined }; } catch (error) { const elapsed = Date.now() - startTime; this.log(`Failed to load component: ${spec.name} (${elapsed}ms)`, error); return { success: false, fromCache: false, errors: errors.length > 0 ? errors : [{ message: error instanceof Error ? error.message : String(error), phase: 'fetch', componentName: spec.name }] }; } } async loadHierarchy(rootSpec, options = {}) { const startTime = Date.now(); const loaded = []; const errors = []; const components = {}; const stats = { fromCache: 0, fetched: 0, compiled: 0, totalTime: 0 }; this.log(`Loading component hierarchy: ${rootSpec.name}`, { location: rootSpec.location, registry: rootSpec.registry }); try { if (this.componentEngine && typeof this.componentEngine.Config === 'function') { await this.componentEngine.Config(false, options.contextUser); } const result = await this.loadComponentRecursive(rootSpec, options, loaded, errors, components, stats, new Set()); stats.totalTime = Date.now() - startTime; this.log(`Hierarchy loaded: ${rootSpec.name}`, { success: errors.length === 0, loadedCount: loaded.length, errors: errors.length, ...stats }); const unwrappedComponents = {}; for (const [name, componentObject] of Object.entries(components)) { if (componentObject && typeof componentObject === 'object' && 'component' in componentObject) { unwrappedComponents[name] = componentObject.component; } else { unwrappedComponents[name] = componentObject; } } return { success: errors.length === 0, rootComponent: result.component, resolvedSpec: result.spec, loadedComponents: loaded, errors, components: unwrappedComponents, stats }; } catch (error) { stats.totalTime = Date.now() - startTime; this.log(`Failed to load hierarchy: ${rootSpec.name}`, error); return { success: false, loadedComponents: loaded, errors: [...errors, { message: error instanceof Error ? error.message : String(error), phase: 'fetch', componentName: rootSpec.name }], stats }; } } async loadComponentRecursive(spec, options, loaded, errors, components, stats, visited) { const componentKey = this.getComponentKey(spec, options); if (visited.has(componentKey)) { this.log(`Circular dependency detected: ${spec.name}`); return { success: true, component: components[spec.name], spec, fromCache: true }; } visited.add(componentKey); const result = await this.loadComponent(spec, options); if (result.success && result.component) { loaded.push(spec.name); components[spec.name] = result.component; if (stats) { if (result.fromCache) stats.fromCache++; else { stats.fetched++; stats.compiled++; } } if (result.spec?.dependencies) { for (const dep of result.spec.dependencies) { const depSpec = { ...dep }; if (depSpec.code) { this.log(`Dependency ${depSpec.name} already has code (from registry population), optimizing load`); } if (depSpec.location === 'registry' && !depSpec.registry) { depSpec.registry = undefined; this.log(`Dependency ${depSpec.name} is a local registry component (registry=undefined)`); } await this.loadComponentRecursive(depSpec, { ...options, isDependent: true }, loaded, errors, components, stats, visited); } } } else if (result.errors) { errors.push(...result.errors); } return result; } async loadDependenciesBatched(dependencies, options) { const batchSize = this.config.dependencyBatchSize || 5; const results = []; for (let i = 0; i < dependencies.length; i += batchSize) { const batch = dependencies.slice(i, i + batchSize); const batchPromises = batch.map(dep => this.loadComponent(dep, options)); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); } return results; } needsFetch(spec) { return spec.location === 'registry' && !spec.code; } async fetchComponentSpec(spec, contextUser, options) { const cacheKey = this.getComponentKey(spec, {}); const cached = this.fetchCache.get(cacheKey); if (cached && this.isCacheValid(cached)) { this.log(`Using cached spec for: ${spec.name}`); return cached.spec; } if (!spec.registry) { this.log(`Fetching from local registry: ${spec.name}`); const localComponent = this.componentEngine.Components?.find((c) => { const nameMatch = c.Name?.toLowerCase() === spec.name?.toLowerCase(); const namespaceMatch = !spec.namespace || c.Namespace?.toLowerCase() === spec.namespace?.toLowerCase(); if (nameMatch && !namespaceMatch) { } return nameMatch && namespaceMatch; }); if (!localComponent) { throw new Error(`Local component not found: ${spec.name}`); } if (!localComponent.Specification) { throw new Error(`Local component ${spec.name} has no specification`); } const fullSpec = JSON.parse(localComponent.Specification); this.fetchCache.set(cacheKey, { spec: fullSpec, fetchedAt: new Date(), usageNotified: false }); return fullSpec; } if (!this.graphQLClient) { await this.initializeGraphQLClient(); } if (!this.graphQLClient) { throw new Error('GraphQL client not available for registry fetching'); } this.log(`Fetching from external registry: ${spec.registry}/${spec.name}`); const fullSpec = await this.graphQLClient.GetRegistryComponent({ registryName: spec.registry, namespace: spec.namespace || 'Global', name: spec.name, version: spec.version || 'latest' }); if (!fullSpec) { throw new Error(`Component not found in registry: ${spec.registry}/${spec.name}`); } const processedSpec = this.applyResolutionMode(fullSpec, spec, options?.resolutionMode); this.fetchCache.set(cacheKey, { spec: processedSpec, fetchedAt: new Date(), usageNotified: false }); return processedSpec; } applyResolutionMode(fullSpec, originalSpec, resolutionMode) { let processedSpec; if (resolutionMode === 'embed') { processedSpec = { ...fullSpec, location: 'embedded', registry: undefined, }; } else { processedSpec = { ...fullSpec, location: originalSpec.location, registry: originalSpec.registry, namespace: originalSpec.namespace || fullSpec.namespace, name: originalSpec.name || fullSpec.name }; } if (processedSpec.dependencies && processedSpec.dependencies.length > 0) { processedSpec.dependencies = processedSpec.dependencies.map(dep => { return this.applyResolutionMode(dep, dep, resolutionMode); }); } return processedSpec; } async compileComponent(spec, options) { const allLibraries = options.allLibraries || this.componentEngine.ComponentLibraries || []; const validLibraries = spec.libraries?.filter(lib => lib && lib.name && lib.globalVariable && lib.name !== 'unknown' && lib.globalVariable !== 'undefined'); const result = await this.compiler.compile({ componentName: spec.name, componentCode: spec.code || '', libraries: validLibraries, dependencies: spec.dependencies, allLibraries }); if (!result.success || !result.component) { throw new Error(result.error?.message || 'Compilation failed'); } if (result.loadedLibraries && result.loadedLibraries.size > 0) { if (!this.runtimeContext.libraries) { this.runtimeContext.libraries = {}; } result.loadedLibraries.forEach((value, key) => { this.runtimeContext.libraries[key] = value; }); } const componentObject = result.component.factory(this.runtimeContext, undefined, {}); return componentObject; } async notifyRegistryUsageIfNeeded(spec, componentKey) { if (!spec.registry || !this.config.enableUsageTracking) { return; } const notificationKey = `${spec.registry}:${componentKey}`; if (this.registryNotifications.has(notificationKey)) { this.log(`Usage already notified for: ${spec.name}`); return; } try { this.log(`Notifying registry usage for: ${spec.name}`); this.registryNotifications.add(notificationKey); const cached = this.fetchCache.get(componentKey); if (cached) { cached.usageNotified = true; } } catch (error) { console.warn(`Failed to notify registry usage for ${componentKey}:`, error); } } async initializeGraphQLClient() { try { const provider = core_1.Metadata?.Provider; if (provider && provider.ExecuteGQL) { const { GraphQLComponentRegistryClient } = await Promise.resolve().then(() => __importStar(require('@memberjunction/graphql-dataprovider'))); this.graphQLClient = new GraphQLComponentRegistryClient(provider); this.log('GraphQL client initialized'); } } catch (error) { (0, core_1.LogError)(`Failed to initialize GraphQL client: ${error instanceof Error ? error.message : String(error)}`); } } isCacheValid(entry) { const age = Date.now() - entry.fetchedAt.getTime(); return age < (this.config.cacheTTL || 3600000); } async calculateHash(spec) { const content = JSON.stringify({ name: spec.name, version: spec.version, code: spec.code, libraries: spec.libraries }); let hash = 0; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return hash.toString(16); } getComponentKey(spec, options) { const registry = spec.registry || 'local'; const namespace = spec.namespace || options.defaultNamespace || 'Global'; const version = spec.version || options.defaultVersion || 'latest'; return `${registry}:${namespace}:${spec.name}:${version}`; } clearCache() { this.fetchCache.clear(); this.registryNotifications.clear(); this.loadingPromises.clear(); this.log('All caches cleared'); } getCacheStats() { return { fetchCacheSize: this.fetchCache.size, notificationsCount: this.registryNotifications.size, loadingCount: this.loadingPromises.size }; } log(message, data) { if (this.config.debug) { console.log(`🎯 [ComponentManager] ${message}`, data || ''); } } } exports.ComponentManager = ComponentManager; //# sourceMappingURL=component-manager.js.map