Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
491
resources/js/modules/api-manager/ObserverManager.js
Normal file
491
resources/js/modules/api-manager/ObserverManager.js
Normal file
@@ -0,0 +1,491 @@
|
||||
// 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
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user