@zenithcore/core
Version:
Core functionality for ZenithKernel framework
283 lines (255 loc) • 8.01 kB
text/typescript
/**
* Advanced Hydration Controller for Zenith Framework
*
* Implements sophisticated hydration strategies based on archipelagoui patterns:
* - immediate: Hydrate on page load
* - visible: Hydrate when visible in viewport
* - interaction: Hydrate on user interaction
* - idle: Hydrate during browser idle time
* - manual: Hydrate only when explicitly triggered
*/
import type { HydraContext } from '@lib/hydra-runtime';
import type { IslandRegistration } from './types';
export type HydrationStrategy =
| 'immediate' // Hydrate on page load
| 'visible' // Hydrate when visible in viewport
| 'interaction' // Hydrate on user interaction
| 'idle' // Hydrate during browser idle time
| 'manual'; // Hydrate only when explicitly triggered
interface HydrationQueueItem {
el: HTMLElement;
entry: string;
context: HydraContext;
priority: number;
strategy: HydrationStrategy;
}
export class HydrationController {
private hydrationQueue: HydrationQueueItem[] = [];
private intersectionObserver?: IntersectionObserver;
private interactionListeners: Map<string, EventListener> = new Map();
private isProcessing = false;
private idleCallbackId?: number;
constructor(
private hydrateComponent: (elementId: string, entry: string, context: HydraContext) => Promise<void>
) {
this.setupIntersectionObserver();
this.setupIdleCallback();
}
/**
* Queue an element for hydration based on strategy
*/
public queueHydration(
el: HTMLElement,
entry: string,
context: HydraContext,
strategy: HydrationStrategy = 'immediate'
): void {
const priority = this.getPriority(el, strategy);
this.hydrationQueue.push({ el, entry, context, priority, strategy });
this.hydrationQueue.sort((a, b) => a.priority - b.priority);
// Setup appropriate listeners based on strategy
if (strategy === 'visible') {
this.intersectionObserver?.observe(el);
} else if (strategy === 'interaction') {
this.setupInteractionListeners(el, entry, context);
} else if (strategy === 'idle') {
this.scheduleIdleHydration();
} else if (strategy === 'immediate') {
this.startProcessing();
}
}
/**
* Process the hydration queue frame by frame
*/
private processQueue(): void {
if (this.hydrationQueue.length === 0) {
this.isProcessing = false;
return;
}
this.isProcessing = true;
const start = performance.now();
const frameTimeBudget = 16; // ~60fps
while (
this.hydrationQueue.length > 0 &&
performance.now() - start < frameTimeBudget
) {
const next = this.hydrationQueue[0];
// Skip if not ready for hydration based on strategy
if (!this.isReadyForHydration(next)) {
this.hydrationQueue.shift();
this.hydrationQueue.push(next); // Move to end
continue;
}
// Hydrate the component
try {
this.hydrateComponent(next.el.id, next.entry, next.context);
} catch (error) {
console.error('Hydration error:', error);
}
this.hydrationQueue.shift();
}
// Continue processing in next frame if needed
if (this.hydrationQueue.length > 0) {
requestAnimationFrame(() => this.processQueue());
} else {
this.isProcessing = false;
}
}
/**
* Start processing the hydration queue
*/
private startProcessing(): void {
if (!this.isProcessing) {
requestAnimationFrame(() => this.processQueue());
}
}
/**
* Setup Intersection Observer for visibility-based hydration
*/
private setupIntersectionObserver(): void {
this.intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const queueItem = this.hydrationQueue.find(
item => item.el === entry.target && item.strategy === 'visible'
);
if (queueItem) {
queueItem.strategy = 'immediate';
this.startProcessing();
}
this.intersectionObserver?.unobserve(entry.target);
}
});
},
{
root: null,
rootMargin: '50px',
threshold: 0.1
}
);
}
/**
* Setup interaction listeners for interaction-based hydration
*/
private setupInteractionListeners(
el: HTMLElement,
entry: string,
context: HydraContext
): void {
const listener = () => {
const queueItem = this.hydrationQueue.find(
item => item.el === el && item.strategy === 'interaction'
);
if (queueItem) {
queueItem.strategy = 'immediate';
this.startProcessing();
}
el.removeEventListener('click', listener);
el.removeEventListener('mouseover', listener);
el.removeEventListener('focus', listener);
};
el.addEventListener('click', listener);
el.addEventListener('mouseover', listener);
el.addEventListener('focus', listener);
this.interactionListeners.set(el.id, listener);
}
/**
* Setup idle callback for idle-based hydration
*/
private setupIdleCallback(): void {
if ('requestIdleCallback' in window) {
this.scheduleIdleHydration();
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => this.processIdleQueue(), 1000);
}
}
/**
* Schedule idle hydration using requestIdleCallback
*/
private scheduleIdleHydration(): void {
if ('requestIdleCallback' in window) {
this.idleCallbackId = window.requestIdleCallback(
() => this.processIdleQueue(),
{ timeout: 1000 }
);
}
}
/**
* Process items queued for idle hydration
*/
private processIdleQueue(): void {
const idleItems = this.hydrationQueue.filter(item => item.strategy === 'idle');
idleItems.forEach(item => {
item.strategy = 'immediate';
});
if (idleItems.length > 0) {
this.startProcessing();
}
}
/**
* Get priority for hydration based on element and strategy
*/
private getPriority(el: HTMLElement, strategy: HydrationStrategy): number {
const basePriority = {
immediate: 1,
visible: 2,
interaction: 3,
idle: 4,
manual: 5
}[strategy] ?? 5;
// Adjust priority based on viewport visibility
const rect = el.getBoundingClientRect();
const isInViewport = rect.top >= 0 && rect.top <= window.innerHeight;
return isInViewport ? basePriority : basePriority + 5;
}
/**
* Check if an item is ready for hydration
*/
private isReadyForHydration(item: HydrationQueueItem): boolean {
switch (item.strategy) {
case 'immediate':
return true;
case 'visible':
const rect = item.el.getBoundingClientRect();
return rect.top >= 0 && rect.top <= window.innerHeight;
case 'interaction':
case 'idle':
case 'manual':
return false;
default:
return true;
}
}
/**
* Manually trigger hydration for an element
*/
public triggerManualHydration(elementId: string): void {
const queueItem = this.hydrationQueue.find(
item => item.el.id === elementId && item.strategy === 'manual'
);
if (queueItem) {
queueItem.strategy = 'immediate';
this.startProcessing();
}
}
/**
* Clean up resources when controller is no longer needed
*/
public destroy(): void {
this.intersectionObserver?.disconnect();
this.interactionListeners.forEach((listener, elementId) => {
const el = document.getElementById(elementId);
if (el) {
el.removeEventListener('click', listener);
el.removeEventListener('mouseover', listener);
el.removeEventListener('focus', listener);
}
});
this.interactionListeners.clear();
if (this.idleCallbackId && 'cancelIdleCallback' in window) {
window.cancelIdleCallback(this.idleCallbackId);
}
}
}