ixp-server
Version:
A comprehensive SDK for building Intent Exchange Protocol (IXP) servers with ease
266 lines • 8.86 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { watch } from 'chokidar';
/**
* Component Registry - Manages component definitions and their lifecycle
*/
export class ComponentRegistry {
constructor(config) {
this.components = new Map();
this.listeners = new Set();
if (typeof config === 'string') {
this.configPath = config;
this.loadFromFile(config);
}
else if (config && typeof config === 'object') {
this.loadFromObject(config);
}
}
/**
* Load components from file path
*/
loadFromFile(configPath) {
try {
const resolvedPath = path.resolve(configPath);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`Component configuration file not found: ${resolvedPath}`);
}
const fileContent = fs.readFileSync(resolvedPath, 'utf8');
const data = JSON.parse(fileContent);
if (!data.components || typeof data.components !== 'object') {
throw new Error('Invalid component configuration: missing or invalid "components" object');
}
this.loadFromObject(data.components);
console.log(`✅ Loaded ${this.components.size} components from ${configPath}`);
}
catch (error) {
console.error('❌ Failed to load components:', error);
throw error;
}
}
/**
* Load components from object
*/
loadFromObject(components) {
this.components.clear();
for (const [name, component] of Object.entries(components)) {
const componentWithName = { ...component, name };
this.validateComponent(componentWithName);
this.components.set(name, componentWithName);
}
}
/**
* Validate component definition
*/
validateComponent(component) {
if (!component.name || typeof component.name !== 'string') {
throw new Error('Component must have a valid name');
}
if (!component.framework || typeof component.framework !== 'string') {
throw new Error(`Component "${component.name}" must specify a framework`);
}
if (!component.remoteUrl || typeof component.remoteUrl !== 'string') {
throw new Error(`Component "${component.name}" must have a remoteUrl`);
}
// Validate URL format
try {
new URL(component.remoteUrl);
}
catch {
throw new Error(`Component "${component.name}" has invalid remoteUrl format`);
}
if (!component.exportName || typeof component.exportName !== 'string') {
throw new Error(`Component "${component.name}" must have an exportName`);
}
if (!component.propsSchema || typeof component.propsSchema !== 'object') {
throw new Error(`Component "${component.name}" must have a propsSchema`);
}
if (component.propsSchema.type !== 'object') {
throw new Error(`Component "${component.name}" propsSchema must be of type "object"`);
}
if (!component.version || typeof component.version !== 'string') {
throw new Error(`Component "${component.name}" must have a version`);
}
if (!Array.isArray(component.allowedOrigins)) {
throw new Error(`Component "${component.name}" must have allowedOrigins array`);
}
// Validate security policy
if (component.securityPolicy) {
if (typeof component.securityPolicy.allowEval !== 'boolean') {
throw new Error(`Component "${component.name}" securityPolicy.allowEval must be boolean`);
}
if (typeof component.securityPolicy.sandboxed !== 'boolean') {
throw new Error(`Component "${component.name}" securityPolicy.sandboxed must be boolean`);
}
}
}
/**
* Get all components
*/
getAll() {
return Array.from(this.components.values());
}
/**
* Get component by name
*/
get(name) {
return this.components.get(name);
}
/**
* Add new component
*/
add(component) {
this.validateComponent(component);
this.components.set(component.name, component);
this.notifyListeners();
}
/**
* Remove component by name
*/
remove(name) {
const removed = this.components.delete(name);
if (removed) {
this.notifyListeners();
}
return removed;
}
/**
* Reload components from file
*/
async reload() {
if (this.configPath) {
this.loadFromFile(this.configPath);
this.notifyListeners();
}
}
/**
* Enable file watching for hot reload
*/
enableFileWatching() {
if (!this.configPath) {
console.warn('Cannot enable file watching: no config path specified');
return;
}
if (this.watcher) {
console.warn('File watching is already enabled');
return;
}
this.watcher = watch(this.configPath, {
persistent: true,
ignoreInitial: true
});
this.watcher.on('change', async () => {
console.log('📁 Component configuration changed, reloading...');
try {
await this.reload();
console.log('✅ Components reloaded successfully');
}
catch (error) {
console.error('❌ Failed to reload components:', error);
}
});
console.log('👀 File watching enabled for component configuration');
}
/**
* Disable file watching
*/
disableFileWatching() {
if (this.watcher) {
this.watcher.close();
this.watcher = undefined;
console.log('👁️ File watching disabled');
}
}
/**
* Add change listener
*/
onChange(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/**
* Notify all listeners of changes
*/
notifyListeners() {
this.listeners.forEach(listener => {
try {
listener();
}
catch (error) {
console.error('Error in component registry listener:', error);
}
});
}
/**
* Get components by criteria
*/
findByCriteria(criteria) {
return this.getAll().filter(component => {
if (criteria.framework && component.framework !== criteria.framework) {
return false;
}
if (criteria.deprecated !== undefined && component.deprecated !== criteria.deprecated) {
return false;
}
if (criteria.sandboxed !== undefined && component.securityPolicy?.sandboxed !== criteria.sandboxed) {
return false;
}
return true;
});
}
/**
* Check if origin is allowed for component
*/
isOriginAllowed(componentName, origin) {
const component = this.get(componentName);
if (!component)
return false;
return component.allowedOrigins.includes(origin) || component.allowedOrigins.includes('*');
}
/**
* Get registry statistics
*/
getStats() {
const components = this.getAll();
const stats = {
total: components.length,
byFramework: {},
deprecated: 0,
sandboxed: 0,
averageBundleSize: '0KB'
};
let totalSize = 0;
let sizeCount = 0;
for (const component of components) {
// Framework stats
stats.byFramework[component.framework] = (stats.byFramework[component.framework] || 0) + 1;
// Deprecated count
if (component.deprecated)
stats.deprecated++;
// Sandboxed count
if (component.securityPolicy?.sandboxed)
stats.sandboxed++;
// Bundle size calculation
if (component.bundleSize) {
const sizeMatch = component.bundleSize.match(/(\d+)KB/);
if (sizeMatch && sizeMatch[1]) {
totalSize += parseInt(sizeMatch[1]);
sizeCount++;
}
}
}
if (sizeCount > 0) {
stats.averageBundleSize = `${Math.round(totalSize / sizeCount)}KB`;
}
return stats;
}
/**
* Cleanup resources
*/
destroy() {
this.disableFileWatching();
this.listeners.clear();
this.components.clear();
}
}
//# sourceMappingURL=ComponentRegistry.js.map