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.

540 lines โ€ข 23.6 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.ComponentRegistryService = void 0; const core_1 = require("@memberjunction/core"); const core_entities_1 = require("@memberjunction/core-entities"); let GraphQLComponentRegistryClient; class ComponentRegistryService { constructor(compiler, runtimeContext, debug = false, graphQLClient) { this.compiledComponentCache = new Map(); this.componentReferences = new Map(); this.componentEngine = core_entities_1.ComponentMetadataEngine.Instance; this.registryProviders = new Map(); this.debug = false; this.cachedProviderClient = null; this.compiler = compiler; this.runtimeContext = runtimeContext; this.debug = debug; this.graphQLClient = graphQLClient; } static getInstance(compiler, context, debug = false, graphQLClient) { if (!ComponentRegistryService.instance) { ComponentRegistryService.instance = new ComponentRegistryService(compiler, context, debug, graphQLClient); } return ComponentRegistryService.instance; } setGraphQLClient(client) { this.graphQLClient = client; if (this.debug) { console.log('โœ… GraphQL client configured for component registry'); } } async getGraphQLClient() { if (this.graphQLClient) { return this.graphQLClient; } if (this.cachedProviderClient) { return this.cachedProviderClient; } try { const provider = core_1.Metadata?.Provider; if (provider && provider.ExecuteGQL !== undefined) { if (!GraphQLComponentRegistryClient) { try { const graphqlModule = await Promise.resolve().then(() => __importStar(require('@memberjunction/graphql-dataprovider'))); GraphQLComponentRegistryClient = graphqlModule.GraphQLComponentRegistryClient; } catch (importError) { if (this.debug) { console.log('โš ๏ธ [ComponentRegistryService] @memberjunction/graphql-dataprovider not available'); } return null; } } if (GraphQLComponentRegistryClient) { try { const client = new GraphQLComponentRegistryClient(provider); this.cachedProviderClient = client; if (this.debug) { console.log('๐Ÿ“ก [ComponentRegistryService] Created GraphQL client from Metadata.Provider'); } return client; } catch (error) { if (this.debug) { console.log('โš ๏ธ [ComponentRegistryService] Failed to create GraphQL client:', error); } } } } } catch (error) { if (this.debug) { console.log('โš ๏ธ [ComponentRegistryService] Could not access Metadata.Provider:', error); } } return null; } async initialize(contextUser) { await this.componentEngine.Config(false, contextUser); } async calculateSpecHash(spec) { if (typeof crypto === 'undefined' || !crypto.subtle) { throw new Error('Web Crypto API not available. This typically happens when running in an insecure context. ' + 'Please use HTTPS or localhost for development. ' + 'Note: crypto.subtle is available in Node.js 15+ and all modern browsers on secure contexts.'); } const specString = JSON.stringify(spec); const encoder = new TextEncoder(); const data = encoder.encode(specString); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashHex; } async getCompiledComponent(componentId, referenceId, contextUser) { await this.initialize(contextUser); const component = this.componentEngine.Components.find((c) => c.ID === componentId); if (!component) { throw new Error(`Component not found: ${componentId}`); } const key = this.getComponentKey(component.Name, component.Namespace, component.Version, component.SourceRegistryID); if (this.compiledComponentCache.has(key)) { const cached = this.compiledComponentCache.get(key); cached.lastUsed = new Date(); cached.useCount++; if (referenceId) { this.addComponentReference(key, referenceId); } if (this.debug) { console.log(`โœ… Reusing compiled component from cache: ${key} (use count: ${cached.useCount})`); } return cached.component(this.runtimeContext); } if (this.debug) { console.log(`๐Ÿ”„ Loading and compiling component: ${key}`); } const spec = await this.getComponentSpec(componentId, contextUser); const allLibraries = this.componentEngine.ComponentLibraries || []; let compilationResult; try { compilationResult = await this.compiler.compile({ componentName: component.Name, componentCode: spec.code, libraries: spec.libraries, allLibraries }); } catch (compileEx) { console.error(`๐Ÿ”ด Error compiling component ${component.Name}`, compileEx); throw compileEx; } if (!compilationResult.success) { console.error(`๐Ÿ”ด Error compiling component ${component.Name}`, compilationResult, 'Code', spec.code); throw new Error(`Failed to compile component ${component.Name}: ${compilationResult.error}`); } const metadata = { name: component.Name, namespace: component.Namespace || '', version: component.Version, description: component.Description || '', title: component.Title || undefined, type: component.Type || undefined, status: component.Status || undefined, properties: spec.properties, events: spec.events, libraries: spec.libraries, dependencies: spec.dependencies, sourceRegistryID: component.SourceRegistryID, isLocal: !component.SourceRegistryID }; if (!compilationResult.component) { throw new Error(`Component compilation succeeded but no component returned`); } const compiledComponentFactory = compilationResult.component.factory; this.compiledComponentCache.set(key, { component: compiledComponentFactory, metadata, compiledAt: new Date(), lastUsed: new Date(), useCount: 1 }); if (referenceId) { this.addComponentReference(key, referenceId); } return compiledComponentFactory(this.runtimeContext); } async getCompiledComponentFromRegistry(registryName, namespace, name, version, referenceId, contextUser) { await this.initialize(contextUser); if (this.debug) { console.log(`๐ŸŒ [ComponentRegistryService] Fetching from external registry: ${registryName}/${namespace}/${name}@${version}`); } const registry = this.componentEngine.ComponentRegistries?.find(r => r.Name === registryName && r.Status === 'Active'); if (!registry) { throw new Error(`Registry not found or inactive: ${registryName}`); } if (this.debug) { console.log(`โœ… [ComponentRegistryService] Found registry: ${registry.Name} (ID: ${registry.ID})`); } const graphQLClient = await this.getGraphQLClient(); if (!graphQLClient) { throw new Error('GraphQL client not available for external registry fetching. No client provided and Metadata.Provider is not a GraphQLDataProvider.'); } const key = `external:${registryName}:${namespace}:${name}:${version}`; const cached = this.compiledComponentCache.get(key); try { const spec = await graphQLClient.GetRegistryComponent({ registryName: registry.Name, namespace, name, version, hash: cached?.specHash }); if (!spec && cached?.specHash) { if (this.debug) { console.log(`โ™ป๏ธ [ComponentRegistryService] Component not modified, using cached: ${key}`); } cached.lastUsed = new Date(); cached.useCount++; if (referenceId) { this.addComponentReference(key, referenceId); } return cached.component(this.runtimeContext); } if (!spec) { throw new Error(`Component not found in registry ${registryName}: ${namespace}/${name}@${version}`); } if (this.debug) { console.log(`โœ… [ComponentRegistryService] Fetched spec from external registry: ${spec.name}`); } const specHash = await this.calculateSpecHash(spec); if (cached && cached.specHash === specHash) { if (this.debug) { console.log(`โ™ป๏ธ [ComponentRegistryService] Using cached compilation for: ${key} (hash match)`); } cached.lastUsed = new Date(); cached.useCount++; if (referenceId) { this.addComponentReference(key, referenceId); } return cached.component(this.runtimeContext); } if (cached && this.debug) { console.log(`๐Ÿ”„ [ComponentRegistryService] Spec changed for: ${key}, recompiling (old hash: ${cached.specHash?.substring(0, 8)}..., new hash: ${specHash.substring(0, 8)}...)`); } const allLibraries = this.componentEngine.ComponentLibraries || []; const compilationResult = await this.compiler.compile({ componentName: spec.name, componentCode: spec.code || '', allLibraries: allLibraries }); if (!compilationResult.success || !compilationResult.component) { throw new Error(`Failed to compile component: ${compilationResult.error?.message || 'Unknown error'}`); } this.compiledComponentCache.set(key, { component: compilationResult.component.factory, metadata: { name: spec.name, namespace: spec.namespace || '', version: spec.version || '1.0.0', description: spec.description || '', type: spec.type, isLocal: false }, compiledAt: new Date(), lastUsed: new Date(), useCount: 1, specHash: specHash }); if (referenceId) { this.addComponentReference(key, referenceId); } if (this.debug) { console.log(`๐ŸŽฏ [ComponentRegistryService] Successfully compiled external component: ${spec.name}`); } return compilationResult.component.factory(this.runtimeContext); } catch (error) { console.error(`โŒ [ComponentRegistryService] Failed to fetch from external registry:`, error); throw error; } } async getComponentSpec(componentId, contextUser) { await this.initialize(contextUser); const component = this.componentEngine.Components.find((c) => c.ID === componentId); if (!component) { throw new Error(`Component not found: ${componentId}`); } if (!component.SourceRegistryID) { if (!component.Specification) { throw new Error(`Local component ${component.Name} has no specification`); } return JSON.parse(component.Specification); } if (component.Specification && component.LastSyncedAt) { if (this.debug) { console.log(`Using cached external component: ${component.Name} (synced: ${component.LastSyncedAt})`); } return JSON.parse(component.Specification); } const registry = this.componentEngine.ComponentRegistries?.find(r => r.ID === component.SourceRegistryID); if (!registry) { throw new Error(`Registry not found: ${component.SourceRegistryID}`); } let spec; if (this.graphQLClient) { if (this.debug) { console.log(`Fetching from registry via GraphQL: ${component.Name}`); } const result = await this.graphQLClient.GetRegistryComponent({ registryName: registry.Name, namespace: component.Namespace || '', name: component.Name, version: component.Version }); if (!result) { throw new Error(`Component not found in registry: ${component.Name}`); } spec = result; } else { spec = await this.fetchFromExternalRegistry(registry.URI || '', component.Name, component.Namespace || '', component.Version, this.getRegistryApiKey(registry.ID)); } await this.cacheExternalComponent(componentId, spec, contextUser); return spec; } async fetchFromExternalRegistry(uri, name, namespace, version, apiKey) { const url = `${uri}/components/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/${version}`; const headers = { 'Accept': 'application/json' }; if (apiKey) { headers['Authorization'] = `Bearer ${apiKey}`; } if (this.debug) { console.log(`Fetching from external registry: ${url}`); } const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`Registry fetch failed: ${response.status} ${response.statusText}`); } const spec = await response.json(); return spec; } async cacheExternalComponent(componentId, spec, contextUser) { const md = new core_1.Metadata(); const componentEntity = await md.GetEntityObject('MJ: Components', contextUser); if (!await componentEntity.Load(componentId)) { throw new Error(`Failed to load component entity: ${componentId}`); } componentEntity.Specification = JSON.stringify(spec); componentEntity.LastSyncedAt = new Date(); if (!componentEntity.ReplicatedAt) { componentEntity.ReplicatedAt = new Date(); } if (spec.name) { componentEntity.Name = spec.name; } if (spec.namespace) { componentEntity.Namespace = spec.namespace; } if (spec.version) { componentEntity.Version = spec.version; } if (spec.title) { componentEntity.Title = spec.title; } if (spec.description) { componentEntity.Description = spec.description; } if (spec.type) { const typeMap = { 'report': 'Report', 'dashboard': 'Dashboard', 'form': 'Form', 'table': 'Table', 'chart': 'Chart', 'navigation': 'Navigation', 'search': 'Search', 'widget': 'Widget', 'utility': 'Utility', 'other': 'Other' }; const mappedType = typeMap[spec.type.toLowerCase()]; if (mappedType) { componentEntity.Type = mappedType; } } if (spec.functionalRequirements) { componentEntity.FunctionalRequirements = spec.functionalRequirements; } if (spec.technicalDesign) { componentEntity.TechnicalDesign = spec.technicalDesign; } const result = await componentEntity.Save(); if (!result) { throw new Error(`Failed to save cached component: ${componentEntity.Name}\n${componentEntity.LatestResult?.CompleteMessage || 'Unknown error'}`); } if (this.debug) { console.log(`Cached external component: ${componentEntity.Name} at ${componentEntity.LastSyncedAt}`); } await this.componentEngine.Config(true, contextUser); } async loadDependencies(componentId, contextUser) { await this.initialize(contextUser); const dependencies = this.componentEngine.ComponentDependencies?.filter(d => d.ComponentID === componentId) || []; const result = []; for (const dep of dependencies) { const depComponent = this.componentEngine.Components.find((c) => c.ID === dep.DependencyComponentID); if (depComponent) { result.push({ name: depComponent.Name, namespace: depComponent.Namespace || '', version: depComponent.Version, isRequired: true, location: depComponent.SourceRegistryID ? 'registry' : 'embedded', sourceRegistryID: depComponent.SourceRegistryID }); } } return result; } async resolveDependencyTree(componentId, contextUser, visited = new Set()) { if (visited.has(componentId)) { return { componentId, circular: true }; } visited.add(componentId); await this.initialize(contextUser); const component = this.componentEngine.Components.find((c) => c.ID === componentId); if (!component) { return { componentId, dependencies: [] }; } const directDeps = await this.loadDependencies(componentId, contextUser); const dependencies = []; for (const dep of directDeps) { const depComponent = this.componentEngine.Components.find(c => c.Name.trim().toLowerCase() === dep.name.trim().toLowerCase() && c.Namespace?.trim().toLowerCase() === dep.namespace?.trim().toLowerCase()); if (depComponent) { const subTree = await this.resolveDependencyTree(depComponent.ID, contextUser, visited); dependencies.push(subTree); } } return { componentId, name: component.Name, namespace: component.Namespace || undefined, version: component.Version, dependencies, totalCount: dependencies.reduce((sum, d) => sum + (d.totalCount || 1), 1) }; } async getComponentsToLoad(rootComponentId, contextUser) { const tree = await this.resolveDependencyTree(rootComponentId, contextUser); const ordered = []; const processNode = (node) => { if (node.dependencies) { node.dependencies.forEach(processNode); } if (!ordered.includes(node.componentId)) { ordered.push(node.componentId); } }; processNode(tree); return ordered; } addComponentReference(componentKey, referenceId) { if (!this.componentReferences.has(componentKey)) { this.componentReferences.set(componentKey, new Set()); } this.componentReferences.get(componentKey).add(referenceId); } removeComponentReference(componentKey, referenceId) { const refs = this.componentReferences.get(componentKey); if (refs) { refs.delete(referenceId); if (refs.size === 0) { this.considerCacheEviction(componentKey); } } } considerCacheEviction(componentKey) { const cached = this.compiledComponentCache.get(componentKey); if (cached) { const timeSinceLastUse = Date.now() - cached.lastUsed.getTime(); const evictionThreshold = 5 * 60 * 1000; if (timeSinceLastUse > evictionThreshold) { if (this.debug) { console.log(`๐Ÿ—‘๏ธ Evicting unused component from cache: ${componentKey}`); } this.compiledComponentCache.delete(componentKey); } } } getRegistryApiKey(registryId) { const envKey = `REGISTRY_API_KEY_${registryId.replace(/-/g, '_').toUpperCase()}`; return process.env[envKey]; } getCacheStats() { let totalUseCount = 0; this.compiledComponentCache.forEach(cached => { totalUseCount += cached.useCount; }); return { compiledComponents: this.compiledComponentCache.size, totalUseCount, memoryEstimate: `~${(this.compiledComponentCache.size * 50)}KB` }; } clearCache() { if (this.debug) { console.log('๐Ÿงน Clearing all component caches'); } this.compiledComponentCache.clear(); this.componentReferences.clear(); } forceClearAll() { this.compiledComponentCache.clear(); this.componentReferences.clear(); console.log('๐Ÿงน Component cache force cleared'); } static reset() { if (ComponentRegistryService.instance) { ComponentRegistryService.instance.forceClearAll(); ComponentRegistryService.instance = null; } } getComponentKey(name, namespace, version, sourceRegistryId) { const registryPart = sourceRegistryId || 'local'; const namespacePart = namespace || 'global'; return `${registryPart}/${namespacePart}/${name}@${version}`; } } exports.ComponentRegistryService = ComponentRegistryService; ComponentRegistryService.instance = null; //# sourceMappingURL=component-registry-service.js.map