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.

407 lines (349 loc) 10.8 kB
/** * @fileoverview Platform-agnostic component registry for managing compiled React components. * Provides storage, retrieval, and lifecycle management for components with namespace support. * @module @memberjunction/react-runtime/registry */ import { RegistryEntry, ComponentMetadata, RegistryConfig } from '../types'; import { resourceManager } from '../utilities/resource-manager'; /** * Default registry configuration */ const DEFAULT_REGISTRY_CONFIG: RegistryConfig = { maxComponents: 1000, cleanupInterval: 60000, // 1 minute useLRU: true, enableNamespaces: true }; /** * Platform-agnostic component registry. * Manages compiled React components with namespace isolation and lifecycle management. */ export class ComponentRegistry { private registry: Map<string, RegistryEntry>; private config: RegistryConfig; private cleanupTimer?: NodeJS.Timeout | number; private registryId: string; /** * Creates a new ComponentRegistry instance * @param config - Optional registry configuration */ constructor(config?: Partial<RegistryConfig>) { this.config = { ...DEFAULT_REGISTRY_CONFIG, ...config }; this.registry = new Map(); this.registryId = `component-registry-${Date.now()}`; // Start cleanup timer if configured if (this.config.cleanupInterval > 0) { this.startCleanupTimer(); } } /** * Registers a compiled component * @param name - Component name * @param component - Compiled component * @param namespace - Component namespace (default: 'Global') * @param version - Component version (default: 'v1') * @param tags - Optional tags for categorization * @returns The registered component's metadata */ register( name: string, component: any, namespace: string = 'Global', version: string = 'v1', tags?: string[] ): ComponentMetadata { const id = this.generateRegistryKey(name, namespace, version); // Create metadata const metadata: ComponentMetadata = { id, name, version, namespace, registeredAt: new Date(), tags }; // Create registry entry const entry: RegistryEntry = { component, metadata, lastAccessed: new Date(), refCount: 0 }; // Check capacity if (this.registry.size >= this.config.maxComponents && this.config.useLRU) { this.evictLRU(); } // Store in registry this.registry.set(id, entry); return metadata; } /** * Gets a component from the registry * @param name - Component name * @param namespace - Component namespace * @param version - Component version * @returns The component if found, undefined otherwise */ get(name: string, namespace: string = 'Global', version?: string): any { const id = version ? this.generateRegistryKey(name, namespace, version) : this.findLatestVersion(name, namespace); if (!id) return undefined; const entry = this.registry.get(id); if (entry) { // Update access time and increment ref count entry.lastAccessed = new Date(); entry.refCount++; return entry.component; } return undefined; } /** * Checks if a component exists in the registry * @param name - Component name * @param namespace - Component namespace * @param version - Component version * @returns true if the component exists */ has(name: string, namespace: string = 'Global', version?: string): boolean { const id = version ? this.generateRegistryKey(name, namespace, version) : this.findLatestVersion(name, namespace); return id ? this.registry.has(id) : false; } /** * Removes a component from the registry * @param name - Component name * @param namespace - Component namespace * @param version - Component version * @returns true if the component was removed */ unregister(name: string, namespace: string = 'Global', version?: string): boolean { const id = version ? this.generateRegistryKey(name, namespace, version) : this.findLatestVersion(name, namespace); if (!id) return false; return this.registry.delete(id); } /** * Gets all components in a namespace * @param namespace - Namespace to query * @returns Array of components in the namespace */ getNamespace(namespace: string): ComponentMetadata[] { const components: ComponentMetadata[] = []; for (const entry of this.registry.values()) { if (entry.metadata.namespace === namespace) { components.push(entry.metadata); } } return components; } /** * Gets all components in a namespace and version as a map * @param namespace - Namespace to query (default: 'Global') * @param version - Version to query (default: 'v1') * @returns Object mapping component names to components */ getAll(namespace: string = 'Global', version: string = 'v1'): Record<string, any> { const components: Record<string, any> = {}; for (const entry of this.registry.values()) { if (entry.metadata.namespace === namespace && entry.metadata.version === version) { components[entry.metadata.name] = entry.component; } } return components; } /** * Gets all registered namespaces * @returns Array of unique namespace names */ getNamespaces(): string[] { const namespaces = new Set<string>(); for (const entry of this.registry.values()) { namespaces.add(entry.metadata.namespace); } return Array.from(namespaces); } /** * Gets components by tags * @param tags - Tags to search for * @returns Array of components matching any of the tags */ getByTags(tags: string[]): ComponentMetadata[] { const components: ComponentMetadata[] = []; for (const entry of this.registry.values()) { if (entry.metadata.tags?.some(tag => tags.includes(tag))) { components.push(entry.metadata); } } return components; } /** * Decrements reference count for a component * @param name - Component name * @param namespace - Component namespace * @param version - Component version */ release(name: string, namespace: string = 'Global', version?: string): void { const id = version ? this.generateRegistryKey(name, namespace, version) : this.findLatestVersion(name, namespace); if (!id) return; const entry = this.registry.get(id); if (entry && entry.refCount > 0) { entry.refCount--; } } /** * Clears all components from the registry */ clear(): void { this.registry.clear(); } /** * Gets the current size of the registry * @returns Number of registered components */ size(): number { return this.registry.size; } /** * Performs cleanup of unused components * @param force - Force cleanup regardless of reference count * @returns Number of components removed */ cleanup(force: boolean = false): number { const toRemove: string[] = []; const now = Date.now(); for (const [id, entry] of this.registry) { // Remove if no references and hasn't been accessed recently const timeSinceAccess = now - entry.lastAccessed.getTime(); const isUnused = entry.refCount === 0 && timeSinceAccess > this.config.cleanupInterval; if (force || isUnused) { toRemove.push(id); } } for (const id of toRemove) { this.registry.delete(id); } return toRemove.length; } /** * Gets registry statistics * @returns Object containing registry stats */ getStats(): { totalComponents: number; namespaces: number; totalRefCount: number; oldestComponent?: Date; newestComponent?: Date; } { let totalRefCount = 0; let oldest: Date | undefined; let newest: Date | undefined; for (const entry of this.registry.values()) { totalRefCount += entry.refCount; if (!oldest || entry.metadata.registeredAt < oldest) { oldest = entry.metadata.registeredAt; } if (!newest || entry.metadata.registeredAt > newest) { newest = entry.metadata.registeredAt; } } return { totalComponents: this.registry.size, namespaces: this.getNamespaces().length, totalRefCount, oldestComponent: oldest, newestComponent: newest }; } /** * Destroys the registry and cleans up resources */ destroy(): void { this.stopCleanupTimer(); this.clear(); // Clean up any resources associated with this registry resourceManager.cleanupComponent(this.registryId); } /** * Generates a unique registry key * @param name - Component name * @param namespace - Component namespace * @param version - Component version * @returns Registry key */ private generateRegistryKey(name: string, namespace: string, version: string): string { if (this.config.enableNamespaces) { return `${namespace}::${name}@${version}`; } return `${name}@${version}`; } /** * Finds the latest version of a component * @param name - Component name * @param namespace - Component namespace * @returns Registry key of latest version or undefined */ private findLatestVersion(name: string, namespace: string): string | undefined { let latestKey: string | undefined; let latestDate: Date | undefined; for (const [key, entry] of this.registry) { if (entry.metadata.name === name && entry.metadata.namespace === namespace) { if (!latestDate || entry.metadata.registeredAt > latestDate) { latestDate = entry.metadata.registeredAt; latestKey = key; } } } return latestKey; } /** * Evicts the least recently used component */ private evictLRU(): void { let lruKey: string | undefined; let lruTime: Date | undefined; for (const [key, entry] of this.registry) { // Skip components with active references if (entry.refCount > 0) continue; if (!lruTime || entry.lastAccessed < lruTime) { lruTime = entry.lastAccessed; lruKey = key; } } if (lruKey) { this.registry.delete(lruKey); } } /** * Starts the automatic cleanup timer */ private startCleanupTimer(): void { this.cleanupTimer = resourceManager.setInterval( this.registryId, () => { this.cleanup(); }, this.config.cleanupInterval, { purpose: 'component-registry-cleanup' } ); } /** * Stops the automatic cleanup timer */ private stopCleanupTimer(): void { if (this.cleanupTimer) { resourceManager.clearInterval(this.registryId, this.cleanupTimer as number); this.cleanupTimer = undefined; } } }