UNPKG

@zenithcore/core

Version:

Core functionality for ZenithKernel framework

599 lines (505 loc) 17.4 kB
/** * Enhanced SSR & Hydration implementation */ import { createAsyncRenderer, RenderPriority } from '../AsyncRenderer'; /** * Types of hydration strategies */ export enum HydrationStrategy { // Hydrate everything at once FULL = 'full', // Hydrate only visible components first, then the rest PROGRESSIVE = 'progressive', // Only hydrate components when they become visible LAZY = 'lazy', // Only hydrate on user interaction INTERACTION = 'interaction', // Hydrate critical components first, then progressively the rest CRITICAL_FIRST = 'critical-first', // Server controls hydration timing via special directives SERVER_CONTROLLED = 'server-controlled' } /** * Interface for hydration options */ export interface HydrationOptions { strategy?: HydrationStrategy; rootElement?: HTMLElement; timeout?: number; priority?: RenderPriority; onComplete?: () => void; onError?: (error: Error) => void; debug?: boolean; idPrefix?: string; } /** * Hydration state for tracking progress */ interface HydrationState { isHydrating: boolean; completed: Set<string>; failed: Set<string>; pending: Set<string>; startTime: number; } /** * Enhanced hydration controller for SSR */ export class HydrationController { private options: HydrationOptions; private state: HydrationState; private asyncRenderer = createAsyncRenderer(); private intersectionObserver: IntersectionObserver | null = null; private mutationObserver: MutationObserver | null = null; private eventListeners: Map<string, [string, EventListener][]> = new Map(); constructor(options: HydrationOptions = {}) { this.options = { strategy: HydrationStrategy.PROGRESSIVE, rootElement: typeof document !== 'undefined' ? document.documentElement : undefined, timeout: 10000, priority: RenderPriority.NORMAL, debug: false, idPrefix: 'hydrate-', ...options }; this.state = { isHydrating: false, completed: new Set(), failed: new Set(), pending: new Set(), startTime: 0 }; } /** * Start the hydration process */ start(): void { if (this.state.isHydrating || typeof document === 'undefined') { return; } this.state.isHydrating = true; this.state.startTime = performance.now(); // Find all elements with hydration markers const rootElement = this.options.rootElement || document.documentElement; const hydrateElements = rootElement.querySelectorAll('[data-hydrate]'); // Setup hydration based on strategy switch (this.options.strategy) { case HydrationStrategy.FULL: this.hydrateAll(hydrateElements); break; case HydrationStrategy.PROGRESSIVE: this.hydrateProgressive(hydrateElements); break; case HydrationStrategy.LAZY: this.hydrateLazy(hydrateElements); break; case HydrationStrategy.INTERACTION: this.hydrateOnInteraction(hydrateElements); break; case HydrationStrategy.CRITICAL_FIRST: this.hydrateCriticalFirst(hydrateElements); break; case HydrationStrategy.SERVER_CONTROLLED: this.hydrateServerControlled(hydrateElements); break; default: this.hydrateProgressive(hydrateElements); } } /** * Hydrate all elements at once */ private hydrateAll(elements: NodeListOf<Element>): void { elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element as HTMLElement, id); } }); // Start a timeout to check if hydration completed setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, forcing completion'); this.complete(); } }, this.options.timeout); } /** * Hydrate visible elements first, then others */ private hydrateProgressive(elements: NodeListOf<Element>): void { const visibleElements: HTMLElement[] = []; const hiddenElements: HTMLElement[] = []; elements.forEach(element => { const el = element as HTMLElement; if (this.isElementVisible(el)) { visibleElements.push(el); } else { hiddenElements.push(el); } }); // Hydrate visible elements with higher priority visibleElements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id, RenderPriority.HIGH); } }); // Hydrate hidden elements with lower priority hiddenElements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id, RenderPriority.LOW); } }); // Start a timeout to check if hydration completed setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, forcing completion'); this.complete(); } }, this.options.timeout); } /** * Hydrate elements only when they become visible */ private hydrateLazy(elements: NodeListOf<Element>): void { // Set up intersection observer this.intersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const element = entry.target as HTMLElement; const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id); // Stop observing this element this.intersectionObserver?.unobserve(element); } } }); }, { rootMargin: '100px' }); // 100px margin to start hydrating just before it becomes visible // Start observing all elements elements.forEach(element => { this.intersectionObserver?.observe(element); const id = element.getAttribute('data-hydrate'); if (id) { this.state.pending.add(id); } }); // Start a timeout to ensure everything eventually gets hydrated setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, hydrating remaining elements'); // Hydrate all remaining elements elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id && !this.state.completed.has(id)) { this.hydrateElement(element as HTMLElement, id); this.intersectionObserver?.unobserve(element); } }); // Complete hydration setTimeout(() => this.complete(), 500); } }, this.options.timeout); } /** * Hydrate elements on user interaction */ private hydrateOnInteraction(elements: NodeListOf<Element>): void { const interactionEvents = ['mouseenter', 'focus', 'touchstart', 'click']; elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (!id) return; this.state.pending.add(id); const el = element as HTMLElement; const listeners: [string, EventListener][] = []; // Create one listener for each event interactionEvents.forEach(eventName => { const listener = () => { this.hydrateElement(el, id); // Remove all event listeners after hydration listeners.forEach(([evt, listener]) => { el.removeEventListener(evt, listener); }); }; el.addEventListener(eventName, listener); listeners.push([eventName, listener]); }); this.eventListeners.set(id, listeners); }); // Start a timeout to ensure everything eventually gets hydrated setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, hydrating remaining elements'); // Hydrate all remaining elements elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id && !this.state.completed.has(id)) { this.hydrateElement(element as HTMLElement, id); // Remove associated event listeners const listeners = this.eventListeners.get(id); if (listeners) { listeners.forEach(([evt, listener]) => { element.removeEventListener(evt, listener); }); this.eventListeners.delete(id); } } }); // Complete hydration setTimeout(() => this.complete(), 500); } }, this.options.timeout); } /** * Hydrate critical elements first, then others progressively */ private hydrateCriticalFirst(elements: NodeListOf<Element>): void { const critical: HTMLElement[] = []; const important: HTMLElement[] = []; const normal: HTMLElement[] = []; const deferred: HTMLElement[] = []; elements.forEach(element => { const el = element as HTMLElement; const priority = el.getAttribute('data-hydrate-priority'); switch (priority) { case 'critical': critical.push(el); break; case 'important': important.push(el); break; case 'deferred': deferred.push(el); break; default: normal.push(el); break; } }); // Schedule hydration in priority order const scheduleGroup = (elements: HTMLElement[], priority: RenderPriority) => { elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id, priority); } }); }; // Hydrate in order of priority scheduleGroup(critical, RenderPriority.IMMEDIATE); setTimeout(() => { scheduleGroup(important, RenderPriority.HIGH); setTimeout(() => { scheduleGroup(normal, RenderPriority.NORMAL); setTimeout(() => { scheduleGroup(deferred, RenderPriority.IDLE); }, 200); }, 100); }, 20); // Start a timeout to check if hydration completed setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, forcing completion'); this.complete(); } }, this.options.timeout); } /** * Server-controlled hydration based on special directives */ private hydrateServerControlled(elements: NodeListOf<Element>): void { const immediateElements: HTMLElement[] = []; const delayedElements: Map<number, HTMLElement[]> = new Map(); elements.forEach(element => { const el = element as HTMLElement; const id = el.getAttribute('data-hydrate'); if (!id) return; const delay = el.getAttribute('data-hydrate-delay'); if (!delay) { immediateElements.push(el); } else { const delayMs = parseInt(delay, 10); if (isNaN(delayMs)) { immediateElements.push(el); } else { if (!delayedElements.has(delayMs)) { delayedElements.set(delayMs, []); } delayedElements.get(delayMs)?.push(el); } } }); // Hydrate immediate elements first immediateElements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id, RenderPriority.HIGH); } }); // Schedule delayed elements delayedElements.forEach((elements, delay) => { setTimeout(() => { elements.forEach(element => { const id = element.getAttribute('data-hydrate'); if (id) { this.hydrateElement(element, id); } }); }, delay); }); // Start a timeout to check if hydration completed setTimeout(() => { if (this.state.pending.size > 0) { this.log('Hydration timeout reached, forcing completion'); this.complete(); } }, this.options.timeout); } /** * Hydrate a specific element */ private hydrateElement( element: HTMLElement, id: string, priority: RenderPriority = this.options.priority || RenderPriority.NORMAL ): void { if (this.state.completed.has(id) || this.state.failed.has(id)) { return; } this.state.pending.add(id); this.log(`Hydrating element: ${id}`); // Get hydration function by ID const hydrateFunction = this.getHydrationFunction(id); if (!hydrateFunction) { this.log(`No hydration function found for ${id}`, 'error'); this.state.pending.delete(id); this.state.failed.add(id); return; } // Schedule the hydration this.asyncRenderer.scheduleTask( async () => { try { await hydrateFunction(element); this.state.pending.delete(id); this.state.completed.add(id); // Mark as hydrated element.setAttribute('data-hydrated', 'true'); element.removeAttribute('data-hydrate'); this.log(`Hydrated element: ${id}`); // Check if all hydration is complete if (this.state.pending.size === 0 && this.state.isHydrating) { this.complete(); } return true; } catch (error) { this.log(`Error hydrating element ${id}: ${error}`, 'error'); this.state.pending.delete(id); this.state.failed.add(id); // Mark as failed element.setAttribute('data-hydration-failed', 'true'); if (this.options.onError) { this.options.onError(error instanceof Error ? error : new Error(String(error))); } return false; } }, { priority, onError: (error) => { this.log(`Error in hydration task ${id}: ${error}`, 'error'); this.state.pending.delete(id); this.state.failed.add(id); // Mark as failed element.setAttribute('data-hydration-failed', 'true'); if (this.options.onError) { this.options.onError(error); } } } ); } /** * Get hydration function by ID */ private getHydrationFunction(id: string): ((element: HTMLElement) => Promise<void>) | null { // This would typically lookup from a global registry of hydration functions // In this example, we'll check for a global object with registered hydrators const globalHydrators = (window as any).__HYDRATORS__ || {}; return globalHydrators[id] || null; } /** * Check if an element is visible */ private isElementVisible(element: HTMLElement): boolean { if (!element.offsetParent && element.offsetHeight === 0 && element.offsetWidth === 0) { return false; } const rect = element.getBoundingClientRect(); const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); return !(rect.bottom < 0 || rect.top - viewHeight >= 0); } /** * Complete the hydration process */ private complete(): void { if (!this.state.isHydrating) return; this.state.isHydrating = false; const duration = performance.now() - this.state.startTime; this.log(`Hydration completed in ${duration.toFixed(2)}ms. Successful: ${this.state.completed.size}, Failed: ${this.state.failed.size}`); // Cleanup resources if (this.intersectionObserver) { this.intersectionObserver.disconnect(); this.intersectionObserver = null; } if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } // Remove all remaining event listeners this.eventListeners.forEach((listeners, id) => { const elements = document.querySelectorAll(`[data-hydrate="${id}"]`); elements.forEach(element => { listeners.forEach(([evt, listener]) => { element.removeEventListener(evt, listener); }); }); }); this.eventListeners.clear(); // Call the completion callback if (this.options.onComplete) { this.options.onComplete(); } } /** * Logger function */ private log(message: string, level: 'info' | 'error' = 'info'): void { if (!this.options.debug) return; if (level === 'error') { console.error(`[HydrationController] ${message}`); } else { console.log(`[HydrationController] ${message}`); } } } /** * Register a hydration function for an island component */ export function registerHydrator( id: string, hydrateFunction: (element: HTMLElement) => Promise<void> ): void { if (typeof window !== 'undefined') { (window as any).__HYDRATORS__ = (window as any).__HYDRATORS__ || {}; (window as any).__HYDRATORS__[id] = hydrateFunction; } } /** * Create a default hydration controller */ export function createHydrationController(options?: HydrationOptions): HydrationController { return new HydrationController(options); }