- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
491 lines
17 KiB
JavaScript
491 lines
17 KiB
JavaScript
// modules/api-manager/ObserverManager.js
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* Observer APIs Manager - Intersection, Resize, Mutation, Performance Observers
|
|
*/
|
|
export class ObserverManager {
|
|
constructor(config = {}) {
|
|
this.config = config;
|
|
this.activeObservers = new Map();
|
|
this.observerInstances = new Map();
|
|
|
|
Logger.info('[ObserverManager] Initialized with support:', {
|
|
intersection: 'IntersectionObserver' in window,
|
|
resize: 'ResizeObserver' in window,
|
|
mutation: 'MutationObserver' in window,
|
|
performance: 'PerformanceObserver' in window
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Intersection Observer - Viewport intersection detection
|
|
*/
|
|
intersection(elements, callback, options = {}) {
|
|
if (!('IntersectionObserver' in window)) {
|
|
Logger.warn('[ObserverManager] IntersectionObserver not supported');
|
|
return this.createFallbackObserver('intersection', elements, callback);
|
|
}
|
|
|
|
const defaultOptions = {
|
|
root: null,
|
|
rootMargin: '50px',
|
|
threshold: [0, 0.1, 0.5, 1.0],
|
|
...options
|
|
};
|
|
|
|
const observerId = this.generateId('intersection');
|
|
const observer = new IntersectionObserver((entries, obs) => {
|
|
const processedEntries = entries.map(entry => ({
|
|
element: entry.target,
|
|
isIntersecting: entry.isIntersecting,
|
|
intersectionRatio: entry.intersectionRatio,
|
|
boundingClientRect: entry.boundingClientRect,
|
|
rootBounds: entry.rootBounds,
|
|
intersectionRect: entry.intersectionRect,
|
|
time: entry.time,
|
|
// Enhanced data
|
|
visibility: this.calculateVisibility(entry),
|
|
direction: this.getScrollDirection(entry),
|
|
position: this.getElementPosition(entry)
|
|
}));
|
|
|
|
callback(processedEntries, obs);
|
|
}, defaultOptions);
|
|
|
|
// Observe elements
|
|
const elementList = Array.isArray(elements) ? elements : [elements];
|
|
elementList.forEach(el => {
|
|
if (el instanceof Element) observer.observe(el);
|
|
});
|
|
|
|
this.observerInstances.set(observerId, observer);
|
|
this.activeObservers.set(observerId, {
|
|
type: 'intersection',
|
|
elements: elementList,
|
|
callback,
|
|
options: defaultOptions
|
|
});
|
|
|
|
Logger.info(`[ObserverManager] IntersectionObserver created: ${observerId}`);
|
|
|
|
return {
|
|
id: observerId,
|
|
observer,
|
|
unobserve: (element) => observer.unobserve(element),
|
|
disconnect: () => this.disconnect(observerId),
|
|
updateThreshold: (threshold) => this.updateIntersectionThreshold(observerId, threshold)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resize Observer - Element resize detection
|
|
*/
|
|
resize(elements, callback, options = {}) {
|
|
if (!('ResizeObserver' in window)) {
|
|
Logger.warn('[ObserverManager] ResizeObserver not supported');
|
|
return this.createFallbackObserver('resize', elements, callback);
|
|
}
|
|
|
|
const observerId = this.generateId('resize');
|
|
const observer = new ResizeObserver((entries) => {
|
|
const processedEntries = entries.map(entry => ({
|
|
element: entry.target,
|
|
contentRect: entry.contentRect,
|
|
borderBoxSize: entry.borderBoxSize,
|
|
contentBoxSize: entry.contentBoxSize,
|
|
devicePixelContentBoxSize: entry.devicePixelContentBoxSize,
|
|
// Enhanced data
|
|
dimensions: {
|
|
width: entry.contentRect.width,
|
|
height: entry.contentRect.height,
|
|
aspectRatio: entry.contentRect.width / entry.contentRect.height
|
|
},
|
|
deltaSize: this.calculateDeltaSize(entry),
|
|
breakpoint: this.detectBreakpoint(entry.contentRect.width)
|
|
}));
|
|
|
|
callback(processedEntries);
|
|
});
|
|
|
|
const elementList = Array.isArray(elements) ? elements : [elements];
|
|
elementList.forEach(el => {
|
|
if (el instanceof Element) observer.observe(el);
|
|
});
|
|
|
|
this.observerInstances.set(observerId, observer);
|
|
this.activeObservers.set(observerId, {
|
|
type: 'resize',
|
|
elements: elementList,
|
|
callback,
|
|
options
|
|
});
|
|
|
|
Logger.info(`[ObserverManager] ResizeObserver created: ${observerId}`);
|
|
|
|
return {
|
|
id: observerId,
|
|
observer,
|
|
unobserve: (element) => observer.unobserve(element),
|
|
disconnect: () => this.disconnect(observerId)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation Observer - DOM change detection
|
|
*/
|
|
mutation(target, callback, options = {}) {
|
|
if (!('MutationObserver' in window)) {
|
|
Logger.warn('[ObserverManager] MutationObserver not supported');
|
|
return null;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
childList: true,
|
|
attributes: true,
|
|
subtree: true,
|
|
attributeOldValue: true,
|
|
characterDataOldValue: true,
|
|
...options
|
|
};
|
|
|
|
const observerId = this.generateId('mutation');
|
|
const observer = new MutationObserver((mutations) => {
|
|
const processedMutations = mutations.map(mutation => ({
|
|
type: mutation.type,
|
|
target: mutation.target,
|
|
addedNodes: Array.from(mutation.addedNodes),
|
|
removedNodes: Array.from(mutation.removedNodes),
|
|
attributeName: mutation.attributeName,
|
|
attributeNamespace: mutation.attributeNamespace,
|
|
oldValue: mutation.oldValue,
|
|
// Enhanced data
|
|
summary: this.summarizeMutation(mutation),
|
|
impact: this.assessMutationImpact(mutation)
|
|
}));
|
|
|
|
callback(processedMutations);
|
|
});
|
|
|
|
observer.observe(target, defaultOptions);
|
|
|
|
this.observerInstances.set(observerId, observer);
|
|
this.activeObservers.set(observerId, {
|
|
type: 'mutation',
|
|
target,
|
|
callback,
|
|
options: defaultOptions
|
|
});
|
|
|
|
Logger.info(`[ObserverManager] MutationObserver created: ${observerId}`);
|
|
|
|
return {
|
|
id: observerId,
|
|
observer,
|
|
disconnect: () => this.disconnect(observerId),
|
|
takeRecords: () => observer.takeRecords()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Performance Observer - Performance metrics monitoring
|
|
*/
|
|
performance(callback, options = {}) {
|
|
if (!('PerformanceObserver' in window)) {
|
|
Logger.warn('[ObserverManager] PerformanceObserver not supported');
|
|
return null;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
entryTypes: ['measure', 'navigation', 'paint', 'largest-contentful-paint'],
|
|
buffered: true,
|
|
...options
|
|
};
|
|
|
|
const observerId = this.generateId('performance');
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries().map(entry => ({
|
|
name: entry.name,
|
|
entryType: entry.entryType,
|
|
startTime: entry.startTime,
|
|
duration: entry.duration,
|
|
// Enhanced data based on entry type
|
|
details: this.enhancePerformanceEntry(entry),
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
callback(entries);
|
|
});
|
|
|
|
observer.observe(defaultOptions);
|
|
|
|
this.observerInstances.set(observerId, observer);
|
|
this.activeObservers.set(observerId, {
|
|
type: 'performance',
|
|
callback,
|
|
options: defaultOptions
|
|
});
|
|
|
|
Logger.info(`[ObserverManager] PerformanceObserver created: ${observerId}`);
|
|
|
|
return {
|
|
id: observerId,
|
|
observer,
|
|
disconnect: () => this.disconnect(observerId),
|
|
takeRecords: () => observer.takeRecords()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Lazy Loading Helper - Uses IntersectionObserver
|
|
*/
|
|
lazyLoad(selector = 'img[data-src], iframe[data-src]', options = {}) {
|
|
const elements = document.querySelectorAll(selector);
|
|
|
|
return this.intersection(elements, (entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const element = entry.element;
|
|
|
|
// Load image or iframe
|
|
if (element.dataset.src) {
|
|
element.src = element.dataset.src;
|
|
delete element.dataset.src;
|
|
}
|
|
|
|
// Load srcset if available
|
|
if (element.dataset.srcset) {
|
|
element.srcset = element.dataset.srcset;
|
|
delete element.dataset.srcset;
|
|
}
|
|
|
|
// Add loaded class
|
|
element.classList.add('loaded');
|
|
|
|
// Stop observing this element
|
|
entry.observer.unobserve(element);
|
|
|
|
Logger.info('[ObserverManager] Lazy loaded:', element.src);
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '100px',
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scroll Trigger Helper - Uses IntersectionObserver
|
|
*/
|
|
scrollTrigger(elements, callback, options = {}) {
|
|
return this.intersection(elements, (entries) => {
|
|
entries.forEach(entry => {
|
|
const triggerData = {
|
|
element: entry.element,
|
|
progress: entry.intersectionRatio,
|
|
isVisible: entry.isIntersecting,
|
|
direction: entry.direction,
|
|
position: entry.position
|
|
};
|
|
|
|
callback(triggerData);
|
|
});
|
|
}, {
|
|
threshold: this.createThresholdArray(options.steps || 10),
|
|
...options
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Viewport Detection Helper
|
|
*/
|
|
viewport(callback, options = {}) {
|
|
const viewportElement = document.createElement('div');
|
|
viewportElement.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
visibility: hidden;
|
|
`;
|
|
document.body.appendChild(viewportElement);
|
|
|
|
return this.resize([viewportElement], (entries) => {
|
|
const viewport = entries[0];
|
|
callback({
|
|
width: viewport.dimensions.width,
|
|
height: viewport.dimensions.height,
|
|
aspectRatio: viewport.dimensions.aspectRatio,
|
|
orientation: viewport.dimensions.width > viewport.dimensions.height ? 'landscape' : 'portrait',
|
|
breakpoint: viewport.breakpoint
|
|
});
|
|
}, options);
|
|
}
|
|
|
|
// Helper Methods
|
|
|
|
generateId(type) {
|
|
return `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
disconnect(observerId) {
|
|
const observer = this.observerInstances.get(observerId);
|
|
if (observer) {
|
|
observer.disconnect();
|
|
this.observerInstances.delete(observerId);
|
|
this.activeObservers.delete(observerId);
|
|
Logger.info(`[ObserverManager] Observer disconnected: ${observerId}`);
|
|
}
|
|
}
|
|
|
|
disconnectAll() {
|
|
this.observerInstances.forEach((observer, id) => {
|
|
observer.disconnect();
|
|
});
|
|
this.observerInstances.clear();
|
|
this.activeObservers.clear();
|
|
Logger.info('[ObserverManager] All observers disconnected');
|
|
}
|
|
|
|
calculateVisibility(entry) {
|
|
if (!entry.isIntersecting) return 0;
|
|
|
|
const visibleArea = entry.intersectionRect.width * entry.intersectionRect.height;
|
|
const totalArea = entry.boundingClientRect.width * entry.boundingClientRect.height;
|
|
|
|
return totalArea > 0 ? Math.round((visibleArea / totalArea) * 100) : 0;
|
|
}
|
|
|
|
getScrollDirection(entry) {
|
|
// This would need to store previous positions to determine direction
|
|
// For now, return based on intersection ratio change
|
|
return entry.intersectionRatio > 0.5 ? 'down' : 'up';
|
|
}
|
|
|
|
getElementPosition(entry) {
|
|
const rect = entry.boundingClientRect;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
if (rect.top < 0 && rect.bottom > 0) return 'entering-top';
|
|
if (rect.top < viewportHeight && rect.bottom > viewportHeight) return 'entering-bottom';
|
|
if (rect.top >= 0 && rect.bottom <= viewportHeight) return 'visible';
|
|
return 'hidden';
|
|
}
|
|
|
|
calculateDeltaSize(entry) {
|
|
// Would need to store previous sizes to calculate delta
|
|
return { width: 0, height: 0 };
|
|
}
|
|
|
|
detectBreakpoint(width) {
|
|
if (width < 576) return 'xs';
|
|
if (width < 768) return 'sm';
|
|
if (width < 992) return 'md';
|
|
if (width < 1200) return 'lg';
|
|
return 'xl';
|
|
}
|
|
|
|
summarizeMutation(mutation) {
|
|
return `${mutation.type} on ${mutation.target.tagName}`;
|
|
}
|
|
|
|
assessMutationImpact(mutation) {
|
|
// Simple impact assessment
|
|
if (mutation.type === 'childList') {
|
|
return mutation.addedNodes.length + mutation.removedNodes.length > 5 ? 'high' : 'low';
|
|
}
|
|
return 'medium';
|
|
}
|
|
|
|
enhancePerformanceEntry(entry) {
|
|
const details = { raw: entry };
|
|
|
|
switch (entry.entryType) {
|
|
case 'navigation':
|
|
details.loadTime = entry.loadEventEnd - entry.navigationStart;
|
|
details.domContentLoaded = entry.domContentLoadedEventEnd - entry.navigationStart;
|
|
break;
|
|
case 'paint':
|
|
details.paintType = entry.name;
|
|
break;
|
|
case 'largest-contentful-paint':
|
|
details.element = entry.element;
|
|
details.url = entry.url;
|
|
break;
|
|
}
|
|
|
|
return details;
|
|
}
|
|
|
|
createThresholdArray(steps) {
|
|
const thresholds = [];
|
|
for (let i = 0; i <= steps; i++) {
|
|
thresholds.push(i / steps);
|
|
}
|
|
return thresholds;
|
|
}
|
|
|
|
createFallbackObserver(type, elements, callback) {
|
|
Logger.warn(`[ObserverManager] Creating fallback for ${type}Observer`);
|
|
|
|
// Simple polling fallback
|
|
const fallbackId = this.generateId(`fallback_${type}`);
|
|
let intervalId;
|
|
|
|
switch (type) {
|
|
case 'intersection':
|
|
intervalId = setInterval(() => {
|
|
const elementList = Array.isArray(elements) ? elements : [elements];
|
|
const entries = elementList.map(el => ({
|
|
element: el,
|
|
isIntersecting: this.isElementInViewport(el),
|
|
intersectionRatio: this.calculateIntersectionRatio(el)
|
|
}));
|
|
callback(entries);
|
|
}, 100);
|
|
break;
|
|
}
|
|
|
|
return {
|
|
id: fallbackId,
|
|
disconnect: () => {
|
|
if (intervalId) clearInterval(intervalId);
|
|
}
|
|
};
|
|
}
|
|
|
|
isElementInViewport(el) {
|
|
const rect = el.getBoundingClientRect();
|
|
return (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= window.innerHeight &&
|
|
rect.right <= window.innerWidth
|
|
);
|
|
}
|
|
|
|
calculateIntersectionRatio(el) {
|
|
const rect = el.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const viewportWidth = window.innerWidth;
|
|
|
|
const visibleHeight = Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0);
|
|
const visibleWidth = Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0);
|
|
|
|
if (visibleHeight <= 0 || visibleWidth <= 0) return 0;
|
|
|
|
const visibleArea = visibleHeight * visibleWidth;
|
|
const totalArea = rect.height * rect.width;
|
|
|
|
return totalArea > 0 ? visibleArea / totalArea : 0;
|
|
}
|
|
|
|
getActiveObservers() {
|
|
return Array.from(this.activeObservers.entries()).map(([id, data]) => ({
|
|
id,
|
|
...data
|
|
}));
|
|
}
|
|
} |