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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -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;
}

View 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();
}

View 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();

View 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);
});

View 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();
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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');
}
}
}

View 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;
}

View File

@@ -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', () => {

View File

@@ -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);
});
}