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