UNPKG

@sc4rfurryx/proteusjs

Version:

The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.

1,578 lines (1,567 loc) 603 kB
/*! * ProteusJS v2.0.0 * Shape-shifting responsive design that adapts like the sea god himself * (c) 2025 sc4rfurry * Released under the MIT License */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * ProteusJS Logger * Centralized logging system with configurable levels and production-safe output */ class Logger { constructor(config = {}) { this.levels = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }; this.config = { level: 'warn', prefix: 'ProteusJS', enableInProduction: false, enableTimestamps: false, enableStackTrace: false, ...config }; } static getInstance(config) { if (!Logger.instance) { Logger.instance = new Logger(config); } return Logger.instance; } static configure(config) { if (Logger.instance) { Logger.instance.config = { ...Logger.instance.config, ...config }; } else { Logger.instance = new Logger(config); } } shouldLog(level) { // Don't log in production unless explicitly enabled if (process.env['NODE_ENV'] === 'production' && !this.config.enableInProduction) { return level === 'error'; // Only errors in production } return this.levels[level] >= this.levels[this.config.level]; } formatMessage(level, message, ...args) { let formattedMessage = message; // Add prefix if (this.config.prefix) { formattedMessage = `${this.config.prefix}: ${formattedMessage}`; } // Add timestamp if (this.config.enableTimestamps) { const timestamp = new Date().toISOString(); formattedMessage = `[${timestamp}] ${formattedMessage}`; } // Add log level const levelPrefix = level.toUpperCase().padEnd(5); formattedMessage = `[${levelPrefix}] ${formattedMessage}`; return [formattedMessage, ...args]; } debug(message, ...args) { if (!this.shouldLog('debug')) return; const [formattedMessage, ...formattedArgs] = this.formatMessage('debug', message, ...args); console.debug(formattedMessage, ...formattedArgs); if (this.config.enableStackTrace) { console.trace(); } } info(message, ...args) { if (!this.shouldLog('info')) return; const [formattedMessage, ...formattedArgs] = this.formatMessage('info', message, ...args); console.info(formattedMessage, ...formattedArgs); } warn(message, ...args) { if (!this.shouldLog('warn')) return; const [formattedMessage, ...formattedArgs] = this.formatMessage('warn', message, ...args); console.warn(formattedMessage, ...formattedArgs); } error(message, error, ...args) { if (!this.shouldLog('error')) return; const [formattedMessage, ...formattedArgs] = this.formatMessage('error', message, ...args); if (error instanceof Error) { console.error(formattedMessage, error, ...formattedArgs); if (this.config.enableStackTrace && error.stack) { console.error('Stack trace:', error.stack); } } else if (error) { console.error(formattedMessage, error, ...formattedArgs); } else { console.error(formattedMessage, ...formattedArgs); } } group(label) { if (!this.shouldLog('info')) return; console.group(`${this.config.prefix}: ${label}`); } groupEnd() { if (!this.shouldLog('info')) return; console.groupEnd(); } time(label) { if (!this.shouldLog('debug')) return; console.time(`${this.config.prefix}: ${label}`); } timeEnd(label) { if (!this.shouldLog('debug')) return; console.timeEnd(`${this.config.prefix}: ${label}`); } setLevel(level) { this.config.level = level; } getLevel() { return this.config.level; } isEnabled(level) { return this.shouldLog(level); } } // Create default logger instance const logger = Logger.getInstance({ level: process.env['NODE_ENV'] === 'development' ? 'debug' : 'warn', prefix: 'ProteusJS', enableInProduction: false, enableTimestamps: process.env['NODE_ENV'] === 'development', enableStackTrace: false }); /** * Event System for ProteusJS * Handles all internal and external events */ class EventSystem { constructor() { this.listeners = new Map(); this.initialized = false; } /** * Initialize the event system */ init() { if (this.initialized) return; this.initialized = true; } /** * Add event listener */ on(eventType, callback) { if (!this.listeners.has(eventType)) { this.listeners.set(eventType, new Set()); } const callbacks = this.listeners.get(eventType); callbacks.add(callback); // Return unsubscribe function return () => { callbacks.delete(callback); if (callbacks.size === 0) { this.listeners.delete(eventType); } }; } /** * Add one-time event listener */ once(eventType, callback) { const unsubscribe = this.on(eventType, (event) => { callback(event); unsubscribe(); }); return unsubscribe; } /** * Remove event listener */ off(eventType, callback) { if (!callback) { // Remove all listeners for this event type this.listeners.delete(eventType); return; } const callbacks = this.listeners.get(eventType); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { this.listeners.delete(eventType); } } } /** * Emit event */ emit(eventType, detail, target) { const callbacks = this.listeners.get(eventType); if (!callbacks || callbacks.size === 0) return; const event = { type: eventType, target: target || document.documentElement, detail, timestamp: Date.now() }; // Execute callbacks callbacks.forEach(callback => { try { callback(event); } catch (error) { console.error(`ProteusJS: Error in event listener for "${eventType}":`, error); } }); } /** * Get all event types with listeners */ getEventTypes() { return Array.from(this.listeners.keys()); } /** * Get listener count for event type */ getListenerCount(eventType) { const callbacks = this.listeners.get(eventType); return callbacks ? callbacks.size : 0; } /** * Check if event type has listeners */ hasListeners(eventType) { return this.getListenerCount(eventType) > 0; } /** * Clear all listeners */ clear() { this.listeners.clear(); } /** * Destroy the event system */ destroy() { this.clear(); this.initialized = false; } /** * Get debug information */ getDebugInfo() { const info = {}; this.listeners.forEach((callbacks, eventType) => { info[eventType] = callbacks.size; }); return { initialized: this.initialized, totalEventTypes: this.listeners.size, listeners: info }; } } /** * Plugin System for ProteusJS * Enables extensibility and modular architecture */ class PluginSystem { constructor(proteus) { this.plugins = new Map(); this.installedPlugins = new Set(); this.initialized = false; this.proteus = proteus; } /** * Initialize the plugin system */ init() { if (this.initialized) return; this.initialized = true; } /** * Register a plugin */ register(plugin) { if (this.plugins.has(plugin.name)) { console.warn(`ProteusJS: Plugin "${plugin.name}" is already registered`); return this; } // Validate plugin if (!this.validatePlugin(plugin)) { throw new Error(`ProteusJS: Invalid plugin "${plugin.name}"`); } this.plugins.set(plugin.name, plugin); return this; } /** * Install a plugin */ install(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { throw new Error(`ProteusJS: Plugin "${pluginName}" not found`); } if (this.installedPlugins.has(pluginName)) { console.warn(`ProteusJS: Plugin "${pluginName}" is already installed`); return this; } // Check dependencies if (plugin.dependencies) { for (const dep of plugin.dependencies) { if (!this.installedPlugins.has(dep)) { throw new Error(`ProteusJS: Plugin "${pluginName}" requires dependency "${dep}" to be installed first`); } } } try { // Install the plugin plugin.install(this.proteus); this.installedPlugins.add(pluginName); // Emit plugin installed event this.proteus.getEventSystem().emit('pluginInstalled', { plugin: pluginName, version: plugin.version }); console.log(`ProteusJS: Plugin "${pluginName}" v${plugin.version} installed`); } catch (error) { console.error(`ProteusJS: Failed to install plugin "${pluginName}":`, error); throw error; } return this; } /** * Uninstall a plugin */ uninstall(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { console.warn(`ProteusJS: Plugin "${pluginName}" not found`); return this; } if (!this.installedPlugins.has(pluginName)) { console.warn(`ProteusJS: Plugin "${pluginName}" is not installed`); return this; } // Check if other plugins depend on this one const dependents = this.getDependents(pluginName); if (dependents.length > 0) { throw new Error(`ProteusJS: Cannot uninstall plugin "${pluginName}" because it's required by: ${dependents.join(', ')}`); } try { // Uninstall the plugin if (plugin.uninstall) { plugin.uninstall(this.proteus); } this.installedPlugins.delete(pluginName); // Emit plugin uninstalled event this.proteus.getEventSystem().emit('pluginUninstalled', { plugin: pluginName, version: plugin.version }); console.log(`ProteusJS: Plugin "${pluginName}" uninstalled`); } catch (error) { console.error(`ProteusJS: Failed to uninstall plugin "${pluginName}":`, error); throw error; } return this; } /** * Check if a plugin is installed */ isInstalled(pluginName) { return this.installedPlugins.has(pluginName); } /** * Get list of registered plugins */ getRegisteredPlugins() { return Array.from(this.plugins.keys()); } /** * Get list of installed plugins */ getInstalledPlugins() { return Array.from(this.installedPlugins); } /** * Get plugin information */ getPluginInfo(pluginName) { return this.plugins.get(pluginName); } /** * Install multiple plugins in dependency order */ installMany(pluginNames) { const sortedPlugins = this.sortByDependencies(pluginNames); for (const pluginName of sortedPlugins) { this.install(pluginName); } return this; } /** * Destroy the plugin system */ destroy() { // Uninstall all plugins in reverse dependency order const installedPlugins = Array.from(this.installedPlugins); const sortedPlugins = this.sortByDependencies(installedPlugins).reverse(); for (const pluginName of sortedPlugins) { try { this.uninstall(pluginName); } catch (error) { console.error(`ProteusJS: Error uninstalling plugin "${pluginName}":`, error); } } this.plugins.clear(); this.installedPlugins.clear(); this.initialized = false; } /** * Validate plugin structure */ validatePlugin(plugin) { if (!plugin.name || typeof plugin.name !== 'string') { console.error('ProteusJS: Plugin must have a valid name'); return false; } if (!plugin.version || typeof plugin.version !== 'string') { console.error('ProteusJS: Plugin must have a valid version'); return false; } if (!plugin.install || typeof plugin.install !== 'function') { console.error('ProteusJS: Plugin must have an install function'); return false; } return true; } /** * Get plugins that depend on the given plugin */ getDependents(pluginName) { const dependents = []; for (const [name, plugin] of this.plugins) { if (this.installedPlugins.has(name) && plugin.dependencies?.includes(pluginName)) { dependents.push(name); } } return dependents; } /** * Sort plugins by dependency order */ sortByDependencies(pluginNames) { const sorted = []; const visited = new Set(); const visiting = new Set(); const visit = (pluginName) => { if (visiting.has(pluginName)) { throw new Error(`ProteusJS: Circular dependency detected involving plugin "${pluginName}"`); } if (visited.has(pluginName)) return; visiting.add(pluginName); const plugin = this.plugins.get(pluginName); if (plugin?.dependencies) { for (const dep of plugin.dependencies) { if (pluginNames.includes(dep)) { visit(dep); } } } visiting.delete(pluginName); visited.add(pluginName); sorted.push(pluginName); }; for (const pluginName of pluginNames) { visit(pluginName); } return sorted; } } /** * Memory Manager for ProteusJS * Prevents memory leaks and manages resource cleanup */ class MemoryManager { constructor() { this.resources = new Map(); this.elementResources = new Map(); this.mutationObserver = null; this.cleanupInterval = null; this.isMonitoring = false; this.setupDOMObserver(); this.startPeriodicCleanup(); } /** * Register a resource for automatic cleanup */ register(resource) { const fullResource = { ...resource, timestamp: Date.now() }; this.resources.set(resource.id, fullResource); // Track element-specific resources if (resource.element) { if (!this.elementResources.has(resource.element)) { this.elementResources.set(resource.element, new Set()); } this.elementResources.get(resource.element).add(resource.id); } return resource.id; } /** * Unregister and cleanup a resource */ unregister(resourceId) { const resource = this.resources.get(resourceId); if (!resource) return false; try { resource.cleanup(); } catch (error) { console.error(`ProteusJS: Error cleaning up resource ${resourceId}:`, error); } this.resources.delete(resourceId); // Remove from element tracking if (resource.element) { const elementResources = this.elementResources.get(resource.element); if (elementResources) { elementResources.delete(resourceId); if (elementResources.size === 0) { this.elementResources.delete(resource.element); } } } return true; } /** * Cleanup all resources associated with an element */ cleanupElement(element) { const resourceIds = this.elementResources.get(element); if (!resourceIds) return 0; let cleanedCount = 0; resourceIds.forEach(resourceId => { if (this.unregister(resourceId)) { cleanedCount++; } }); return cleanedCount; } /** * Cleanup resources by type */ cleanupByType(type) { let cleanedCount = 0; const toCleanup = []; this.resources.forEach((resource, id) => { if (resource.type === type) { toCleanup.push(id); } }); toCleanup.forEach(id => { if (this.unregister(id)) { cleanedCount++; } }); return cleanedCount; } /** * Cleanup old resources based on age */ cleanupOldResources(maxAge = 300000) { let cleanedCount = 0; const now = Date.now(); const toCleanup = []; this.resources.forEach((resource, id) => { if (now - resource.timestamp > maxAge) { toCleanup.push(id); } }); toCleanup.forEach(id => { if (this.unregister(id)) { cleanedCount++; } }); return cleanedCount; } /** * Force garbage collection if available */ forceGarbageCollection() { if ('gc' in window && typeof window.gc === 'function') { try { window.gc(); } catch (error) { // Ignore errors - gc might not be available } } } /** * Get memory usage information */ getMemoryInfo() { const info = { managedResources: this.resources.size, trackedElements: this.elementResources.size, resourcesByType: {} }; // Count resources by type this.resources.forEach(resource => { info.resourcesByType[resource.type] = (info.resourcesByType[resource.type] || 0) + 1; }); // Add browser memory info if available if ('memory' in performance) { const memory = performance.memory; info.browserMemory = { usedJSHeapSize: memory.usedJSHeapSize, totalJSHeapSize: memory.totalJSHeapSize, jsHeapSizeLimit: memory.jsHeapSizeLimit }; } return info; } /** * Check for potential memory leaks */ detectLeaks() { const warnings = []; const now = Date.now(); // Check for too many resources if (this.resources.size > 1000) { warnings.push(`High number of managed resources: ${this.resources.size}`); } // Check for old resources let oldResourceCount = 0; this.resources.forEach(resource => { if (now - resource.timestamp > 600000) { // 10 minutes oldResourceCount++; } }); if (oldResourceCount > 50) { warnings.push(`Many old resources detected: ${oldResourceCount}`); } // Check for orphaned element resources let orphanedCount = 0; this.elementResources.forEach((resourceIds, element) => { if (!document.contains(element)) { orphanedCount += resourceIds.size; } }); if (orphanedCount > 0) { warnings.push(`Orphaned element resources detected: ${orphanedCount}`); } return warnings; } /** * Cleanup all resources and stop monitoring */ destroy() { // Cleanup all resources const resourceIds = Array.from(this.resources.keys()); resourceIds.forEach(id => this.unregister(id)); // Stop monitoring this.stopMonitoring(); // Clear maps this.resources.clear(); this.elementResources.clear(); } /** * Start monitoring for memory leaks */ startMonitoring() { if (this.isMonitoring) return; this.isMonitoring = true; } /** * Stop monitoring */ stopMonitoring() { if (!this.isMonitoring) return; this.isMonitoring = false; if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Setup DOM mutation observer to detect removed elements */ setupDOMObserver() { if (typeof MutationObserver === 'undefined') return; this.mutationObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { this.handleElementRemoval(node); } }); } }); }); this.mutationObserver.observe(document.body, { childList: true, subtree: true }); } /** * Handle element removal from DOM */ handleElementRemoval(element) { // Cleanup resources for this element this.cleanupElement(element); // Also check descendants const descendants = element.querySelectorAll('*'); descendants.forEach(descendant => { this.cleanupElement(descendant); }); } /** * Start periodic cleanup */ startPeriodicCleanup() { this.cleanupInterval = window.setInterval(() => { // Cleanup orphaned resources let orphanedCount = 0; this.elementResources.forEach((resourceIds, element) => { if (!document.contains(element)) { orphanedCount += this.cleanupElement(element); } }); // Cleanup very old resources const oldCount = this.cleanupOldResources(600000); // 10 minutes if (orphanedCount > 0 || oldCount > 0) { console.log(`ProteusJS: Cleaned up ${orphanedCount} orphaned and ${oldCount} old resources`); } }, 60000); // Run every minute } } /** * ResizeObserver Polyfill for ProteusJS * Provides ResizeObserver functionality for browsers that don't support it */ class ResizeObserverPolyfill { constructor(callback) { this.observedElements = new Map(); this.rafId = null; this.isObserving = false; this.callback = callback; } /** * Start observing an element for resize changes */ observe(element, options) { if (this.observedElements.has(element)) return; const rect = element.getBoundingClientRect(); this.observedElements.set(element, { lastWidth: rect.width, lastHeight: rect.height }); if (!this.isObserving) { this.startObserving(); } } /** * Stop observing an element */ unobserve(element) { this.observedElements.delete(element); if (this.observedElements.size === 0) { this.stopObserving(); } } /** * Disconnect all observations */ disconnect() { this.observedElements.clear(); this.stopObserving(); } /** * Start the polling mechanism */ startObserving() { if (this.isObserving) return; this.isObserving = true; this.checkForChanges(); } /** * Stop the polling mechanism */ stopObserving() { if (!this.isObserving) return; this.isObserving = false; if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; } } /** * Check for size changes in observed elements */ checkForChanges() { if (!this.isObserving) return; const changedEntries = []; this.observedElements.forEach((lastSize, element) => { // Check if element is still in DOM if (!document.contains(element)) { this.observedElements.delete(element); return; } const rect = element.getBoundingClientRect(); const currentWidth = rect.width; const currentHeight = rect.height; if (currentWidth !== lastSize.lastWidth || currentHeight !== lastSize.lastHeight) { // Update stored size this.observedElements.set(element, { lastWidth: currentWidth, lastHeight: currentHeight }); // Create entry const entry = { target: element, contentRect: this.createDOMRectReadOnly(rect), contentBoxSize: [{ inlineSize: currentWidth, blockSize: currentHeight }], borderBoxSize: [{ inlineSize: currentWidth, blockSize: currentHeight }] }; changedEntries.push(entry); } }); // Call callback if there are changes if (changedEntries.length > 0) { try { this.callback(changedEntries); } catch (error) { console.error('ResizeObserver callback error:', error); } } // Schedule next check if (this.isObserving) { this.rafId = requestAnimationFrame(() => this.checkForChanges()); } } /** * Create a DOMRectReadOnly-like object */ createDOMRectReadOnly(rect) { return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, toJSON: () => ({ x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left }) }; } } // Add static method for feature detection ResizeObserverPolyfill.isSupported = () => { return typeof ResizeObserver !== 'undefined'; }; /** * IntersectionObserver Polyfill for ProteusJS * Provides IntersectionObserver functionality for browsers that don't support it */ class IntersectionObserverPolyfill { constructor(callback, options) { this.observedElements = new Map(); this.rafId = null; this.isObserving = false; this.callback = callback; this.root = (options?.root instanceof Element ? options.root : null); this.rootMargin = options?.rootMargin || '0px'; this.thresholds = this.normalizeThresholds(options?.threshold); this.parsedRootMargin = this.parseRootMargin(this.rootMargin); } /** * Start observing an element */ observe(element) { if (this.observedElements.has(element)) return; this.observedElements.set(element, { lastRatio: 0, wasIntersecting: false }); if (!this.isObserving) { this.startObserving(); } } /** * Stop observing an element */ unobserve(element) { this.observedElements.delete(element); if (this.observedElements.size === 0) { this.stopObserving(); } } /** * Disconnect all observations */ disconnect() { this.observedElements.clear(); this.stopObserving(); } /** * Start the polling mechanism */ startObserving() { if (this.isObserving) return; this.isObserving = true; this.checkForIntersections(); } /** * Stop the polling mechanism */ stopObserving() { if (!this.isObserving) return; this.isObserving = false; if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; } } /** * Check for intersection changes */ checkForIntersections() { if (!this.isObserving) return; const changedEntries = []; const rootBounds = this.getRootBounds(); this.observedElements.forEach((lastState, element) => { // Check if element is still in DOM if (!document.contains(element)) { this.observedElements.delete(element); return; } const targetRect = element.getBoundingClientRect(); const intersectionRect = this.calculateIntersection(targetRect, rootBounds); const intersectionRatio = this.calculateIntersectionRatio(targetRect, intersectionRect); const isIntersecting = intersectionRatio > 0; // Check if we should trigger callback const shouldTrigger = this.shouldTriggerCallback(intersectionRatio, lastState.lastRatio, isIntersecting, lastState.wasIntersecting); if (shouldTrigger) { // Update stored state this.observedElements.set(element, { lastRatio: intersectionRatio, wasIntersecting: isIntersecting }); // Create entry const entry = { target: element, boundingClientRect: this.createDOMRectReadOnly(targetRect), intersectionRect: this.createDOMRectReadOnly(intersectionRect), rootBounds: rootBounds ? this.createDOMRectReadOnly(rootBounds) : null, intersectionRatio, isIntersecting, time: performance.now() }; changedEntries.push(entry); } }); // Call callback if there are changes if (changedEntries.length > 0) { try { this.callback(changedEntries); } catch (error) { console.error('IntersectionObserver callback error:', error); } } // Schedule next check if (this.isObserving) { this.rafId = requestAnimationFrame(() => this.checkForIntersections()); } } /** * Get root bounds with margin applied */ getRootBounds() { const rootElement = this.root || document.documentElement; const rect = rootElement.getBoundingClientRect(); return new DOMRect(rect.left - this.parsedRootMargin.left, rect.top - this.parsedRootMargin.top, rect.width + this.parsedRootMargin.left + this.parsedRootMargin.right, rect.height + this.parsedRootMargin.top + this.parsedRootMargin.bottom); } /** * Calculate intersection rectangle */ calculateIntersection(targetRect, rootBounds) { const left = Math.max(targetRect.left, rootBounds.left); const top = Math.max(targetRect.top, rootBounds.top); const right = Math.min(targetRect.right, rootBounds.right); const bottom = Math.min(targetRect.bottom, rootBounds.bottom); const width = Math.max(0, right - left); const height = Math.max(0, bottom - top); return new DOMRect(left, top, width, height); } /** * Calculate intersection ratio */ calculateIntersectionRatio(targetRect, intersectionRect) { const targetArea = targetRect.width * targetRect.height; if (targetArea === 0) return 0; const intersectionArea = intersectionRect.width * intersectionRect.height; return intersectionArea / targetArea; } /** * Check if callback should be triggered based on thresholds */ shouldTriggerCallback(currentRatio, lastRatio, isIntersecting, wasIntersecting) { // Always trigger on first observation if (lastRatio === 0 && !wasIntersecting) { return true; } // Check if intersection state changed if (isIntersecting !== wasIntersecting) { return true; } // Check if any threshold was crossed for (const threshold of this.thresholds) { if ((lastRatio < threshold && currentRatio >= threshold) || (lastRatio > threshold && currentRatio <= threshold)) { return true; } } return false; } /** * Normalize threshold values */ normalizeThresholds(threshold) { if (threshold === undefined) return [0]; if (typeof threshold === 'number') return [threshold]; return threshold.slice().sort((a, b) => a - b); } /** * Parse root margin string */ parseRootMargin(margin) { const values = margin.split(/\s+/).map(value => { const num = parseFloat(value); return value.endsWith('%') ? (num / 100) * window.innerHeight : num; }); switch (values.length) { case 1: return { top: values[0], right: values[0], bottom: values[0], left: values[0] }; case 2: return { top: values[0], right: values[1], bottom: values[0], left: values[1] }; case 3: return { top: values[0], right: values[1], bottom: values[2], left: values[1] }; case 4: return { top: values[0], right: values[1], bottom: values[2], left: values[3] }; default: return { top: 0, right: 0, bottom: 0, left: 0 }; } } /** * Create a DOMRectReadOnly-like object */ createDOMRectReadOnly(rect) { return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, toJSON: () => ({ x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left }) }; } } // Add static method for feature detection IntersectionObserverPolyfill.isSupported = () => { return typeof IntersectionObserver !== 'undefined'; }; /** * Observer Manager for ProteusJS * Manages ResizeObserver and IntersectionObserver instances efficiently */ class ObserverManager { constructor() { this.resizeObservers = new Map(); this.intersectionObservers = new Map(); this.resizeEntries = new Map(); this.intersectionEntries = new Map(); this.isPolyfillMode = false; this.checkPolyfillNeeds(); } /** * Observe element for resize changes */ observeResize(element, callback, options) { const observerKey = this.getResizeObserverKey(options); let observer = this.resizeObservers.get(observerKey); if (!observer) { observer = this.createResizeObserver(options); this.resizeObservers.set(observerKey, observer); } // Store entry for cleanup const entry = { element, callback: callback, ...(options && { options }) }; this.resizeEntries.set(element, entry); // Start observing observer.observe(element, options); // Return unobserve function return () => this.unobserveResize(element); } /** * Observe element for intersection changes */ observeIntersection(element, callback, options) { const observerKey = this.getIntersectionObserverKey(options); let observer = this.intersectionObservers.get(observerKey); if (!observer) { observer = this.createIntersectionObserver(callback, options); this.intersectionObservers.set(observerKey, observer); } // Store entry for cleanup const entry = { element, callback: callback, ...(options && { options }) }; this.intersectionEntries.set(element, entry); // Start observing observer.observe(element); // Return unobserve function return () => this.unobserveIntersection(element); } /** * Stop observing element for resize changes */ unobserveResize(element) { const entry = this.resizeEntries.get(element); if (!entry) return; const observerKey = this.getResizeObserverKey(entry.options); const observer = this.resizeObservers.get(observerKey); if (observer) { observer.unobserve(element); } this.resizeEntries.delete(element); this.cleanupResizeObserver(observerKey); } /** * Stop observing element for intersection changes */ unobserveIntersection(element) { const entry = this.intersectionEntries.get(element); if (!entry) return; const observerKey = this.getIntersectionObserverKey(entry.options); const observer = this.intersectionObservers.get(observerKey); if (observer) { observer.unobserve(element); } this.intersectionEntries.delete(element); this.cleanupIntersectionObserver(observerKey); } /** * Get total number of observed elements */ getObservedElementCount() { return this.resizeEntries.size + this.intersectionEntries.size; } /** * Get number of active observers */ getObserverCount() { return this.resizeObservers.size + this.intersectionObservers.size; } /** * Check if element is being observed for resize */ isObservingResize(element) { return this.resizeEntries.has(element); } /** * Check if element is being observed for intersection */ isObservingIntersection(element) { return this.intersectionEntries.has(element); } /** * Disconnect all observers and clean up */ destroy() { // Disconnect all resize observers this.resizeObservers.forEach(observer => observer.disconnect()); this.resizeObservers.clear(); this.resizeEntries.clear(); // Disconnect all intersection observers this.intersectionObservers.forEach(observer => observer.disconnect()); this.intersectionObservers.clear(); this.intersectionEntries.clear(); } /** * Get debug information */ getDebugInfo() { return { isPolyfillMode: this.isPolyfillMode, resizeObservers: this.resizeObservers.size, intersectionObservers: this.intersectionObservers.size, resizeEntries: this.resizeEntries.size, intersectionEntries: this.intersectionEntries.size, totalObservedElements: this.getObservedElementCount(), totalObservers: this.getObserverCount() }; } /** * Check if polyfills are needed and set up accordingly */ checkPolyfillNeeds() { if (typeof ResizeObserver === 'undefined') { this.setupResizeObserverPolyfill(); this.isPolyfillMode = true; } if (typeof IntersectionObserver === 'undefined') { this.setupIntersectionObserverPolyfill(); this.isPolyfillMode = true; } } /** * Set up ResizeObserver polyfill */ setupResizeObserverPolyfill() { globalThis.ResizeObserver = ResizeObserverPolyfill; } /** * Set up IntersectionObserver polyfill */ setupIntersectionObserverPolyfill() { globalThis.IntersectionObserver = IntersectionObserverPolyfill; } /** * Create a new ResizeObserver instance */ createResizeObserver(_options) { return new ResizeObserver((entries) => { entries.forEach(entry => { const storedEntry = this.resizeEntries.get(entry.target); if (storedEntry) { storedEntry.callback(entry); } }); }); } /** * Create a new IntersectionObserver instance */ createIntersectionObserver(_callback, options) { return new IntersectionObserver((entries) => { entries.forEach(entry => { const storedEntry = this.intersectionEntries.get(entry.target); if (storedEntry) { storedEntry.callback(entry); } }); }, options); } /** * Generate key for ResizeObserver based on options */ getResizeObserverKey(options) { if (!options) return 'default'; return `box:${options.box || 'content-box'}`; } /** * Generate key for IntersectionObserver based on options */ getIntersectionObserverKey(options) { if (!options) return 'default'; const root = options.root ? 'custom' : 'viewport'; const rootMargin = options.rootMargin || '0px'; const threshold = Array.isArray(options.threshold) ? options.threshold.join(',') : (options.threshold || 0).toString(); return `${root}:${rootMargin}:${threshold}`; } /** * Clean up ResizeObserver if no longer needed */ cleanupResizeObserver(observerKey) { const hasElements = Array.from(this.resizeEntries.values()).some(entry => this.getResizeObserverKey(entry.options) === observerKey); if (!hasElements) { const observer = this.resizeObservers.get(observerKey); if (observer) { observer.disconnect(); this.resizeObservers.delete(observerKey); } } } /** * Clean up IntersectionObserver if no longer needed */ cleanupIntersectionObserver(observerKey) { const hasElements = Array.from(this.intersectionEntries.values()).some(entry => this.getIntersectionObserverKey(entry.options) === observerKey); if (!hasElements) { const observer = this.intersectionObservers.get(observerKey); if (observer) { observer.disconnect(); this.intersectionObservers.delete(observerKey); } } } } /** * Debounce and throttle utilities for ProteusJS * Optimizes performance by controlling function execution frequency */ /** * Debounce function execution */ function debounce(func, wait, options = {}) { const { leading = false, trailing = true, maxWait } = options; let timeoutId = null; let lastCallTime; let lastInvokeTime = 0; let lastArgs; let lastThis; let result; function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = undefined; lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { lastInvokeTime = time; timeoutId = window.setTimeout(timerExpired, wait); return leading ? invokeFunc(time) : result; } function remainingWait(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return maxWait !== undefined ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; return (lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxWait !== undefined && timeSinceLastInvoke >= maxWait)); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } timeoutId = window.setTimeout(timerExpired, remainingWait(time)); return undefined; } function trailingEdge(time) { timeoutId = null; if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = undefined; lastThis = undefined; return result; } function cancel() { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; } lastInvokeTime = 0; lastArgs = undefined; lastCallTime = undefined; lastThis = undefined; } function flush() { return timeoutId === null ? result : trailingEdge(Date.now()); } function debounced(...args) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; lastThis = this; lastCallTime = time; if (isInvoking) { if (timeoutId === null) { return leadingEdge(lastCallTime); } if (maxWait !== undefined) { timeoutId = window.setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timeoutId === null) { timeoutId = window.setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; } /** * Performance utilities for ProteusJS * Provides timing, measurement, and optimization tools */ class PerformanceTracker { constructor(budget) { this.marks = new Map(); this.measurements = []; this.warningThreshold = 0.8; // 80% of budget this.budget = { responseTime: 60, // 60ms default frameRate: 60, // 60fps default memoryUsage: 100, // 100MB default ...budget }; } /** * Start timing a performance mark */ mark(name, metadata) { const mark = { name, startTime: performance.now(), ...(metadata && { metadata }) }; this.marks.set(name, mark); // Use Performance API if available if (typeof performance.mark === 'function') { performance.mark(`proteus-${name}-start`); } } /** * End timing a performance mark */ measure(name) { const mark = this.marks.get(name); if (!mark) { console.warn(`ProteusJS: Performance mark "${name}" not found`); return null; } const endTime = performance.now(); const duration = endTime - mark.startTime; const measurement = { ...mark, endTime, duration }; this.measurements.push(measurement); this.marks.delete(name); // Use Performance API if available if (typeof performance.mark === 'function' && typeof performance.measure === 'function') { performance.mark(`proteus-${name}-end`); performance.measure(`proteus-${name}`, `proteus-${name}-start`, `proteus-${name}-end`); } // Check against budget this.checkBudget(measurement); return measurement; } /** * Get all measurements */ getMeasurements() { return [...this.measurements]; } /** * Get measurements by name pattern */ getMeasurementsByPattern(pattern) { return this.measurements.filter(m => pattern.test(m.name)); } /** * Get average duration for measurements with the same name */ getAverageDuration(name) { const matching = this.measurements.filter(m => m.name === name && m.duration !== undefined); if (matching.length === 0) return 0; const total = matching.reduce((sum, m) => sum + (m.duration || 0), 0); return total / matching.length; } /** * Get performance statistics */ getStats() { const stats = { totalMeasurements: this.measurements.length, activeMeasurements: this.marks.size, budget: this.budget, violations: this.getBudgetViolations() }; // Group by name const byName = {}; this.measurements.forEach(m => {