@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
JavaScript
;
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