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:
@@ -1,15 +1,12 @@
|
||||
// modules/core/click-manager.js
|
||||
import { Logger } from './logger.js';
|
||||
import { useEvent } from './useEvent.js';
|
||||
import {navigateTo} from "./navigateTo";
|
||||
import {SimpleCache} from "../utils/cache";
|
||||
import { navigateTo } from "./navigateTo";
|
||||
import { LinkPrefetcher } from './LinkPrefetcher.js';
|
||||
|
||||
let callback = null;
|
||||
let unsubscribes = [];
|
||||
let cleanupInterval = null;
|
||||
const prefetchCache = new SimpleCache(20, 60000); //new Map();
|
||||
const maxCacheSize = 20; // max. Anzahl gecachter Seiten
|
||||
const cacheTTL = 60000; // Lebensdauer in ms (60s)
|
||||
let prefetcher = null;
|
||||
|
||||
function isInternal(link) {
|
||||
return link.origin === location.origin;
|
||||
@@ -36,15 +33,14 @@ function handleClick(e) {
|
||||
if (isInternal(link)) {
|
||||
e.preventDefault();
|
||||
|
||||
const cached = prefetchCache.get(href);
|
||||
const valid = cached && Date.now() - cached.timestamp < cacheTTL;
|
||||
const cached = prefetcher ? prefetcher.getCached(href) : null;
|
||||
|
||||
const options = {
|
||||
viewTransition: link.hasAttribute('data-view-transition'),
|
||||
replace: link.hasAttribute('data-replace'),
|
||||
modal: link.hasAttribute('data-modal'),
|
||||
prefetched: valid,
|
||||
data: valid ? cached.data : null,
|
||||
prefetched: cached !== null,
|
||||
data: cached,
|
||||
};
|
||||
|
||||
Logger.info(`[click-manager] internal: ${href}`, options);
|
||||
@@ -57,37 +53,18 @@ function handleClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
let prefetchTimeout;
|
||||
function handleMouseOver(e) {
|
||||
clearTimeout(prefetchTimeout);
|
||||
const link = e.target.closest('a[href]');
|
||||
if (!link || !isInternal(link)) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
if (!href || prefetchCache.has(href)) return;
|
||||
|
||||
// optional: Wait 150ms to reduce noise
|
||||
prefetchTimeout = setTimeout(() => prefetch(href), 150);
|
||||
if (!link || !prefetcher) return;
|
||||
|
||||
prefetcher.handleHover(link);
|
||||
}
|
||||
|
||||
function prefetch(href) {
|
||||
Logger.info(`[click-manager] prefetching: ${href}`);
|
||||
|
||||
fetch(href)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
if (prefetchCache.cache.size >= maxCacheSize) {
|
||||
const oldestKey = [...prefetchCache.cache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
|
||||
prefetchCache.cache.delete(oldestKey);
|
||||
}
|
||||
prefetchCache.set(href, {
|
||||
data: html,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.warn(`[click-manager] prefetch failed for ${href}`, err);
|
||||
});
|
||||
function handleMouseOut(e) {
|
||||
const link = e.target.closest('a[href]');
|
||||
if (!link || !prefetcher) return;
|
||||
|
||||
prefetcher.handleMouseLeave();
|
||||
}
|
||||
|
||||
function handlePopState() {
|
||||
@@ -97,40 +74,77 @@ function handlePopState() {
|
||||
}
|
||||
|
||||
export function getPrefetched(href) {
|
||||
const cached = prefetchCache.get(href);
|
||||
const valid = cached && Date.now() - cached.timestamp < cacheTTL;
|
||||
return valid ? cached.data : null;
|
||||
return prefetcher ? prefetcher.getCached(href) : null;
|
||||
}
|
||||
|
||||
export function prefetchHref(href) {
|
||||
if (!href || prefetchCache.has(href)) return;
|
||||
prefetch(href);
|
||||
if (!href || !prefetcher) return;
|
||||
prefetcher.prefetch(href, { priority: 'high' });
|
||||
}
|
||||
|
||||
export function init(onNavigate) {
|
||||
export function prefetchUrls(urls) {
|
||||
if (!prefetcher) return;
|
||||
prefetcher.prefetchEager(urls);
|
||||
}
|
||||
|
||||
export function init(onNavigate, prefetchOptions = {}) {
|
||||
callback = onNavigate;
|
||||
|
||||
// Initialize prefetcher with options
|
||||
prefetcher = new LinkPrefetcher({
|
||||
strategies: ['hover', 'visible'],
|
||||
hoverDelay: 150,
|
||||
observerMargin: '50px',
|
||||
maxCacheSize: 20,
|
||||
cacheTTL: 60000,
|
||||
...prefetchOptions
|
||||
});
|
||||
|
||||
unsubscribes = [
|
||||
useEvent(document, 'click', handleClick),
|
||||
useEvent(document, 'mouseover', handleMouseOver),
|
||||
useEvent(document, 'mouseout', handleMouseOut),
|
||||
useEvent(window, 'popstate', handlePopState),
|
||||
]
|
||||
|
||||
cleanupInterval = setInterval(() => {
|
||||
for (const [key, val] of prefetchCache.cache.entries()) {
|
||||
if (Date.now() - val.timestamp > prefetchCache.ttl) {
|
||||
prefetchCache.cache.delete(key);
|
||||
}
|
||||
];
|
||||
|
||||
// Observe all initial links if visible strategy is enabled
|
||||
if (prefetchOptions.strategies?.includes('visible') !== false) {
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
prefetcher.observeLinks();
|
||||
});
|
||||
} else {
|
||||
prefetcher.observeLinks();
|
||||
}
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
// Support for eager prefetching via data attributes
|
||||
document.querySelectorAll('a[data-prefetch="eager"]').forEach(link => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href) {
|
||||
prefetcher.prefetch(href, { priority: 'high' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Logger.info('[click-manager] ready');
|
||||
Logger.info('[click-manager] ready with prefetching');
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
callback = null;
|
||||
prefetchCache.clear();
|
||||
|
||||
if (prefetcher) {
|
||||
prefetcher.destroy();
|
||||
prefetcher = null;
|
||||
}
|
||||
|
||||
unsubscribes.forEach(unsub => unsub());
|
||||
unsubscribes = [];
|
||||
|
||||
Logger.info('[click-manager] destroyed');
|
||||
}
|
||||
|
||||
// Export prefetcher for advanced usage
|
||||
export function getPrefetcher() {
|
||||
return prefetcher;
|
||||
}
|
||||
|
||||
353
resources/js/core/DependencyManager.js
Normal file
353
resources/js/core/DependencyManager.js
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* Dependency Management System für Module
|
||||
* Verwaltet Dependencies und Initialisierungsreihenfolge
|
||||
*/
|
||||
import { Logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModuleDependency
|
||||
* @property {string} name - Module name
|
||||
* @property {string} version - Required version (semver)
|
||||
* @property {boolean} optional - Whether dependency is optional
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModuleDefinition
|
||||
* @property {string} name - Module name
|
||||
* @property {string} version - Module version
|
||||
* @property {ModuleDependency[]} dependencies - Required dependencies
|
||||
* @property {string[]} provides - Services this module provides
|
||||
* @property {number} priority - Initialization priority (higher = earlier)
|
||||
*/
|
||||
|
||||
export class DependencyManager {
|
||||
constructor() {
|
||||
/** @type {Map<string, ModuleDefinition>} */
|
||||
this.modules = new Map();
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
this.dependents = new Map(); // who depends on this module
|
||||
|
||||
/** @type {Set<string>} */
|
||||
this.initialized = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
this.initializing = new Set();
|
||||
|
||||
/** @type {string[]} */
|
||||
this.initializationOrder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module with its dependencies
|
||||
* @param {ModuleDefinition} definition - Module definition
|
||||
*/
|
||||
register(definition) {
|
||||
if (this.modules.has(definition.name)) {
|
||||
Logger.warn(`[DependencyManager] Module '${definition.name}' already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate definition
|
||||
if (!this.validateDefinition(definition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modules.set(definition.name, definition);
|
||||
|
||||
// Build dependents map
|
||||
definition.dependencies.forEach(dep => {
|
||||
if (!this.dependents.has(dep.name)) {
|
||||
this.dependents.set(dep.name, []);
|
||||
}
|
||||
this.dependents.get(dep.name).push(definition.name);
|
||||
});
|
||||
|
||||
Logger.info(`[DependencyManager] Registered '${definition.name}' v${definition.version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate initialization order based on dependencies
|
||||
* @returns {string[]} Ordered list of module names
|
||||
*/
|
||||
calculateInitializationOrder() {
|
||||
const visited = new Set();
|
||||
const visiting = new Set();
|
||||
const order = [];
|
||||
|
||||
/**
|
||||
* Depth-first search with cycle detection
|
||||
* @param {string} moduleName
|
||||
*/
|
||||
const visit = (moduleName) => {
|
||||
if (visiting.has(moduleName)) {
|
||||
throw new Error(`Circular dependency detected involving '${moduleName}'`);
|
||||
}
|
||||
|
||||
if (visited.has(moduleName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const module = this.modules.get(moduleName);
|
||||
if (!module) {
|
||||
return; // Module not registered, might be optional
|
||||
}
|
||||
|
||||
visiting.add(moduleName);
|
||||
|
||||
// Visit dependencies first
|
||||
module.dependencies.forEach(dep => {
|
||||
if (!dep.optional || this.modules.has(dep.name)) {
|
||||
visit(dep.name);
|
||||
}
|
||||
});
|
||||
|
||||
visiting.delete(moduleName);
|
||||
visited.add(moduleName);
|
||||
order.push(moduleName);
|
||||
};
|
||||
|
||||
// Sort by priority first, then resolve dependencies
|
||||
const modulesByPriority = Array.from(this.modules.entries())
|
||||
.sort(([, a], [, b]) => (b.priority || 0) - (a.priority || 0))
|
||||
.map(([name]) => name);
|
||||
|
||||
modulesByPriority.forEach(moduleName => {
|
||||
if (!visited.has(moduleName)) {
|
||||
visit(moduleName);
|
||||
}
|
||||
});
|
||||
|
||||
this.initializationOrder = order;
|
||||
Logger.info(`[DependencyManager] Initialization order: ${order.join(' → ')}`);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all dependencies for a module are satisfied
|
||||
* @param {string} moduleName - Module to check
|
||||
* @returns {Object} Dependency check result
|
||||
*/
|
||||
checkDependencies(moduleName) {
|
||||
const module = this.modules.get(moduleName);
|
||||
if (!module) {
|
||||
return { satisfied: false, missing: [], reason: `Module '${moduleName}' not registered` };
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
const optional = [];
|
||||
|
||||
module.dependencies.forEach(dep => {
|
||||
const depModule = this.modules.get(dep.name);
|
||||
|
||||
if (!depModule) {
|
||||
if (dep.optional) {
|
||||
optional.push(dep.name);
|
||||
} else {
|
||||
missing.push(dep.name);
|
||||
}
|
||||
} else if (!this.initialized.has(dep.name)) {
|
||||
if (!dep.optional) {
|
||||
missing.push(`${dep.name} (not initialized)`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
satisfied: missing.length === 0,
|
||||
missing,
|
||||
optional,
|
||||
reason: missing.length > 0 ? `Missing: ${missing.join(', ')}` : 'OK'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark module as initialized
|
||||
* @param {string} moduleName - Module name
|
||||
*/
|
||||
markInitialized(moduleName) {
|
||||
this.initialized.add(moduleName);
|
||||
this.initializing.delete(moduleName);
|
||||
Logger.info(`[DependencyManager] '${moduleName}' initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark module as initializing
|
||||
* @param {string} moduleName - Module name
|
||||
*/
|
||||
markInitializing(moduleName) {
|
||||
this.initializing.add(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modules that depend on the given module
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {string[]} Dependent module names
|
||||
*/
|
||||
getDependents(moduleName) {
|
||||
return this.dependents.get(moduleName) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module definition
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {ModuleDefinition|undefined} Module definition
|
||||
*/
|
||||
getModule(moduleName) {
|
||||
return this.modules.get(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if module is ready to initialize
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {boolean} Ready status
|
||||
*/
|
||||
isReadyToInitialize(moduleName) {
|
||||
if (this.initialized.has(moduleName) || this.initializing.has(moduleName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const check = this.checkDependencies(moduleName);
|
||||
return check.satisfied;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get initialization status
|
||||
* @returns {Object} Status report
|
||||
*/
|
||||
getStatus() {
|
||||
const total = this.modules.size;
|
||||
const initialized = this.initialized.size;
|
||||
const initializing = this.initializing.size;
|
||||
const pending = total - initialized - initializing;
|
||||
|
||||
return {
|
||||
total,
|
||||
initialized,
|
||||
initializing,
|
||||
pending,
|
||||
modules: {
|
||||
initialized: Array.from(this.initialized),
|
||||
initializing: Array.from(this.initializing),
|
||||
pending: Array.from(this.modules.keys()).filter(name =>
|
||||
!this.initialized.has(name) && !this.initializing.has(name)
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate module definition
|
||||
* @private
|
||||
* @param {ModuleDefinition} definition - Module definition to validate
|
||||
* @returns {boolean} Validation result
|
||||
*/
|
||||
validateDefinition(definition) {
|
||||
if (!definition.name || typeof definition.name !== 'string') {
|
||||
Logger.error('[DependencyManager] Module name is required and must be string');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!definition.version || typeof definition.version !== 'string') {
|
||||
Logger.error(`[DependencyManager] Version is required for module '${definition.name}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(definition.dependencies)) {
|
||||
Logger.error(`[DependencyManager] Dependencies must be array for module '${definition.name}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate dependencies
|
||||
for (const dep of definition.dependencies) {
|
||||
if (!dep.name || typeof dep.name !== 'string') {
|
||||
Logger.error(`[DependencyManager] Invalid dependency in module '${definition.name}': missing name`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset dependency manager
|
||||
*/
|
||||
reset() {
|
||||
this.modules.clear();
|
||||
this.dependents.clear();
|
||||
this.initialized.clear();
|
||||
this.initializing.clear();
|
||||
this.initializationOrder = [];
|
||||
Logger.info('[DependencyManager] Reset complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create module definition helper
|
||||
* @param {string} name - Module name
|
||||
* @param {string} version - Module version
|
||||
* @returns {Object} Module definition builder
|
||||
*/
|
||||
static createDefinition(name, version) {
|
||||
const definition = {
|
||||
name,
|
||||
version,
|
||||
dependencies: [],
|
||||
provides: [],
|
||||
priority: 0
|
||||
};
|
||||
|
||||
// Create a chainable API
|
||||
const builder = {
|
||||
depends(moduleName, moduleVersion = '*', optional = false) {
|
||||
definition.dependencies.push({
|
||||
name: moduleName,
|
||||
version: moduleVersion,
|
||||
optional
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
provides(...services) {
|
||||
definition.provides.push(...services);
|
||||
return this;
|
||||
},
|
||||
|
||||
priority(level) {
|
||||
definition.priority = level;
|
||||
return this;
|
||||
},
|
||||
|
||||
// Return the final definition
|
||||
build() {
|
||||
return definition;
|
||||
}
|
||||
};
|
||||
|
||||
// Make the builder return the definition when accessed as an object
|
||||
Object.setPrototypeOf(builder, definition);
|
||||
|
||||
// Allow direct access to the definition properties
|
||||
Object.keys(definition).forEach(key => {
|
||||
if (!(key in builder)) {
|
||||
Object.defineProperty(builder, key, {
|
||||
get() { return definition[key]; },
|
||||
set(value) { definition[key] = value; },
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const dependencyManager = new DependencyManager();
|
||||
|
||||
// Debug access
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dependencyManager = dependencyManager;
|
||||
window.depStatus = () => dependencyManager.getStatus();
|
||||
}
|
||||
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();
|
||||
223
resources/js/core/ModuleErrorBoundary.js
Normal file
223
resources/js/core/ModuleErrorBoundary.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Error Boundary System für Module
|
||||
* Bietet Schutz vor Module-Crashes und Recovery-Mechanismen
|
||||
*/
|
||||
import { Logger } from './logger.js';
|
||||
|
||||
export class ModuleErrorBoundary {
|
||||
constructor() {
|
||||
this.crashedModules = new Set();
|
||||
this.recoveryAttempts = new Map();
|
||||
this.maxRecoveryAttempts = 3;
|
||||
this.recoveryDelay = 1000; // 1 second
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a module with error handling
|
||||
* @param {Object} module - The module to wrap
|
||||
* @param {string} moduleName - Name of the module for logging
|
||||
* @returns {Proxy} - Protected module instance
|
||||
*/
|
||||
wrapModule(module, moduleName) {
|
||||
if (!module || typeof module !== 'object') {
|
||||
Logger.warn(`[ErrorBoundary] Cannot wrap non-object module: ${moduleName}`);
|
||||
return module;
|
||||
}
|
||||
|
||||
return new Proxy(module, {
|
||||
get: (target, prop, receiver) => {
|
||||
const originalValue = target[prop];
|
||||
|
||||
// If property is not a function, return as-is
|
||||
if (typeof originalValue !== 'function') {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
// Check if property is non-configurable - return original if so
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||
if (descriptor && !descriptor.configurable) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
// Return wrapped function with error handling
|
||||
return (...args) => {
|
||||
try {
|
||||
const result = originalValue.apply(target, args);
|
||||
|
||||
// Handle async functions
|
||||
if (result && typeof result.catch === 'function') {
|
||||
return result.catch(error => {
|
||||
this.handleModuleError(error, moduleName, prop, args);
|
||||
return this.getRecoveryValue(moduleName, prop);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleModuleError(error, moduleName, prop, args);
|
||||
return this.getRecoveryValue(moduleName, prop);
|
||||
}
|
||||
};
|
||||
},
|
||||
getOwnPropertyDescriptor: (target, prop) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
||||
|
||||
// For non-configurable properties, return original descriptor
|
||||
if (descriptor && !descriptor.configurable) {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
},
|
||||
has: (target, prop) => {
|
||||
return prop in target;
|
||||
},
|
||||
ownKeys: (target) => {
|
||||
return Object.getOwnPropertyNames(target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles module errors with recovery logic
|
||||
* @param {Error} error - The error that occurred
|
||||
* @param {string} moduleName - Name of the failing module
|
||||
* @param {string} method - Method that failed
|
||||
* @param {Array} args - Arguments passed to the method
|
||||
*/
|
||||
handleModuleError(error, moduleName, method, args) {
|
||||
const errorKey = `${moduleName}.${method}`;
|
||||
|
||||
Logger.error(`[ErrorBoundary] Module ${moduleName} crashed in ${method}():`, error);
|
||||
|
||||
// Track crashed modules
|
||||
this.crashedModules.add(moduleName);
|
||||
|
||||
// Track recovery attempts
|
||||
const attempts = this.recoveryAttempts.get(errorKey) || 0;
|
||||
this.recoveryAttempts.set(errorKey, attempts + 1);
|
||||
|
||||
// Emit custom event for external handling
|
||||
window.dispatchEvent(new CustomEvent('module-error', {
|
||||
detail: {
|
||||
moduleName,
|
||||
method,
|
||||
error: error.message,
|
||||
args,
|
||||
attempts: attempts + 1
|
||||
}
|
||||
}));
|
||||
|
||||
// Attempt recovery if under limit
|
||||
if (attempts < this.maxRecoveryAttempts) {
|
||||
this.scheduleRecovery(moduleName, method, args);
|
||||
} else {
|
||||
Logger.error(`[ErrorBoundary] Module ${moduleName} exceeded recovery attempts. Marking as permanently failed.`);
|
||||
this.markModuleAsPermanentlyFailed(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a recovery attempt for a failed module method
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @param {string} method - Method to retry
|
||||
* @param {Array} args - Original arguments
|
||||
*/
|
||||
scheduleRecovery(moduleName, method, args) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
Logger.info(`[ErrorBoundary] Attempting recovery for ${moduleName}.${method}()`);
|
||||
// This would need module registry integration
|
||||
// For now, just log the attempt
|
||||
} catch (recoveryError) {
|
||||
Logger.error(`[ErrorBoundary] Recovery failed for ${moduleName}.${method}():`, recoveryError);
|
||||
}
|
||||
}, this.recoveryDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a safe fallback value for failed module methods
|
||||
* @param {string} moduleName - Name of the module
|
||||
* @param {string} method - Method that failed
|
||||
* @returns {*} - Safe fallback value
|
||||
*/
|
||||
getRecoveryValue(moduleName, method) {
|
||||
// Return safe defaults based on common method names
|
||||
switch (method) {
|
||||
case 'init':
|
||||
case 'destroy':
|
||||
case 'update':
|
||||
case 'render':
|
||||
return Promise.resolve();
|
||||
case 'getData':
|
||||
case 'getConfig':
|
||||
return {};
|
||||
case 'isEnabled':
|
||||
case 'isActive':
|
||||
return false;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a module as permanently failed
|
||||
* @param {string} moduleName - Name of the module
|
||||
*/
|
||||
markModuleAsPermanentlyFailed(moduleName) {
|
||||
window.dispatchEvent(new CustomEvent('module-permanent-failure', {
|
||||
detail: { moduleName }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets health status of all modules
|
||||
* @returns {Object} - Health status report
|
||||
*/
|
||||
getHealthStatus() {
|
||||
return {
|
||||
totalCrashedModules: this.crashedModules.size,
|
||||
crashedModules: Array.from(this.crashedModules),
|
||||
recoveryAttempts: Object.fromEntries(this.recoveryAttempts),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets error tracking for a module (useful for hot reload)
|
||||
* @param {string} moduleName - Name of the module to reset
|
||||
*/
|
||||
resetModule(moduleName) {
|
||||
this.crashedModules.delete(moduleName);
|
||||
|
||||
// Remove all recovery attempts for this module
|
||||
for (const [key] of this.recoveryAttempts) {
|
||||
if (key.startsWith(`${moduleName}.`)) {
|
||||
this.recoveryAttempts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(`[ErrorBoundary] Reset error tracking for module: ${moduleName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all error tracking
|
||||
*/
|
||||
reset() {
|
||||
this.crashedModules.clear();
|
||||
this.recoveryAttempts.clear();
|
||||
Logger.info('[ErrorBoundary] Reset all error tracking');
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const moduleErrorBoundary = new ModuleErrorBoundary();
|
||||
|
||||
// Global error handlers
|
||||
window.addEventListener('error', (event) => {
|
||||
Logger.error('[Global] Unhandled error:', event.error || event.message);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
Logger.error('[Global] Unhandled promise rejection:', event.reason);
|
||||
});
|
||||
286
resources/js/core/StateManager.js
Normal file
286
resources/js/core/StateManager.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Reactive State Management System für Module-Kommunikation
|
||||
* Ermöglicht type-safe state sharing zwischen Modulen
|
||||
*/
|
||||
import { Logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} StateChangeEvent
|
||||
* @property {string} key - State key that changed
|
||||
* @property {*} value - New value
|
||||
* @property {*} oldValue - Previous value
|
||||
* @property {string} source - Module that triggered the change
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StateSubscription
|
||||
* @property {string} id - Unique subscription ID
|
||||
* @property {Function} callback - Callback function
|
||||
* @property {string} subscriber - Module name that subscribed
|
||||
*/
|
||||
|
||||
export class StateManager {
|
||||
constructor() {
|
||||
/** @type {Map<string, any>} */
|
||||
this.state = new Map();
|
||||
|
||||
/** @type {Map<string, StateSubscription[]>} */
|
||||
this.subscribers = new Map();
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
this.stateOwners = new Map();
|
||||
|
||||
/** @type {Map<string, any>} */
|
||||
this.defaultValues = new Map();
|
||||
|
||||
/** @type {string} */
|
||||
this.currentModule = 'unknown';
|
||||
|
||||
this.subscriptionCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current module context for state operations
|
||||
* @param {string} moduleName - Name of the module
|
||||
*/
|
||||
setContext(moduleName) {
|
||||
this.currentModule = moduleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a state key with default value and owner
|
||||
* @param {string} key - State key
|
||||
* @param {*} defaultValue - Default value
|
||||
* @param {string} [owner] - Module that owns this state
|
||||
*/
|
||||
register(key, defaultValue, owner = this.currentModule) {
|
||||
if (this.state.has(key)) {
|
||||
Logger.warn(`[StateManager] State key '${key}' already registered by ${this.stateOwners.get(key)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.set(key, defaultValue);
|
||||
this.defaultValues.set(key, defaultValue);
|
||||
this.stateOwners.set(key, owner);
|
||||
this.subscribers.set(key, []);
|
||||
|
||||
Logger.info(`[StateManager] Registered '${key}' (owner: ${owner})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current value of state key
|
||||
* @param {string} key - State key
|
||||
* @returns {*} Current value or undefined
|
||||
*/
|
||||
get(key) {
|
||||
if (!this.state.has(key)) {
|
||||
Logger.warn(`[StateManager] Unknown state key: '${key}'`);
|
||||
return undefined;
|
||||
}
|
||||
return this.state.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value for state key (with ownership check)
|
||||
* @param {string} key - State key
|
||||
* @param {*} value - New value
|
||||
* @param {boolean} [force=false] - Force set even if not owner
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
set(key, value, force = false) {
|
||||
if (!this.state.has(key)) {
|
||||
Logger.warn(`[StateManager] Cannot set unknown state key: '${key}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const owner = this.stateOwners.get(key);
|
||||
if (!force && owner !== this.currentModule) {
|
||||
Logger.warn(`[StateManager] Module '${this.currentModule}' cannot modify '${key}' (owned by ${owner})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldValue = this.state.get(key);
|
||||
if (oldValue === value) {
|
||||
return true; // No change
|
||||
}
|
||||
|
||||
this.state.set(key, value);
|
||||
this.notifySubscribers(key, value, oldValue);
|
||||
|
||||
Logger.info(`[StateManager] Updated '${key}' by ${this.currentModule}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to state changes
|
||||
* @param {string} key - State key to watch
|
||||
* @param {Function} callback - Callback function (value, oldValue, key) => void
|
||||
* @param {string} [subscriber] - Subscriber module name
|
||||
* @returns {string} Subscription ID for unsubscribing
|
||||
*/
|
||||
subscribe(key, callback, subscriber = this.currentModule) {
|
||||
if (!this.state.has(key)) {
|
||||
Logger.warn(`[StateManager] Cannot subscribe to unknown state key: '${key}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const subscriptionId = `${subscriber}_${++this.subscriptionCounter}`;
|
||||
const subscription = {
|
||||
id: subscriptionId,
|
||||
callback,
|
||||
subscriber
|
||||
};
|
||||
|
||||
if (!this.subscribers.has(key)) {
|
||||
this.subscribers.set(key, []);
|
||||
}
|
||||
|
||||
this.subscribers.get(key).push(subscription);
|
||||
Logger.info(`[StateManager] Subscribed '${subscriber}' to '${key}'`);
|
||||
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from state changes
|
||||
* @param {string} subscriptionId - Subscription ID from subscribe()
|
||||
*/
|
||||
unsubscribe(subscriptionId) {
|
||||
for (const [key, subscriptions] of this.subscribers.entries()) {
|
||||
const index = subscriptions.findIndex(sub => sub.id === subscriptionId);
|
||||
if (index !== -1) {
|
||||
const subscription = subscriptions[index];
|
||||
subscriptions.splice(index, 1);
|
||||
Logger.info(`[StateManager] Unsubscribed '${subscription.subscriber}' from '${key}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger.warn(`[StateManager] Subscription ID not found: ${subscriptionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all subscribers of a state change
|
||||
* @private
|
||||
* @param {string} key - State key
|
||||
* @param {*} value - New value
|
||||
* @param {*} oldValue - Previous value
|
||||
*/
|
||||
notifySubscribers(key, value, oldValue) {
|
||||
const subscriptions = this.subscribers.get(key) || [];
|
||||
|
||||
subscriptions.forEach(subscription => {
|
||||
try {
|
||||
subscription.callback(value, oldValue, key);
|
||||
} catch (error) {
|
||||
Logger.error(`[StateManager] Error in subscriber '${subscription.subscriber}' for '${key}':`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state key to default value
|
||||
* @param {string} key - State key to reset
|
||||
* @returns {boolean} Success status
|
||||
*/
|
||||
reset(key) {
|
||||
if (!this.state.has(key)) {
|
||||
Logger.warn(`[StateManager] Cannot reset unknown state key: '${key}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const defaultValue = this.defaultValues.get(key);
|
||||
return this.set(key, defaultValue, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all subscriptions for a module (useful for cleanup)
|
||||
* @param {string} moduleName - Module name
|
||||
*/
|
||||
clearModuleSubscriptions(moduleName) {
|
||||
let cleared = 0;
|
||||
|
||||
for (const [key, subscriptions] of this.subscribers.entries()) {
|
||||
const filtered = subscriptions.filter(sub => sub.subscriber !== moduleName);
|
||||
cleared += subscriptions.length - filtered.length;
|
||||
this.subscribers.set(key, filtered);
|
||||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
Logger.info(`[StateManager] Cleared ${cleared} subscriptions for module '${moduleName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current state snapshot (for debugging)
|
||||
* @returns {Object} Current state and metadata
|
||||
*/
|
||||
getSnapshot() {
|
||||
const snapshot = {
|
||||
state: Object.fromEntries(this.state),
|
||||
owners: Object.fromEntries(this.stateOwners),
|
||||
subscriptions: {}
|
||||
};
|
||||
|
||||
for (const [key, subs] of this.subscribers.entries()) {
|
||||
snapshot.subscriptions[key] = subs.map(sub => ({
|
||||
id: sub.id,
|
||||
subscriber: sub.subscriber
|
||||
}));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset entire state manager (useful for testing)
|
||||
*/
|
||||
resetAll() {
|
||||
this.state.clear();
|
||||
this.subscribers.clear();
|
||||
this.stateOwners.clear();
|
||||
this.defaultValues.clear();
|
||||
this.subscriptionCounter = 0;
|
||||
Logger.info('[StateManager] Reset complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scoped state manager for a specific module
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Object} Scoped state interface
|
||||
*/
|
||||
createScope(moduleName) {
|
||||
return {
|
||||
register: (key, defaultValue) => {
|
||||
this.setContext(moduleName);
|
||||
return this.register(key, defaultValue, moduleName);
|
||||
},
|
||||
|
||||
get: (key) => this.get(key),
|
||||
|
||||
set: (key, value) => {
|
||||
this.setContext(moduleName);
|
||||
return this.set(key, value);
|
||||
},
|
||||
|
||||
subscribe: (key, callback) => {
|
||||
this.setContext(moduleName);
|
||||
return this.subscribe(key, callback, moduleName);
|
||||
},
|
||||
|
||||
unsubscribe: (subscriptionId) => this.unsubscribe(subscriptionId),
|
||||
|
||||
reset: (key) => this.reset(key),
|
||||
|
||||
cleanup: () => this.clearModuleSubscriptions(moduleName)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
export const stateManager = new StateManager();
|
||||
|
||||
// Debug access
|
||||
if (typeof window !== 'undefined') {
|
||||
window.stateManager = stateManager;
|
||||
window.stateSnapshot = () => stateManager.getSnapshot();
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
// modules/core/frameloop.js
|
||||
import {Logger} from "./logger";
|
||||
|
||||
/**
|
||||
* @typedef {Object} FrameTaskOptions
|
||||
* @property {boolean} [autoStart=false] - Automatically start the frame loop
|
||||
*/
|
||||
|
||||
/** @type {Map<string, Function>} */
|
||||
const tasks = new Map();
|
||||
/** @type {boolean} */
|
||||
let running = false;
|
||||
/** @type {boolean} */
|
||||
let showDebug = false;
|
||||
|
||||
let lastTime = performance.now();
|
||||
let frameCount = 0;
|
||||
let fps = 0;
|
||||
|
||||
|
||||
// Create debug overlay
|
||||
const debugOverlay = document.createElement('div');
|
||||
debugOverlay.style.position = 'fixed';
|
||||
debugOverlay.style.bottom = '0';
|
||||
@@ -28,8 +36,10 @@ debugOverlay.style.lineHeight = '1.4';
|
||||
document.body.appendChild(debugOverlay);
|
||||
|
||||
import { PerformanceMonitor } from './PerformanceMonitor.js';
|
||||
/** @type {PerformanceMonitor} */
|
||||
export const monitor = new PerformanceMonitor();
|
||||
|
||||
// Debug toggle with § key
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === '§') {
|
||||
showDebug = !showDebug;
|
||||
@@ -37,19 +47,35 @@ window.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Register a task to run on every frame
|
||||
* @param {string} id - Unique identifier for the task
|
||||
* @param {Function} callback - Function to execute each frame
|
||||
* @param {FrameTaskOptions} [options={}] - Task options
|
||||
*/
|
||||
export function registerFrameTask(id, callback, options = {}) {
|
||||
tasks.set(id, callback);
|
||||
if (options.autoStart && !running) startFrameLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a frame task
|
||||
* @param {string} id - Task identifier to remove
|
||||
*/
|
||||
export function unregisterFrameTask(id) {
|
||||
tasks.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered frame tasks
|
||||
*/
|
||||
export function clearFrameTasks() {
|
||||
tasks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the main frame loop
|
||||
*/
|
||||
export function startFrameLoop() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
@@ -77,6 +103,10 @@ export function startFrameLoop() {
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the frame loop
|
||||
* Note: Loop continues until next frame, this just sets the flag
|
||||
*/
|
||||
export function stopFrameLoop() {
|
||||
running = false;
|
||||
// Achtung: Loop läuft weiter, solange nicht aktiv gestoppt
|
||||
|
||||
@@ -8,8 +8,8 @@ closeBtn.addEventListener("click", () => {
|
||||
*/
|
||||
|
||||
|
||||
import {registerModules} from "../modules";
|
||||
|
||||
import { registerModules, activeModules } from "../modules/index.js";
|
||||
import { Logger } from './logger.js';
|
||||
import {useEvent} from "./useEvent";
|
||||
|
||||
|
||||
@@ -100,13 +100,184 @@ document.startViewTransition(() => {
|
||||
container.innerHTML = newContent;
|
||||
});*/
|
||||
|
||||
import { fadeScrollTrigger, zoomScrollTrigger, fixedZoomScrollTrigger } from '../modules/scrollfx/Tween.js';
|
||||
import {autoLoadResponsiveVideos} from "../utils/autoLoadResponsiveVideo";
|
||||
|
||||
/**
|
||||
* Initialize DOM elements with data-module attributes
|
||||
*/
|
||||
function initDataModuleElements() {
|
||||
const elements = document.querySelectorAll('[data-module]');
|
||||
Logger.info(`[DOMInit] Found ${elements.length} elements with data-module attributes`);
|
||||
|
||||
elements.forEach(element => {
|
||||
const moduleName = element.dataset.module;
|
||||
const moduleData = activeModules.get(moduleName);
|
||||
|
||||
if (!moduleData || !moduleData.mod) {
|
||||
Logger.warn(`[DOMInit] Module "${moduleName}" not found or failed to initialize`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse options from data-options attribute
|
||||
let options = {};
|
||||
try {
|
||||
if (element.dataset.options) {
|
||||
options = JSON.parse(element.dataset.options);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn(`[DOMInit] Invalid JSON in data-options for ${moduleName}:`, error);
|
||||
}
|
||||
|
||||
// Initialize the module on this element
|
||||
try {
|
||||
const moduleInstance = moduleData.mod;
|
||||
|
||||
// Check for element-specific init method first, then fallback to general init
|
||||
if (typeof moduleInstance.initElement === 'function') {
|
||||
const result = moduleInstance.initElement(element, options);
|
||||
Logger.info(`[DOMInit] Initialized ${moduleName} on element:`, element);
|
||||
|
||||
// Store reference for cleanup
|
||||
element._moduleInstance = result;
|
||||
element._moduleName = moduleName;
|
||||
} else if (typeof moduleInstance.init === 'function') {
|
||||
const result = moduleInstance.init(element, options);
|
||||
Logger.info(`[DOMInit] Initialized ${moduleName} on element:`, element);
|
||||
|
||||
// Store reference for cleanup
|
||||
element._moduleInstance = result;
|
||||
element._moduleName = moduleName;
|
||||
} else {
|
||||
Logger.warn(`[DOMInit] Module ${moduleName} has no init method for DOM elements`);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[DOMInit] Failed to initialize ${moduleName} on element:`, error, element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize SPA Router for seamless navigation
|
||||
*/
|
||||
function initSPARouter() {
|
||||
const spaRouterModule = activeModules.get('spa-router');
|
||||
|
||||
if (!spaRouterModule || !spaRouterModule.mod) {
|
||||
Logger.info('[Init] SPA Router module not available, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize SPA Router with default configuration
|
||||
const router = spaRouterModule.mod.init({
|
||||
containerSelector: 'main',
|
||||
enableTransitions: true,
|
||||
transitionDuration: 300
|
||||
});
|
||||
|
||||
// Make functions globally available for re-initialization
|
||||
window.initAutoFormHandling = initAutoFormHandling;
|
||||
window.initDataModuleElements = initDataModuleElements;
|
||||
|
||||
Logger.info('[Init] SPA Router initialized successfully');
|
||||
} catch (error) {
|
||||
Logger.error('[Init] Failed to initialize SPA Router:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-initialize all forms with form-handling (opt-out approach)
|
||||
*/
|
||||
function initAutoFormHandling() {
|
||||
// Find all forms except those explicitly opting out and those already enhanced
|
||||
const forms = document.querySelectorAll('form:not([data-form-handling="false"]):not([data-auto-enhanced])');
|
||||
Logger.info(`[AutoForms] Found ${forms.length} forms for auto-enhancement`);
|
||||
|
||||
if (forms.length === 0) return;
|
||||
|
||||
// Check if form-handling module is available
|
||||
const formHandlingModule = activeModules.get('form-handling');
|
||||
if (!formHandlingModule || !formHandlingModule.mod) {
|
||||
Logger.warn('[AutoForms] form-handling module not available, skipping auto-init');
|
||||
return;
|
||||
}
|
||||
|
||||
forms.forEach(form => {
|
||||
// Skip forms that already have explicit data-module
|
||||
if (form.hasAttribute('data-module')) {
|
||||
Logger.info(`[AutoForms] Skipping form with explicit data-module:`, form);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip forms that are already enhanced (double-check)
|
||||
if (form.hasAttribute('data-auto-enhanced')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse options from data-form-options attribute
|
||||
let options = {};
|
||||
try {
|
||||
if (form.dataset.formOptions) {
|
||||
options = JSON.parse(form.dataset.formOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn('[AutoForms] Invalid JSON in data-form-options:', error);
|
||||
}
|
||||
|
||||
// Set default options for auto-enhanced forms
|
||||
const autoOptions = {
|
||||
validateOnSubmit: true,
|
||||
validateOnBlur: false,
|
||||
validateOnInput: false,
|
||||
showInlineErrors: true,
|
||||
ajaxSubmit: true,
|
||||
enableStateTracking: false, // Conservative default
|
||||
...options
|
||||
};
|
||||
|
||||
// Initialize form handling
|
||||
try {
|
||||
// Use initElement for DOM-specific initialization
|
||||
const result = formHandlingModule.mod.initElement ?
|
||||
formHandlingModule.mod.initElement(form, autoOptions) :
|
||||
formHandlingModule.mod.init(form, autoOptions);
|
||||
|
||||
Logger.info('[AutoForms] Auto-enhanced form:', {
|
||||
id: form.id || 'unnamed',
|
||||
action: form.action || 'none',
|
||||
method: form.method || 'get',
|
||||
elements: form.elements.length
|
||||
});
|
||||
|
||||
// Mark as auto-enhanced
|
||||
form.setAttribute('data-auto-enhanced', 'true');
|
||||
form._moduleInstance = result;
|
||||
form._moduleName = 'form-handling';
|
||||
} catch (error) {
|
||||
Logger.error('[AutoForms] Failed to auto-enhance form:', error, form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the application
|
||||
* Sets up all modules and core functionality
|
||||
* @async
|
||||
* @function initApp
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function initApp() {
|
||||
|
||||
await registerModules();
|
||||
|
||||
// Initialize DOM elements after modules are registered
|
||||
initDataModuleElements();
|
||||
|
||||
// Auto-enhance all forms with form-handling
|
||||
initAutoFormHandling();
|
||||
|
||||
// Initialize SPA Router for seamless navigation
|
||||
initSPARouter();
|
||||
|
||||
autoLoadResponsiveVideos();
|
||||
|
||||
|
||||
@@ -1,37 +1,135 @@
|
||||
import {monitor} from "./frameloop";
|
||||
|
||||
/**
|
||||
* Centralized logging system with performance monitoring integration
|
||||
* @class Logger
|
||||
*/
|
||||
export class Logger {
|
||||
static enabled = true //import.meta.env.MODE !== 'production';
|
||||
/** @type {boolean} Enable/disable logging */
|
||||
static enabled = import.meta.env.MODE !== 'production';
|
||||
|
||||
/** @type {string} Log level: 'debug', 'info', 'warn', 'error', 'none' */
|
||||
static level = import.meta.env.VITE_LOG_LEVEL || (import.meta.env.MODE === 'production' ? 'error' : 'debug');
|
||||
|
||||
/** @type {Object} Log level priorities */
|
||||
static levels = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
none: 99
|
||||
};
|
||||
|
||||
/**
|
||||
* Log debug messages (development only)
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
static debug(...args) {
|
||||
this._write('log', '[DEBUG]', args, 'debug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log general information
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
static log(...args) {
|
||||
this._write('log', '[LOG]', args);
|
||||
}
|
||||
|
||||
static warn(...args) {
|
||||
this._write('warn', '[WARN]', args)
|
||||
this._write('log', '[LOG]', args, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log informational messages
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
static info(...args) {
|
||||
this._write('info', '[INFO]', args);
|
||||
this._write('info', '[INFO]', args, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning messages
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
static warn(...args) {
|
||||
this._write('warn', '[WARN]', args, 'warn');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error messages
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
static error(...args) {
|
||||
this._write('error', '[ERROR]', args);
|
||||
this._write('error', '[ERROR]', args, 'error');
|
||||
}
|
||||
|
||||
static _write(consoleMethod, prefix, args) {
|
||||
if(!this.enabled) return;
|
||||
/**
|
||||
* Internal method to write log messages
|
||||
* @private
|
||||
* @param {string} consoleMethod - Console method to use
|
||||
* @param {string} prefix - Log level prefix
|
||||
* @param {Array} args - Arguments to log
|
||||
* @param {string} logLevel - Log level (debug, info, warn, error)
|
||||
*/
|
||||
static _write(consoleMethod, prefix, args, logLevel = 'info') {
|
||||
if (!this.enabled) return;
|
||||
|
||||
// Check if this log level should be output
|
||||
const currentLevelPriority = this.levels[this.level] || this.levels.info;
|
||||
const messageLevelPriority = this.levels[logLevel] || this.levels.info;
|
||||
|
||||
if (messageLevelPriority < currentLevelPriority) {
|
||||
return;
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const timestamp = date.toLocaleTimeString('de-DE');
|
||||
|
||||
const msg = `${prefix} [${timestamp}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
|
||||
const msg = `${prefix} [${timestamp}] ${args.map(a => {
|
||||
if (a instanceof Error) {
|
||||
return `${a.name}: ${a.message}\n${a.stack || ''}`;
|
||||
} else if (typeof a === 'object' && a !== null) {
|
||||
try {
|
||||
return JSON.stringify(a);
|
||||
} catch (e) {
|
||||
return '[Circular Object]';
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}).join(' ')}`;
|
||||
|
||||
if(typeof console[consoleMethod] === 'function') {
|
||||
console[consoleMethod](msg);
|
||||
}
|
||||
|
||||
monitor?.log(msg)
|
||||
// Safe monitor logging
|
||||
if (monitor && typeof monitor.log === 'function') {
|
||||
try {
|
||||
monitor.log(msg);
|
||||
} catch (e) {
|
||||
// Silent fail if monitor fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set log level dynamically
|
||||
* @param {string} level - New log level (debug, info, warn, error, none)
|
||||
*/
|
||||
static setLevel(level) {
|
||||
if (this.levels.hasOwnProperty(level)) {
|
||||
this.level = level;
|
||||
this.info(`[Logger] Log level set to: ${level}`);
|
||||
} else {
|
||||
this.warn(`[Logger] Invalid log level: ${level}. Valid levels:`, Object.keys(this.levels));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable logging
|
||||
* @param {boolean} enabled - Enable or disable logging
|
||||
*/
|
||||
static setEnabled(enabled) {
|
||||
this.enabled = !!enabled;
|
||||
if (enabled) {
|
||||
console.log('[Logger] Logging enabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
resources/js/core/prefetch-config.js
Normal file
78
resources/js/core/prefetch-config.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Prefetching Configuration Examples
|
||||
*
|
||||
* Use these configurations with startRouter() or directly with LinkPrefetcher
|
||||
*/
|
||||
|
||||
// Default configuration - balanced performance
|
||||
export const defaultConfig = {
|
||||
strategies: ['hover', 'visible'], // Prefetch on hover and when visible
|
||||
hoverDelay: 150, // Wait 150ms before prefetching on hover
|
||||
observerMargin: '50px', // Start prefetching when link is 50px from viewport
|
||||
maxCacheSize: 20, // Cache up to 20 pages
|
||||
cacheTTL: 60000, // Cache for 60 seconds
|
||||
priority: 'low' // Use low priority for network requests
|
||||
};
|
||||
|
||||
// Aggressive prefetching - for fast networks
|
||||
export const aggressiveConfig = {
|
||||
strategies: ['hover', 'visible', 'eager'],
|
||||
hoverDelay: 50, // Faster hover response
|
||||
observerMargin: '200px', // Prefetch earlier
|
||||
maxCacheSize: 50, // Larger cache
|
||||
cacheTTL: 300000, // Cache for 5 minutes
|
||||
priority: 'high' // High priority requests
|
||||
};
|
||||
|
||||
// Conservative prefetching - for slow networks or mobile
|
||||
export const conservativeConfig = {
|
||||
strategies: ['hover'], // Only prefetch on hover
|
||||
hoverDelay: 300, // Wait longer before prefetching
|
||||
observerMargin: '0px', // Don't use intersection observer
|
||||
maxCacheSize: 10, // Smaller cache
|
||||
cacheTTL: 30000, // Cache for 30 seconds
|
||||
priority: 'low' // Low priority
|
||||
};
|
||||
|
||||
// Minimal prefetching - disabled by default
|
||||
export const minimalConfig = {
|
||||
strategies: [], // No automatic prefetching
|
||||
maxCacheSize: 5, // Minimal cache
|
||||
cacheTTL: 15000, // Short cache duration
|
||||
};
|
||||
|
||||
// Adaptive configuration based on connection
|
||||
export function getAdaptiveConfig() {
|
||||
// Use Network Information API if available
|
||||
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
|
||||
if (!connection) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Check effective connection type
|
||||
if (connection.effectiveType === '4g' && !connection.saveData) {
|
||||
return aggressiveConfig;
|
||||
} else if (connection.effectiveType === '3g' || connection.saveData) {
|
||||
return conservativeConfig;
|
||||
} else if (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g') {
|
||||
return minimalConfig;
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Custom configuration for specific pages
|
||||
export function getPageSpecificConfig(pathname) {
|
||||
// Disable prefetching on certain pages
|
||||
if (pathname.includes('/admin') || pathname.includes('/checkout')) {
|
||||
return minimalConfig;
|
||||
}
|
||||
|
||||
// Aggressive prefetching on landing pages
|
||||
if (pathname === '/' || pathname === '/products') {
|
||||
return aggressiveConfig;
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
}
|
||||
@@ -90,9 +90,10 @@ function animateLayoutSwitch(type) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Router
|
||||
* Startet den Router mit optionaler Prefetch-Konfiguration
|
||||
* @param {Object} prefetchOptions - Optionen für das Prefetching-System
|
||||
*/
|
||||
export function startRouter() {
|
||||
export function startRouter(prefetchOptions = {}) {
|
||||
initClickManager((href, link, options) => {
|
||||
if (!runGuard(href)) return;
|
||||
|
||||
@@ -122,7 +123,7 @@ export function startRouter() {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, prefetchOptions);
|
||||
|
||||
// Bei Seitenstart erste Route prüfen
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -65,6 +65,7 @@ export class SecureLogger {
|
||||
return response.json();
|
||||
})
|
||||
.catch(err => {
|
||||
// Fallback auf console.error da wir hier im Logger sind
|
||||
console.error('Fehler beim Senden des Logs:', err);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user