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:
297
resources/js/core/LinkPrefetcher.js
Normal file
297
resources/js/core/LinkPrefetcher.js
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Advanced Link Prefetching System
|
||||
* Supports multiple strategies: hover, visible, eager
|
||||
*/
|
||||
import { Logger } from './logger.js';
|
||||
import { SimpleCache } from '../utils/cache.js';
|
||||
|
||||
export class LinkPrefetcher {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
maxCacheSize: options.maxCacheSize || 20,
|
||||
cacheTTL: options.cacheTTL || 60000, // 60 seconds
|
||||
hoverDelay: options.hoverDelay || 150, // ms before prefetch on hover
|
||||
strategies: options.strategies || ['hover', 'visible'], // Available: hover, visible, eager
|
||||
observerMargin: options.observerMargin || '50px',
|
||||
priority: options.priority || 'low', // low, high
|
||||
...options
|
||||
};
|
||||
|
||||
this.cache = new SimpleCache(this.options.maxCacheSize, this.options.cacheTTL);
|
||||
this.prefetching = new Set(); // Currently prefetching URLs
|
||||
this.prefetched = new Set(); // Already prefetched URLs
|
||||
this.observer = null;
|
||||
this.hoverTimeout = null;
|
||||
this.cleanupInterval = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Setup strategies
|
||||
if (this.options.strategies.includes('visible')) {
|
||||
this.setupIntersectionObserver();
|
||||
}
|
||||
|
||||
// Setup cleanup interval
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cache.cleanup();
|
||||
}, 120000); // Clean every 2 minutes
|
||||
|
||||
Logger.info('[LinkPrefetcher] initialized with strategies:', this.options.strategies);
|
||||
}
|
||||
|
||||
setupIntersectionObserver() {
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
Logger.warn('[LinkPrefetcher] IntersectionObserver not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const link = entry.target;
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
// Lower priority for visible links
|
||||
this.prefetch(href, { priority: 'low' });
|
||||
|
||||
// Stop observing once prefetched
|
||||
this.observer.unobserve(link);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: this.options.observerMargin
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a link should be prefetched
|
||||
*/
|
||||
shouldPrefetch(link) {
|
||||
if (!link || !(link instanceof HTMLAnchorElement)) return false;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip external links
|
||||
try {
|
||||
const url = new URL(href, window.location.origin);
|
||||
if (url.origin !== window.location.origin) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip if has data-no-prefetch attribute
|
||||
if (link.hasAttribute('data-no-prefetch')) return false;
|
||||
|
||||
// Skip download links
|
||||
if (link.hasAttribute('download')) return false;
|
||||
|
||||
// Skip target="_blank" links
|
||||
if (link.target === '_blank') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a URL
|
||||
*/
|
||||
async prefetch(href, options = {}) {
|
||||
if (!href || this.prefetching.has(href) || this.cache.has(href)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as prefetching
|
||||
this.prefetching.add(href);
|
||||
|
||||
try {
|
||||
Logger.info(`[LinkPrefetcher] prefetching: ${href}`);
|
||||
|
||||
// Use different methods based on priority
|
||||
if (options.priority === 'high' || this.options.priority === 'high') {
|
||||
await this.fetchWithPriority(href, 'high');
|
||||
} else {
|
||||
// Use link prefetch hint for low priority
|
||||
this.createPrefetchLink(href);
|
||||
// Also fetch for cache
|
||||
await this.fetchWithPriority(href, 'low');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn(`[LinkPrefetcher] failed to prefetch ${href}:`, error);
|
||||
} finally {
|
||||
this.prefetching.delete(href);
|
||||
this.prefetched.add(href);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with priority
|
||||
*/
|
||||
async fetchWithPriority(href, priority = 'low') {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
||||
|
||||
try {
|
||||
const response = await fetch(href, {
|
||||
signal: controller.signal,
|
||||
priority: priority, // Fetch priority hint (Chrome 121+)
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-Prefetch': 'true'
|
||||
}
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
this.cache.set(href, {
|
||||
data: html,
|
||||
timestamp: Date.now(),
|
||||
headers: {
|
||||
contentType: response.headers.get('content-type'),
|
||||
lastModified: response.headers.get('last-modified')
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link element for browser prefetch hint
|
||||
*/
|
||||
createPrefetchLink(href) {
|
||||
// Check if already exists
|
||||
if (document.querySelector(`link[rel="prefetch"][href="${href}"]`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'prefetch';
|
||||
link.href = href;
|
||||
link.as = 'document';
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Remove after some time to avoid cluttering
|
||||
setTimeout(() => link.remove(), 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle hover events
|
||||
*/
|
||||
handleHover(link) {
|
||||
if (!this.shouldPrefetch(link)) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = setTimeout(() => {
|
||||
this.prefetch(href, { priority: 'high' });
|
||||
}, this.options.hoverDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse leave
|
||||
*/
|
||||
handleMouseLeave() {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe a link for intersection
|
||||
*/
|
||||
observeLink(link) {
|
||||
if (!this.observer || !this.shouldPrefetch(link)) return;
|
||||
|
||||
this.observer.observe(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe all links in a container
|
||||
*/
|
||||
observeLinks(container = document) {
|
||||
if (!this.options.strategies.includes('visible')) return;
|
||||
|
||||
const links = container.querySelectorAll('a[href]');
|
||||
links.forEach(link => this.observeLink(link));
|
||||
}
|
||||
|
||||
/**
|
||||
* Eagerly prefetch specific URLs
|
||||
*/
|
||||
prefetchEager(urls) {
|
||||
if (!Array.isArray(urls)) urls = [urls];
|
||||
|
||||
urls.forEach(url => {
|
||||
this.prefetch(url, { priority: 'high' });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached content
|
||||
*/
|
||||
getCached(href) {
|
||||
const cached = this.cache.get(href);
|
||||
return cached ? cached.data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if URL is cached
|
||||
*/
|
||||
isCached(href) {
|
||||
return this.cache.has(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear();
|
||||
this.prefetched.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
cacheSize: this.cache.size(),
|
||||
prefetching: this.prefetching.size,
|
||||
prefetched: this.prefetched.size,
|
||||
strategies: this.options.strategies
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the prefetcher
|
||||
*/
|
||||
destroy() {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
clearInterval(this.cleanupInterval);
|
||||
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
this.cache.clear();
|
||||
this.prefetching.clear();
|
||||
this.prefetched.clear();
|
||||
|
||||
// Remove all prefetch links
|
||||
document.querySelectorAll('link[rel="prefetch"]').forEach(link => link.remove());
|
||||
|
||||
Logger.info('[LinkPrefetcher] destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const linkPrefetcher = new LinkPrefetcher();
|
||||
Reference in New Issue
Block a user