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

@@ -0,0 +1,555 @@
// modules/api-manager/AnimationManager.js
import { Logger } from '../../core/logger.js';
/**
* Web Animations API Manager - High-performance animations with timeline control
*/
export class AnimationManager {
constructor(config = {}) {
this.config = config;
this.activeAnimations = new Map();
this.animationGroups = new Map();
this.defaultEasing = config.easing || 'ease-out';
this.defaultDuration = config.duration || 300;
// Check for Web Animations API support
this.supported = 'animate' in Element.prototype;
if (!this.supported) {
Logger.warn('[AnimationManager] Web Animations API not supported, using fallbacks');
} else {
Logger.info('[AnimationManager] Initialized with Web Animations API support');
}
}
/**
* Animate element using Web Animations API
*/
animate(element, keyframes, options = {}) {
if (!element || !keyframes) {
Logger.warn('[AnimationManager] Missing element or keyframes');
return null;
}
const animationOptions = {
duration: this.defaultDuration,
easing: this.defaultEasing,
fill: 'forwards',
...options
};
const animationId = this.generateId();
if (this.supported) {
const animation = element.animate(keyframes, animationOptions);
// Enhanced animation object
const enhancedAnimation = this.enhanceAnimation(animation, animationId, {
element,
keyframes,
options: animationOptions
});
this.activeAnimations.set(animationId, enhancedAnimation);
// Auto-cleanup on finish
animation.addEventListener('finish', () => {
this.activeAnimations.delete(animationId);
});
Logger.info(`[AnimationManager] Animation started: ${animationId}`);
return enhancedAnimation;
} else {
return this.fallbackAnimate(element, keyframes, animationOptions, animationId);
}
}
/**
* Create keyframe animations
*/
keyframes(keyframeSet) {
// Convert CSS-like keyframes to Web Animations format
if (Array.isArray(keyframeSet)) {
return keyframeSet;
}
// Handle object format: { '0%': {...}, '50%': {...}, '100%': {...} }
if (typeof keyframeSet === 'object') {
const frames = [];
const sortedKeys = Object.keys(keyframeSet).sort((a, b) => {
const aPercent = parseFloat(a.replace('%', ''));
const bPercent = parseFloat(b.replace('%', ''));
return aPercent - bPercent;
});
sortedKeys.forEach(key => {
const offset = parseFloat(key.replace('%', '')) / 100;
frames.push({
...keyframeSet[key],
offset
});
});
return frames;
}
return keyframeSet;
}
/**
* Pre-defined animation effects
*/
effects = {
// Entrance animations
fadeIn: (duration = 300) => ([
{ opacity: 0 },
{ opacity: 1 }
]),
slideInLeft: (distance = '100px', duration = 300) => ([
{ transform: `translateX(-${distance})`, opacity: 0 },
{ transform: 'translateX(0)', opacity: 1 }
]),
slideInRight: (distance = '100px', duration = 300) => ([
{ transform: `translateX(${distance})`, opacity: 0 },
{ transform: 'translateX(0)', opacity: 1 }
]),
slideInUp: (distance = '100px', duration = 300) => ([
{ transform: `translateY(${distance})`, opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
]),
slideInDown: (distance = '100px', duration = 300) => ([
{ transform: `translateY(-${distance})`, opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
]),
scaleIn: (scale = 0.8, duration = 300) => ([
{ transform: `scale(${scale})`, opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
]),
rotateIn: (rotation = '-180deg', duration = 600) => ([
{ transform: `rotate(${rotation})`, opacity: 0 },
{ transform: 'rotate(0deg)', opacity: 1 }
]),
// Exit animations
fadeOut: (duration = 300) => ([
{ opacity: 1 },
{ opacity: 0 }
]),
slideOutLeft: (distance = '100px', duration = 300) => ([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: `translateX(-${distance})`, opacity: 0 }
]),
slideOutRight: (distance = '100px', duration = 300) => ([
{ transform: 'translateX(0)', opacity: 1 },
{ transform: `translateX(${distance})`, opacity: 0 }
]),
scaleOut: (scale = 0.8, duration = 300) => ([
{ transform: 'scale(1)', opacity: 1 },
{ transform: `scale(${scale})`, opacity: 0 }
]),
// Attention seekers
bounce: (intensity = '20px', duration = 600) => ([
{ transform: 'translateY(0)' },
{ transform: `translateY(-${intensity})`, offset: 0.25 },
{ transform: 'translateY(0)', offset: 0.5 },
{ transform: `translateY(-${intensity})`, offset: 0.75 },
{ transform: 'translateY(0)' }
]),
pulse: (scale = 1.1, duration = 600) => ([
{ transform: 'scale(1)' },
{ transform: `scale(${scale})`, offset: 0.5 },
{ transform: 'scale(1)' }
]),
shake: (distance = '10px', duration = 600) => ([
{ transform: 'translateX(0)' },
{ transform: `translateX(-${distance})`, offset: 0.1 },
{ transform: `translateX(${distance})`, offset: 0.2 },
{ transform: `translateX(-${distance})`, offset: 0.3 },
{ transform: `translateX(${distance})`, offset: 0.4 },
{ transform: `translateX(-${distance})`, offset: 0.5 },
{ transform: `translateX(${distance})`, offset: 0.6 },
{ transform: `translateX(-${distance})`, offset: 0.7 },
{ transform: `translateX(${distance})`, offset: 0.8 },
{ transform: `translateX(-${distance})`, offset: 0.9 },
{ transform: 'translateX(0)' }
]),
rubberBand: (duration = 1000) => ([
{ transform: 'scale(1)' },
{ transform: 'scale(1.25, 0.75)', offset: 0.3 },
{ transform: 'scale(0.75, 1.25)', offset: 0.4 },
{ transform: 'scale(1.15, 0.85)', offset: 0.5 },
{ transform: 'scale(0.95, 1.05)', offset: 0.65 },
{ transform: 'scale(1.05, 0.95)', offset: 0.75 },
{ transform: 'scale(1)' }
]),
// Loading animations
spin: (duration = 1000) => ([
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
]),
heartbeat: (scale = 1.3, duration = 1000) => ([
{ transform: 'scale(1)' },
{ transform: `scale(${scale})`, offset: 0.14 },
{ transform: 'scale(1)', offset: 0.28 },
{ transform: `scale(${scale})`, offset: 0.42 },
{ transform: 'scale(1)', offset: 0.70 }
])
};
/**
* Predefined easing functions
*/
easings = {
linear: 'linear',
ease: 'ease',
easeIn: 'ease-in',
easeOut: 'ease-out',
easeInOut: 'ease-in-out',
// Custom cubic-bezier easings
easeInQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
easeOutQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
easeInOutQuad: 'cubic-bezier(0.455, 0.03, 0.515, 0.955)',
easeInCubic: 'cubic-bezier(0.32, 0, 0.67, 0)',
easeOutCubic: 'cubic-bezier(0.33, 1, 0.68, 1)',
easeInOutCubic: 'cubic-bezier(0.65, 0, 0.35, 1)',
easeInQuart: 'cubic-bezier(0.5, 0, 0.75, 0)',
easeOutQuart: 'cubic-bezier(0.25, 1, 0.5, 1)',
easeInOutQuart: 'cubic-bezier(0.76, 0, 0.24, 1)',
// Bouncy easings
easeOutBack: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
easeInBack: 'cubic-bezier(0.36, 0, 0.66, -0.56)',
easeInOutBack: 'cubic-bezier(0.68, -0.6, 0.32, 1.6)'
};
/**
* Quick effect methods
*/
fadeIn(element, options = {}) {
return this.animate(element, this.effects.fadeIn(), {
duration: 300,
easing: this.easings.easeOut,
...options
});
}
fadeOut(element, options = {}) {
return this.animate(element, this.effects.fadeOut(), {
duration: 300,
easing: this.easings.easeIn,
...options
});
}
slideIn(element, direction = 'up', options = {}) {
const effectMap = {
up: 'slideInUp',
down: 'slideInDown',
left: 'slideInLeft',
right: 'slideInRight'
};
return this.animate(element, this.effects[effectMap[direction]](), {
duration: 400,
easing: this.easings.easeOutBack,
...options
});
}
bounce(element, options = {}) {
return this.animate(element, this.effects.bounce(), {
duration: 600,
easing: this.easings.easeInOut,
...options
});
}
pulse(element, options = {}) {
return this.animate(element, this.effects.pulse(), {
duration: 600,
easing: this.easings.easeInOut,
iterations: Infinity,
...options
});
}
/**
* Animation groups - animate multiple elements together
*/
group(animations, options = {}) {
const groupId = this.generateId('group');
const animationPromises = [];
animations.forEach(({ element, keyframes, animationOptions = {} }) => {
const animation = this.animate(element, keyframes, {
...animationOptions,
...options
});
if (animation && animation.finished) {
animationPromises.push(animation.finished);
}
});
const groupController = {
id: groupId,
finished: Promise.all(animationPromises),
play: () => {
animations.forEach(anim => {
if (anim.animation) anim.animation.play();
});
},
pause: () => {
animations.forEach(anim => {
if (anim.animation) anim.animation.pause();
});
},
reverse: () => {
animations.forEach(anim => {
if (anim.animation) anim.animation.reverse();
});
},
cancel: () => {
animations.forEach(anim => {
if (anim.animation) anim.animation.cancel();
});
}
};
this.animationGroups.set(groupId, groupController);
return groupController;
}
/**
* Staggered animations - animate elements with delay
*/
stagger(elements, keyframes, options = {}) {
const staggerDelay = options.staggerDelay || 100;
const animations = [];
elements.forEach((element, index) => {
const delay = index * staggerDelay;
const animation = this.animate(element, keyframes, {
...options,
delay
});
animations.push({ element, animation });
});
return this.group(animations.map(({ element, animation }) => ({
element,
keyframes,
animation
})));
}
/**
* Timeline animations - sequence animations
*/
timeline(sequence) {
const timelineId = this.generateId('timeline');
let currentTime = 0;
const animations = [];
sequence.forEach(step => {
const delay = step.at !== undefined ? step.at : currentTime;
const animation = this.animate(step.element, step.keyframes, {
...step.options,
delay
});
animations.push(animation);
if (step.at === undefined) {
currentTime += step.options?.duration || this.defaultDuration;
}
});
return {
id: timelineId,
animations,
finished: Promise.all(animations.map(a => a.finished)),
play: () => animations.forEach(a => a.play()),
pause: () => animations.forEach(a => a.pause()),
reverse: () => animations.forEach(a => a.reverse()),
cancel: () => animations.forEach(a => a.cancel())
};
}
/**
* Scroll-triggered animations
*/
onScroll(element, keyframes, options = {}) {
const scrollTrigger = options.trigger || element;
const startOffset = options.start || 0;
const endOffset = options.end || 1;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const progress = Math.min(Math.max(
(entry.intersectionRatio - startOffset) / (endOffset - startOffset),
0
), 1);
if (progress >= 0) {
this.animate(element, keyframes, {
...options,
duration: options.duration || this.defaultDuration
});
}
}
});
}, {
threshold: this.createThresholdArray(20)
});
observer.observe(scrollTrigger);
return {
observer,
disconnect: () => observer.disconnect()
};
}
// Helper methods
enhanceAnimation(animation, id, metadata) {
return {
id,
animation,
metadata,
// Enhanced controls
play: () => animation.play(),
pause: () => animation.pause(),
reverse: () => animation.reverse(),
cancel: () => animation.cancel(),
finish: () => animation.finish(),
// Properties
get currentTime() { return animation.currentTime; },
set currentTime(time) { animation.currentTime = time; },
get playbackRate() { return animation.playbackRate; },
set playbackRate(rate) { animation.playbackRate = rate; },
get playState() { return animation.playState; },
get finished() { return animation.finished; },
// Enhanced methods
seek(progress) {
animation.currentTime = animation.effect.getTiming().duration * progress;
},
setProgress(progress) {
this.seek(Math.max(0, Math.min(1, progress)));
},
onFinish(callback) {
animation.addEventListener('finish', callback);
},
onCancel(callback) {
animation.addEventListener('cancel', callback);
}
};
}
fallbackAnimate(element, keyframes, options, id) {
Logger.info('[AnimationManager] Using CSS fallback animation');
// Simple CSS transition fallback
const finalFrame = keyframes[keyframes.length - 1];
element.style.transition = `all ${options.duration}ms ${options.easing}`;
// Apply final styles
Object.assign(element.style, finalFrame);
// Return promise-like object
return {
id,
finished: new Promise(resolve => setTimeout(resolve, options.duration)),
play: () => {},
pause: () => {},
cancel: () => {
element.style.transition = '';
}
};
}
generateId(prefix = 'anim') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
createThresholdArray(steps) {
const thresholds = [];
for (let i = 0; i <= steps; i++) {
thresholds.push(i / steps);
}
return thresholds;
}
/**
* Get all active animations
*/
getActiveAnimations() {
return Array.from(this.activeAnimations.entries()).map(([id, animation]) => ({
id,
playState: animation.playState,
currentTime: animation.currentTime,
metadata: animation.metadata
}));
}
/**
* Cancel all active animations
*/
cancelAll() {
this.activeAnimations.forEach(animation => {
animation.cancel();
});
this.activeAnimations.clear();
Logger.info('[AnimationManager] All animations cancelled');
}
/**
* Pause all active animations
*/
pauseAll() {
this.activeAnimations.forEach(animation => {
animation.pause();
});
Logger.info('[AnimationManager] All animations paused');
}
/**
* Resume all paused animations
*/
resumeAll() {
this.activeAnimations.forEach(animation => {
if (animation.playState === 'paused') {
animation.play();
}
});
Logger.info('[AnimationManager] All animations resumed');
}
}

View File

@@ -0,0 +1,678 @@
// modules/api-manager/BiometricAuthManager.js
import { Logger } from '../../core/logger.js';
/**
* Biometric Authentication Manager - WebAuthn API
*/
export class BiometricAuthManager {
constructor(config = {}) {
this.config = {
rpName: config.rpName || 'Custom PHP Framework',
rpId: config.rpId || window.location.hostname,
timeout: config.timeout || 60000,
userVerification: config.userVerification || 'preferred',
authenticatorSelection: {
authenticatorAttachment: 'platform', // Prefer built-in authenticators
userVerification: 'preferred',
requireResidentKey: false,
...config.authenticatorSelection
},
...config
};
this.credentials = new Map();
this.authSessions = new Map();
// Check WebAuthn support
this.support = {
webAuthn: 'credentials' in navigator && 'create' in navigator.credentials,
conditionalUI: 'conditional' in window.PublicKeyCredential || false,
userVerifyingPlatformAuthenticator: false,
residentKey: false
};
// Enhanced feature detection
this.detectFeatures();
Logger.info('[BiometricAuthManager] Initialized with support:', this.support);
}
async detectFeatures() {
if (!this.support.webAuthn) return;
try {
// Check for user-verifying platform authenticator
const available = await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
this.support.userVerifyingPlatformAuthenticator = available;
// Check for resident key support
if (window.PublicKeyCredential.isConditionalMediationAvailable) {
this.support.conditionalUI = await window.PublicKeyCredential.isConditionalMediationAvailable();
}
Logger.info('[BiometricAuthManager] Enhanced features detected:', {
platform: this.support.userVerifyingPlatformAuthenticator,
conditional: this.support.conditionalUI
});
} catch (error) {
Logger.warn('[BiometricAuthManager] Feature detection failed:', error);
}
}
/**
* Register new biometric credential
*/
async register(userInfo, options = {}) {
if (!this.support.webAuthn) {
throw new Error('WebAuthn not supported');
}
const {
challenge = null,
excludeCredentials = [],
extensions = {}
} = options;
try {
// Generate challenge if not provided
const challengeBuffer = challenge ?
this.base64ToArrayBuffer(challenge) :
crypto.getRandomValues(new Uint8Array(32));
// Create credential creation options
const createOptions = {
rp: {
name: this.config.rpName,
id: this.config.rpId
},
user: {
id: this.stringToArrayBuffer(userInfo.id || userInfo.username),
name: userInfo.username || userInfo.email,
displayName: userInfo.displayName || userInfo.name || userInfo.username
},
challenge: challengeBuffer,
pubKeyCredParams: [
{ type: 'public-key', alg: -7 }, // ES256
{ type: 'public-key', alg: -35 }, // ES384
{ type: 'public-key', alg: -36 }, // ES512
{ type: 'public-key', alg: -257 }, // RS256
{ type: 'public-key', alg: -258 }, // RS384
{ type: 'public-key', alg: -259 } // RS512
],
authenticatorSelection: this.config.authenticatorSelection,
timeout: this.config.timeout,
attestation: 'direct',
extensions: {
credProps: true,
...extensions
}
};
// Add exclude list if provided
if (excludeCredentials.length > 0) {
createOptions.excludeCredentials = excludeCredentials.map(cred => ({
type: 'public-key',
id: typeof cred === 'string' ? this.base64ToArrayBuffer(cred) : cred,
transports: ['internal', 'hybrid']
}));
}
Logger.info('[BiometricAuthManager] Starting registration...');
// Create credential
const credential = await navigator.credentials.create({
publicKey: createOptions
});
if (!credential) {
throw new Error('Credential creation failed');
}
// Process the credential
const processedCredential = this.processCredential(credential, 'registration');
// Store credential info locally
const credentialInfo = {
id: processedCredential.id,
rawId: processedCredential.rawId,
userId: userInfo.id || userInfo.username,
userDisplayName: userInfo.displayName || userInfo.name,
createdAt: Date.now(),
lastUsed: null,
counter: processedCredential.response.counter || 0,
transports: credential.response.getTransports?.() || ['internal']
};
this.credentials.set(processedCredential.id, credentialInfo);
Logger.info('[BiometricAuthManager] Registration successful:', {
id: processedCredential.id,
user: userInfo.username,
authenticator: credentialInfo.transports
});
return {
success: true,
credential: processedCredential,
info: credentialInfo,
attestation: this.parseAttestation(credential.response)
};
} catch (error) {
Logger.error('[BiometricAuthManager] Registration failed:', error);
return {
success: false,
error: error.message,
name: error.name
};
}
}
/**
* Authenticate with biometric credential
*/
async authenticate(options = {}) {
if (!this.support.webAuthn) {
throw new Error('WebAuthn not supported');
}
const {
challenge = null,
allowCredentials = [],
userVerification = this.config.userVerification,
conditional = false,
extensions = {}
} = options;
try {
// Generate challenge if not provided
const challengeBuffer = challenge ?
this.base64ToArrayBuffer(challenge) :
crypto.getRandomValues(new Uint8Array(32));
// Create authentication options
const getOptions = {
challenge: challengeBuffer,
timeout: this.config.timeout,
userVerification,
extensions: {
credProps: true,
...extensions
}
};
// Add allow list if provided
if (allowCredentials.length > 0) {
getOptions.allowCredentials = allowCredentials.map(cred => ({
type: 'public-key',
id: typeof cred === 'string' ? this.base64ToArrayBuffer(cred) : cred,
transports: ['internal', 'hybrid', 'usb', 'nfc', 'ble']
}));
}
Logger.info('[BiometricAuthManager] Starting authentication...', { conditional });
// Authenticate
const credential = conditional && this.support.conditionalUI ?
await navigator.credentials.get({
publicKey: getOptions,
mediation: 'conditional'
}) :
await navigator.credentials.get({
publicKey: getOptions
});
if (!credential) {
throw new Error('Authentication failed');
}
// Process the credential
const processedCredential = this.processCredential(credential, 'authentication');
// Update credential usage
const credentialInfo = this.credentials.get(processedCredential.id);
if (credentialInfo) {
credentialInfo.lastUsed = Date.now();
credentialInfo.counter = processedCredential.response.counter || 0;
}
// Create authentication session
const sessionId = this.generateSessionId();
const authSession = {
id: sessionId,
credentialId: processedCredential.id,
userId: credentialInfo?.userId,
authenticatedAt: Date.now(),
userAgent: navigator.userAgent,
ipAddress: await this.getClientIP().catch(() => 'unknown')
};
this.authSessions.set(sessionId, authSession);
Logger.info('[BiometricAuthManager] Authentication successful:', {
sessionId,
credentialId: processedCredential.id,
userId: credentialInfo?.userId
});
return {
success: true,
credential: processedCredential,
session: authSession,
info: credentialInfo
};
} catch (error) {
Logger.error('[BiometricAuthManager] Authentication failed:', error);
return {
success: false,
error: error.message,
name: error.name
};
}
}
/**
* Check if biometric authentication is available
*/
async isAvailable() {
const availability = {
webAuthn: this.support.webAuthn,
platform: this.support.userVerifyingPlatformAuthenticator,
conditional: this.support.conditionalUI,
hasCredentials: this.credentials.size > 0,
recommended: false
};
// Overall recommendation
availability.recommended = availability.webAuthn &&
(availability.platform || availability.hasCredentials);
return availability;
}
/**
* Set up conditional UI for seamless authentication
*/
async setupConditionalUI(inputSelector = 'input[type="email"], input[type="text"]', options = {}) {
if (!this.support.conditionalUI) {
Logger.warn('[BiometricAuthManager] Conditional UI not supported');
return null;
}
const inputs = document.querySelectorAll(inputSelector);
if (inputs.length === 0) {
Logger.warn('[BiometricAuthManager] No suitable inputs found for conditional UI');
return null;
}
try {
// Set up conditional UI
inputs.forEach(input => {
input.addEventListener('focus', async () => {
Logger.info('[BiometricAuthManager] Setting up conditional authentication');
try {
const result = await this.authenticate({
...options,
conditional: true
});
if (result.success) {
// Trigger custom event for successful authentication
const event = new CustomEvent('biometric-auth-success', {
detail: result
});
input.dispatchEvent(event);
// Auto-fill username if available
if (result.info?.userDisplayName) {
input.value = result.info.userDisplayName;
}
}
} catch (error) {
Logger.warn('[BiometricAuthManager] Conditional auth failed:', error);
}
});
});
Logger.info('[BiometricAuthManager] Conditional UI setup complete');
return { inputs: inputs.length, selector: inputSelector };
} catch (error) {
Logger.error('[BiometricAuthManager] Conditional UI setup failed:', error);
return null;
}
}
/**
* Create a complete biometric login flow
*/
createLoginFlow(options = {}) {
const {
registerSelector = '#register-biometric',
loginSelector = '#login-biometric',
statusSelector = '#biometric-status',
onRegister = null,
onLogin = null,
onError = null
} = options;
return {
async init() {
const availability = await this.isAvailable();
// Update status display
const statusEl = document.querySelector(statusSelector);
if (statusEl) {
statusEl.innerHTML = this.createStatusHTML(availability);
}
// Set up register button
const registerBtn = document.querySelector(registerSelector);
if (registerBtn) {
registerBtn.style.display = availability.webAuthn ? 'block' : 'none';
registerBtn.onclick = () => this.handleRegister();
}
// Set up login button
const loginBtn = document.querySelector(loginSelector);
if (loginBtn) {
loginBtn.style.display = availability.hasCredentials ? 'block' : 'none';
loginBtn.onclick = () => this.handleLogin();
}
// Set up conditional UI
if (availability.conditional) {
await this.setupConditionalUI();
}
return availability;
},
async handleRegister() {
try {
// Get user information (could be from form or prompt)
const userInfo = await this.getUserInfo();
if (!userInfo) return;
const result = await this.register(userInfo);
if (result.success) {
if (onRegister) onRegister(result);
this.showSuccess('Biometric authentication registered successfully!');
} else {
if (onError) onError(result);
this.showError(result.error);
}
} catch (error) {
if (onError) onError({ error: error.message });
this.showError(error.message);
}
},
async handleLogin() {
try {
const result = await this.authenticate();
if (result.success) {
if (onLogin) onLogin(result);
this.showSuccess('Biometric authentication successful!');
} else {
if (onError) onError(result);
this.showError(result.error);
}
} catch (error) {
if (onError) onError({ error: error.message });
this.showError(error.message);
}
},
async getUserInfo() {
// Try to get from form first
const usernameInput = document.querySelector('input[name="username"], input[name="email"]');
const nameInput = document.querySelector('input[name="name"], input[name="display_name"]');
if (usernameInput?.value) {
return {
id: usernameInput.value,
username: usernameInput.value,
displayName: nameInput?.value || usernameInput.value
};
}
// Fallback to prompt
const username = prompt('Please enter your username or email:');
if (!username) return null;
const displayName = prompt('Please enter your display name:') || username;
return {
id: username,
username,
displayName
};
},
createStatusHTML(availability) {
if (!availability.webAuthn) {
return '<div class="alert alert-warning">⚠️ Biometric authentication not supported in this browser</div>';
}
if (!availability.platform && !availability.hasCredentials) {
return '<div class="alert alert-info"> No biometric authenticators available</div>';
}
if (availability.hasCredentials) {
return '<div class="alert alert-success">✅ Biometric authentication available</div>';
}
return '<div class="alert alert-info">🔐 Biometric authentication can be set up</div>';
},
showSuccess(message) {
this.showMessage(message, 'success');
},
showError(message) {
this.showMessage(message, 'error');
},
showMessage(message, type = 'info') {
// Create toast notification
const toast = document.createElement('div');
toast.className = `biometric-toast toast-${type}`;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'error' ? '#f44336' : type === 'success' ? '#4caf50' : '#2196f3'};
color: white;
padding: 1rem;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 10001;
max-width: 300px;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 5000);
}
};
}
// Helper methods
processCredential(credential, type) {
const processed = {
id: this.arrayBufferToBase64(credential.rawId),
rawId: this.arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {}
};
if (type === 'registration') {
processed.response = {
attestationObject: this.arrayBufferToBase64(credential.response.attestationObject),
clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
transports: credential.response.getTransports?.() || []
};
} else {
processed.response = {
authenticatorData: this.arrayBufferToBase64(credential.response.authenticatorData),
clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
signature: this.arrayBufferToBase64(credential.response.signature),
userHandle: credential.response.userHandle ?
this.arrayBufferToBase64(credential.response.userHandle) : null
};
}
return processed;
}
parseAttestation(response) {
try {
// Basic attestation parsing
const clientDataJSON = JSON.parse(this.arrayBufferToString(response.clientDataJSON));
return {
format: 'packed', // Simplified
clientData: clientDataJSON,
origin: clientDataJSON.origin,
challenge: clientDataJSON.challenge
};
} catch (error) {
Logger.warn('[BiometricAuthManager] Attestation parsing failed:', error);
return null;
}
}
async getClientIP() {
try {
// Use a public IP service (consider privacy implications)
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
return data.ip;
} catch (error) {
return 'unknown';
}
}
generateSessionId() {
return `biometric_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Encoding/decoding utilities
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
stringToArrayBuffer(str) {
const encoder = new TextEncoder();
return encoder.encode(str);
}
arrayBufferToString(buffer) {
const decoder = new TextDecoder();
return decoder.decode(buffer);
}
/**
* Get all registered credentials
*/
getCredentials() {
return Array.from(this.credentials.values());
}
/**
* Get active authentication sessions
*/
getActiveSessions() {
return Array.from(this.authSessions.values());
}
/**
* Revoke a credential
*/
revokeCredential(credentialId) {
const removed = this.credentials.delete(credentialId);
if (removed) {
Logger.info(`[BiometricAuthManager] Credential revoked: ${credentialId}`);
}
return removed;
}
/**
* End authentication session
*/
endSession(sessionId) {
const removed = this.authSessions.delete(sessionId);
if (removed) {
Logger.info(`[BiometricAuthManager] Session ended: ${sessionId}`);
}
return removed;
}
/**
* Cleanup expired sessions
*/
cleanupSessions(maxAge = 24 * 60 * 60 * 1000) { // 24 hours
const now = Date.now();
let cleaned = 0;
for (const [sessionId, session] of this.authSessions.entries()) {
if (now - session.authenticatedAt > maxAge) {
this.authSessions.delete(sessionId);
cleaned++;
}
}
if (cleaned > 0) {
Logger.info(`[BiometricAuthManager] Cleaned up ${cleaned} expired sessions`);
}
return cleaned;
}
/**
* Get comprehensive status report
*/
async getStatusReport() {
const availability = await this.isAvailable();
return {
availability,
credentials: this.credentials.size,
sessions: this.authSessions.size,
support: this.support,
config: {
rpName: this.config.rpName,
rpId: this.config.rpId,
timeout: this.config.timeout
},
timestamp: Date.now()
};
}
}

View File

@@ -0,0 +1,704 @@
// modules/api-manager/DeviceManager.js
import { Logger } from '../../core/logger.js';
/**
* Device APIs Manager - Geolocation, Sensors, Battery, Network, Vibration
*/
export class DeviceManager {
constructor(config = {}) {
this.config = config;
this.activeWatchers = new Map();
this.sensorData = new Map();
// Check API support
this.support = {
geolocation: 'geolocation' in navigator,
deviceMotion: 'DeviceMotionEvent' in window,
deviceOrientation: 'DeviceOrientationEvent' in window,
vibration: 'vibrate' in navigator,
battery: 'getBattery' in navigator,
networkInfo: 'connection' in navigator || 'mozConnection' in navigator || 'webkitConnection' in navigator,
wakeLock: 'wakeLock' in navigator,
bluetooth: 'bluetooth' in navigator,
usb: 'usb' in navigator,
serial: 'serial' in navigator
};
Logger.info('[DeviceManager] Initialized with support:', this.support);
// Initialize sensors if available
this.initializeSensors();
}
/**
* Geolocation API
*/
geolocation = {
// Get current position
getCurrent: (options = {}) => {
return new Promise((resolve, reject) => {
if (!this.support.geolocation) {
reject(new Error('Geolocation not supported'));
return;
}
const defaultOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000,
...options
};
navigator.geolocation.getCurrentPosition(
(position) => {
const location = this.enhanceLocationData(position);
Logger.info('[DeviceManager] Location acquired:', {
lat: location.latitude,
lng: location.longitude,
accuracy: location.accuracy
});
resolve(location);
},
(error) => {
Logger.error('[DeviceManager] Geolocation failed:', error.message);
reject(error);
},
defaultOptions
);
});
},
// Watch position changes
watch: (callback, options = {}) => {
if (!this.support.geolocation) {
throw new Error('Geolocation not supported');
}
const defaultOptions = {
enableHighAccuracy: true,
timeout: 30000,
maximumAge: 10000,
...options
};
const watchId = navigator.geolocation.watchPosition(
(position) => {
const location = this.enhanceLocationData(position);
callback(location);
},
(error) => {
Logger.error('[DeviceManager] Location watch failed:', error.message);
callback({ error });
},
defaultOptions
);
this.activeWatchers.set(`geo_${watchId}`, {
type: 'geolocation',
id: watchId,
stop: () => {
navigator.geolocation.clearWatch(watchId);
this.activeWatchers.delete(`geo_${watchId}`);
}
});
Logger.info('[DeviceManager] Location watch started:', watchId);
return {
id: watchId,
stop: () => {
navigator.geolocation.clearWatch(watchId);
this.activeWatchers.delete(`geo_${watchId}`);
Logger.info('[DeviceManager] Location watch stopped:', watchId);
}
};
},
// Calculate distance between two points
distance: (pos1, pos2) => {
const R = 6371; // Earth's radius in km
const dLat = this.toRadians(pos2.latitude - pos1.latitude);
const dLon = this.toRadians(pos2.longitude - pos1.longitude);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(pos1.latitude)) * Math.cos(this.toRadians(pos2.latitude)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return {
kilometers: distance,
miles: distance * 0.621371,
meters: distance * 1000
};
}
};
/**
* Device Motion and Orientation
*/
motion = {
// Start motion detection
start: (callback, options = {}) => {
if (!this.support.deviceMotion) {
throw new Error('Device Motion not supported');
}
const handler = (event) => {
const motionData = {
acceleration: event.acceleration,
accelerationIncludingGravity: event.accelerationIncludingGravity,
rotationRate: event.rotationRate,
interval: event.interval,
timestamp: event.timeStamp,
// Enhanced data
totalAcceleration: this.calculateTotalAcceleration(event.acceleration),
shake: this.detectShake(event.accelerationIncludingGravity),
orientation: this.getDeviceOrientation(event)
};
callback(motionData);
};
// Request permission for iOS 13+
if (typeof DeviceMotionEvent.requestPermission === 'function') {
DeviceMotionEvent.requestPermission().then(response => {
if (response === 'granted') {
window.addEventListener('devicemotion', handler);
}
});
} else {
window.addEventListener('devicemotion', handler);
}
const watcherId = this.generateId('motion');
this.activeWatchers.set(watcherId, {
type: 'motion',
handler,
stop: () => {
window.removeEventListener('devicemotion', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Motion detection started');
return {
id: watcherId,
stop: () => {
window.removeEventListener('devicemotion', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Motion detection stopped');
}
};
},
// Start orientation detection
startOrientation: (callback, options = {}) => {
if (!this.support.deviceOrientation) {
throw new Error('Device Orientation not supported');
}
const handler = (event) => {
const orientationData = {
alpha: event.alpha, // Z axis (0-360)
beta: event.beta, // X axis (-180 to 180)
gamma: event.gamma, // Y axis (-90 to 90)
absolute: event.absolute,
timestamp: event.timeStamp,
// Enhanced data
compass: this.calculateCompass(event.alpha),
tilt: this.calculateTilt(event.beta, event.gamma),
rotation: this.getRotationState(event)
};
callback(orientationData);
};
// Request permission for iOS 13+
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(response => {
if (response === 'granted') {
window.addEventListener('deviceorientation', handler);
}
});
} else {
window.addEventListener('deviceorientation', handler);
}
const watcherId = this.generateId('orientation');
this.activeWatchers.set(watcherId, {
type: 'orientation',
handler,
stop: () => {
window.removeEventListener('deviceorientation', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Orientation detection started');
return {
id: watcherId,
stop: () => {
window.removeEventListener('deviceorientation', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Orientation detection stopped');
}
};
}
};
/**
* Vibration API
*/
vibration = {
// Simple vibration
vibrate: (pattern) => {
if (!this.support.vibration) {
Logger.warn('[DeviceManager] Vibration not supported');
return false;
}
try {
navigator.vibrate(pattern);
Logger.info('[DeviceManager] Vibration triggered:', pattern);
return true;
} catch (error) {
Logger.error('[DeviceManager] Vibration failed:', error);
return false;
}
},
// Predefined patterns
patterns: {
short: 200,
long: 600,
double: [200, 100, 200],
triple: [200, 100, 200, 100, 200],
sos: [100, 30, 100, 30, 100, 200, 200, 30, 200, 30, 200, 200, 100, 30, 100, 30, 100],
heartbeat: [100, 30, 100, 130, 40, 30, 40, 30, 100],
notification: [200, 100, 200],
success: [100],
error: [300, 100, 300],
warning: [200, 100, 200, 100, 200]
},
// Stop vibration
stop: () => {
if (this.support.vibration) {
navigator.vibrate(0);
Logger.info('[DeviceManager] Vibration stopped');
}
},
// Haptic feedback helpers
success: () => this.vibration.vibrate(this.vibration.patterns.success),
error: () => this.vibration.vibrate(this.vibration.patterns.error),
warning: () => this.vibration.vibrate(this.vibration.patterns.warning),
notification: () => this.vibration.vibrate(this.vibration.patterns.notification)
};
/**
* Battery API
*/
battery = {
// Get battery status
get: async () => {
if (!this.support.battery) {
Logger.warn('[DeviceManager] Battery API not supported');
return null;
}
try {
const battery = await navigator.getBattery();
const batteryInfo = {
level: Math.round(battery.level * 100),
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
// Enhanced data
status: this.getBatteryStatus(battery),
timeRemaining: this.formatBatteryTime(battery)
};
Logger.info('[DeviceManager] Battery status:', batteryInfo);
return batteryInfo;
} catch (error) {
Logger.error('[DeviceManager] Battery status failed:', error);
return null;
}
},
// Watch battery changes
watch: async (callback) => {
if (!this.support.battery) {
throw new Error('Battery API not supported');
}
try {
const battery = await navigator.getBattery();
const events = ['chargingchange', 'levelchange', 'chargingtimechange', 'dischargingtimechange'];
const handlers = [];
events.forEach(eventType => {
const handler = () => {
const batteryInfo = {
level: Math.round(battery.level * 100),
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
status: this.getBatteryStatus(battery),
timeRemaining: this.formatBatteryTime(battery),
event: eventType
};
callback(batteryInfo);
};
battery.addEventListener(eventType, handler);
handlers.push({ event: eventType, handler });
});
const watcherId = this.generateId('battery');
this.activeWatchers.set(watcherId, {
type: 'battery',
battery,
handlers,
stop: () => {
handlers.forEach(({ event, handler }) => {
battery.removeEventListener(event, handler);
});
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Battery watch started');
return {
id: watcherId,
stop: () => {
handlers.forEach(({ event, handler }) => {
battery.removeEventListener(event, handler);
});
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Battery watch stopped');
}
};
} catch (error) {
Logger.error('[DeviceManager] Battery watch failed:', error);
throw error;
}
}
};
/**
* Network Information API
*/
network = {
// Get connection info
get: () => {
if (!this.support.networkInfo) {
Logger.warn('[DeviceManager] Network Information not supported');
return null;
}
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
return {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
// Enhanced data
speed: this.getConnectionSpeed(connection),
quality: this.getConnectionQuality(connection),
recommendation: this.getNetworkRecommendation(connection)
};
},
// Watch network changes
watch: (callback) => {
if (!this.support.networkInfo) {
throw new Error('Network Information not supported');
}
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
const handler = () => {
const networkInfo = {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
speed: this.getConnectionSpeed(connection),
quality: this.getConnectionQuality(connection),
recommendation: this.getNetworkRecommendation(connection)
};
callback(networkInfo);
};
connection.addEventListener('change', handler);
const watcherId = this.generateId('network');
this.activeWatchers.set(watcherId, {
type: 'network',
connection,
handler,
stop: () => {
connection.removeEventListener('change', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Network watch started');
return {
id: watcherId,
stop: () => {
connection.removeEventListener('change', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Network watch stopped');
}
};
}
};
/**
* Wake Lock API
*/
wakeLock = {
// Request wake lock
request: async (type = 'screen') => {
if (!this.support.wakeLock) {
Logger.warn('[DeviceManager] Wake Lock not supported');
return null;
}
try {
const wakeLock = await navigator.wakeLock.request(type);
Logger.info(`[DeviceManager] Wake lock acquired: ${type}`);
return {
type: wakeLock.type,
release: () => {
wakeLock.release();
Logger.info(`[DeviceManager] Wake lock released: ${type}`);
}
};
} catch (error) {
Logger.error('[DeviceManager] Wake lock failed:', error);
throw error;
}
}
};
// Helper methods
initializeSensors() {
// Initialize any sensors that need setup
if (this.support.deviceMotion || this.support.deviceOrientation) {
// Store baseline sensor data for comparison
this.sensorData.set('motionBaseline', { x: 0, y: 0, z: 0 });
this.sensorData.set('shakeThreshold', this.config.shakeThreshold || 15);
}
}
enhanceLocationData(position) {
return {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp,
// Enhanced data
coordinates: `${position.coords.latitude},${position.coords.longitude}`,
accuracyLevel: this.getAccuracyLevel(position.coords.accuracy),
mapUrl: `https://maps.google.com/?q=${position.coords.latitude},${position.coords.longitude}`
};
}
getAccuracyLevel(accuracy) {
if (accuracy <= 5) return 'excellent';
if (accuracy <= 10) return 'good';
if (accuracy <= 50) return 'fair';
return 'poor';
}
calculateTotalAcceleration(acceleration) {
if (!acceleration) return 0;
const x = acceleration.x || 0;
const y = acceleration.y || 0;
const z = acceleration.z || 0;
return Math.sqrt(x * x + y * y + z * z);
}
detectShake(acceleration) {
if (!acceleration) return false;
const threshold = this.sensorData.get('shakeThreshold');
const x = Math.abs(acceleration.x || 0);
const y = Math.abs(acceleration.y || 0);
const z = Math.abs(acceleration.z || 0);
return (x > threshold || y > threshold || z > threshold);
}
getDeviceOrientation(event) {
const acceleration = event.accelerationIncludingGravity;
if (!acceleration) return 'unknown';
const x = acceleration.x || 0;
const y = acceleration.y || 0;
const z = acceleration.z || 0;
if (Math.abs(x) > Math.abs(y) && Math.abs(x) > Math.abs(z)) {
return x > 0 ? 'landscape-right' : 'landscape-left';
} else if (Math.abs(y) > Math.abs(z)) {
return y > 0 ? 'portrait-upside-down' : 'portrait';
} else {
return z > 0 ? 'face-down' : 'face-up';
}
}
calculateCompass(alpha) {
if (alpha === null) return null;
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const index = Math.round(alpha / 45) % 8;
return {
degrees: Math.round(alpha),
direction: directions[index],
cardinal: this.getCardinalDirection(alpha)
};
}
getCardinalDirection(alpha) {
if (alpha >= 337.5 || alpha < 22.5) return 'North';
if (alpha >= 22.5 && alpha < 67.5) return 'Northeast';
if (alpha >= 67.5 && alpha < 112.5) return 'East';
if (alpha >= 112.5 && alpha < 157.5) return 'Southeast';
if (alpha >= 157.5 && alpha < 202.5) return 'South';
if (alpha >= 202.5 && alpha < 247.5) return 'Southwest';
if (alpha >= 247.5 && alpha < 292.5) return 'West';
if (alpha >= 292.5 && alpha < 337.5) return 'Northwest';
return 'Unknown';
}
calculateTilt(beta, gamma) {
return {
x: Math.round(beta || 0),
y: Math.round(gamma || 0),
magnitude: Math.round(Math.sqrt((beta || 0) ** 2 + (gamma || 0) ** 2))
};
}
getRotationState(event) {
const { alpha, beta, gamma } = event;
// Determine if device is being rotated significantly
const rotationThreshold = 10;
const isRotating = Math.abs(beta) > rotationThreshold || Math.abs(gamma) > rotationThreshold;
return {
isRotating,
intensity: isRotating ? Math.max(Math.abs(beta), Math.abs(gamma)) : 0
};
}
getBatteryStatus(battery) {
const level = battery.level * 100;
if (battery.charging) return 'charging';
if (level <= 10) return 'critical';
if (level <= 20) return 'low';
if (level <= 50) return 'medium';
return 'high';
}
formatBatteryTime(battery) {
const time = battery.charging ? battery.chargingTime : battery.dischargingTime;
if (time === Infinity || isNaN(time)) return 'Unknown';
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return `${hours}h ${minutes}m`;
}
getConnectionSpeed(connection) {
const downlink = connection.downlink;
if (downlink >= 10) return 'fast';
if (downlink >= 1.5) return 'good';
if (downlink >= 0.5) return 'slow';
return 'very-slow';
}
getConnectionQuality(connection) {
const effectiveType = connection.effectiveType;
switch (effectiveType) {
case '4g': return 'excellent';
case '3g': return 'good';
case '2g': return 'poor';
case 'slow-2g': return 'very-poor';
default: return 'unknown';
}
}
getNetworkRecommendation(connection) {
const quality = this.getConnectionQuality(connection);
switch (quality) {
case 'excellent':
return 'Full quality content recommended';
case 'good':
return 'Moderate quality content recommended';
case 'poor':
return 'Light content only, avoid large files';
case 'very-poor':
return 'Text-only content recommended';
default:
return 'Monitor connection quality';
}
}
toRadians(degrees) {
return degrees * (Math.PI / 180);
}
generateId(prefix = 'device') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Stop all active watchers
*/
stopAllWatchers() {
this.activeWatchers.forEach(watcher => {
watcher.stop();
});
this.activeWatchers.clear();
Logger.info('[DeviceManager] All watchers stopped');
}
/**
* Get device capabilities summary
*/
getCapabilities() {
return {
support: this.support,
activeWatchers: this.activeWatchers.size,
watcherTypes: Array.from(this.activeWatchers.values()).map(w => w.type)
};
}
}

View File

@@ -0,0 +1,554 @@
// modules/api-manager/MediaManager.js
import { Logger } from '../../core/logger.js';
/**
* Media APIs Manager - Camera, Microphone, WebRTC, Audio, Recording
*/
export class MediaManager {
constructor(config = {}) {
this.config = config;
this.activeStreams = new Map();
this.activeConnections = new Map();
this.audioContext = null;
// Check API support
this.support = {
mediaDevices: navigator.mediaDevices !== undefined,
webRTC: 'RTCPeerConnection' in window,
webAudio: 'AudioContext' in window || 'webkitAudioContext' in window,
mediaRecorder: 'MediaRecorder' in window,
screenShare: navigator.mediaDevices?.getDisplayMedia !== undefined
};
Logger.info('[MediaManager] Initialized with support:', this.support);
}
/**
* Get user camera stream
*/
async getUserCamera(constraints = {}) {
if (!this.support.mediaDevices) {
throw new Error('MediaDevices API not supported');
}
const defaultConstraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: false,
...constraints
};
try {
const stream = await navigator.mediaDevices.getUserMedia(defaultConstraints);
const streamId = this.generateId('camera');
this.activeStreams.set(streamId, {
stream,
type: 'camera',
constraints: defaultConstraints,
tracks: stream.getTracks()
});
Logger.info(`[MediaManager] Camera stream acquired: ${streamId}`);
return {
id: streamId,
stream,
video: stream.getVideoTracks()[0],
audio: stream.getAudioTracks()[0],
stop: () => this.stopStream(streamId),
switchCamera: () => this.switchCamera(streamId),
takePhoto: (canvas) => this.takePhoto(stream, canvas),
applyFilter: (filter) => this.applyVideoFilter(streamId, filter)
};
} catch (error) {
Logger.warn('[MediaManager] Camera access failed:', error.message);
throw error;
}
}
/**
* Get user microphone stream
*/
async getUserMicrophone(constraints = {}) {
if (!this.support.mediaDevices) {
throw new Error('MediaDevices API not supported');
}
const defaultConstraints = {
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
...constraints.audio
},
video: false,
...constraints
};
try {
const stream = await navigator.mediaDevices.getUserMedia(defaultConstraints);
const streamId = this.generateId('microphone');
this.activeStreams.set(streamId, {
stream,
type: 'microphone',
constraints: defaultConstraints,
tracks: stream.getTracks()
});
Logger.info(`[MediaManager] Microphone stream acquired: ${streamId}`);
return {
id: streamId,
stream,
audio: stream.getAudioTracks()[0],
stop: () => this.stopStream(streamId),
getVolume: () => this.getAudioLevel(stream),
startRecording: (options) => this.startRecording(stream, options)
};
} catch (error) {
Logger.warn('[MediaManager] Microphone access failed:', error.message);
throw error;
}
}
/**
* Get screen share stream
*/
async getScreenShare(constraints = {}) {
if (!this.support.screenShare) {
throw new Error('Screen sharing not supported');
}
const defaultConstraints = {
video: {
cursor: 'always'
},
audio: false,
...constraints
};
try {
const stream = await navigator.mediaDevices.getDisplayMedia(defaultConstraints);
const streamId = this.generateId('screen');
this.activeStreams.set(streamId, {
stream,
type: 'screen',
constraints: defaultConstraints,
tracks: stream.getTracks()
});
// Auto-cleanup when user stops sharing
stream.getTracks().forEach(track => {
track.addEventListener('ended', () => {
this.stopStream(streamId);
});
});
Logger.info(`[MediaManager] Screen share acquired: ${streamId}`);
return {
id: streamId,
stream,
video: stream.getVideoTracks()[0],
audio: stream.getAudioTracks()[0],
stop: () => this.stopStream(streamId)
};
} catch (error) {
Logger.warn('[MediaManager] Screen share failed:', error.message);
throw error;
}
}
/**
* Start recording media stream
*/
async startRecording(stream, options = {}) {
if (!this.support.mediaRecorder) {
throw new Error('MediaRecorder API not supported');
}
const defaultOptions = {
mimeType: 'video/webm;codecs=vp9',
videoBitsPerSecond: 2000000,
audioBitsPerSecond: 128000,
...options
};
// Find supported MIME type
const mimeType = this.getSupportedMimeType([
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4'
]) || defaultOptions.mimeType;
const recorder = new MediaRecorder(stream, {
...defaultOptions,
mimeType
});
const recordingId = this.generateId('recording');
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: mimeType });
this.onRecordingComplete(recordingId, blob);
};
recorder.start();
Logger.info(`[MediaManager] Recording started: ${recordingId}`);
return {
id: recordingId,
recorder,
stop: () => {
recorder.stop();
return new Promise(resolve => {
recorder.onstop = () => {
const blob = new Blob(chunks, { type: mimeType });
resolve({
blob,
url: URL.createObjectURL(blob),
size: blob.size,
type: blob.type,
download: (filename = `recording-${Date.now()}.webm`) => {
this.downloadBlob(blob, filename);
}
});
};
});
},
pause: () => recorder.pause(),
resume: () => recorder.resume(),
get state() { return recorder.state; }
};
}
/**
* Take photo from video stream
*/
takePhoto(stream, canvas) {
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.muted = true;
return new Promise((resolve) => {
video.onloadedmetadata = () => {
if (!canvas) {
canvas = document.createElement('canvas');
}
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
canvas.toBlob((blob) => {
resolve({
canvas,
blob,
url: URL.createObjectURL(blob),
dataURL: canvas.toDataURL('image/jpeg', 0.9),
download: (filename = `photo-${Date.now()}.jpg`) => {
this.downloadBlob(blob, filename);
}
});
}, 'image/jpeg', 0.9);
video.remove();
};
});
}
/**
* Web Audio Context setup
*/
getAudioContext() {
if (!this.audioContext) {
if (this.support.webAudio) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
Logger.info('[MediaManager] Audio context created');
} else {
Logger.warn('[MediaManager] Web Audio API not supported');
return null;
}
}
return this.audioContext;
}
/**
* Create audio analyzer for visualizations
*/
createAudioAnalyzer(stream, options = {}) {
const audioContext = this.getAudioContext();
if (!audioContext) return null;
const source = audioContext.createMediaStreamSource(stream);
const analyzer = audioContext.createAnalyser();
analyzer.fftSize = options.fftSize || 256;
analyzer.smoothingTimeConstant = options.smoothing || 0.8;
source.connect(analyzer);
const bufferLength = analyzer.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
return {
analyzer,
bufferLength,
dataArray,
getFrequencyData: () => {
analyzer.getByteFrequencyData(dataArray);
return Array.from(dataArray);
},
getTimeDomainData: () => {
analyzer.getByteTimeDomainData(dataArray);
return Array.from(dataArray);
},
getAverageVolume: () => {
analyzer.getByteFrequencyData(dataArray);
return dataArray.reduce((sum, value) => sum + value, 0) / bufferLength;
}
};
}
/**
* Simple WebRTC peer connection setup
*/
async createPeerConnection(config = {}) {
if (!this.support.webRTC) {
throw new Error('WebRTC not supported');
}
const defaultConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
],
...config
};
const pc = new RTCPeerConnection(defaultConfig);
const connectionId = this.generateId('rtc');
this.activeConnections.set(connectionId, pc);
// Enhanced peer connection with event handling
const enhancedPC = {
id: connectionId,
connection: pc,
// Event handlers
onTrack: (callback) => pc.addEventListener('track', callback),
onIceCandidate: (callback) => pc.addEventListener('icecandidate', callback),
onConnectionStateChange: (callback) => pc.addEventListener('connectionstatechange', callback),
// Methods
addStream: (stream) => {
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
},
createOffer: () => pc.createOffer(),
createAnswer: () => pc.createAnswer(),
setLocalDescription: (desc) => pc.setLocalDescription(desc),
setRemoteDescription: (desc) => pc.setRemoteDescription(desc),
addIceCandidate: (candidate) => pc.addIceCandidate(candidate),
close: () => {
pc.close();
this.activeConnections.delete(connectionId);
},
get connectionState() { return pc.connectionState; },
get iceConnectionState() { return pc.iceConnectionState; }
};
Logger.info(`[MediaManager] Peer connection created: ${connectionId}`);
return enhancedPC;
}
/**
* Get available media devices
*/
async getDevices() {
if (!this.support.mediaDevices) {
return { cameras: [], microphones: [], speakers: [] };
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return {
cameras: devices.filter(d => d.kind === 'videoinput'),
microphones: devices.filter(d => d.kind === 'audioinput'),
speakers: devices.filter(d => d.kind === 'audiooutput'),
all: devices
};
} catch (error) {
Logger.warn('[MediaManager] Device enumeration failed:', error);
return { cameras: [], microphones: [], speakers: [] };
}
}
/**
* Check device permissions
*/
async checkPermissions() {
const permissions = {};
try {
if (navigator.permissions) {
const camera = await navigator.permissions.query({ name: 'camera' });
const microphone = await navigator.permissions.query({ name: 'microphone' });
permissions.camera = camera.state;
permissions.microphone = microphone.state;
}
} catch (error) {
Logger.warn('[MediaManager] Permission check failed:', error);
}
return permissions;
}
// Helper methods
stopStream(streamId) {
const streamData = this.activeStreams.get(streamId);
if (streamData) {
streamData.tracks.forEach(track => track.stop());
this.activeStreams.delete(streamId);
Logger.info(`[MediaManager] Stream stopped: ${streamId}`);
}
}
stopAllStreams() {
this.activeStreams.forEach((streamData, id) => {
streamData.tracks.forEach(track => track.stop());
});
this.activeStreams.clear();
Logger.info('[MediaManager] All streams stopped');
}
async switchCamera(streamId) {
const streamData = this.activeStreams.get(streamId);
if (!streamData || streamData.type !== 'camera') return null;
const currentFacing = streamData.constraints.video.facingMode;
const newFacing = currentFacing === 'user' ? 'environment' : 'user';
// Stop current stream
this.stopStream(streamId);
// Get new stream with switched camera
return this.getUserCamera({
video: {
...streamData.constraints.video,
facingMode: newFacing
}
});
}
getAudioLevel(stream) {
const audioContext = this.getAudioContext();
if (!audioContext) return 0;
const source = audioContext.createMediaStreamSource(stream);
const analyzer = audioContext.createAnalyser();
source.connect(analyzer);
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
analyzer.getByteFrequencyData(dataArray);
return dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length;
}
getSupportedMimeType(types) {
return types.find(type => MediaRecorder.isTypeSupported(type));
}
downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
generateId(prefix = 'media') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
onRecordingComplete(id, blob) {
Logger.info(`[MediaManager] Recording completed: ${id}, size: ${blob.size} bytes`);
// Custom completion handling can be added here
}
/**
* Apply video filters using CSS filters
*/
applyVideoFilter(streamId, filterName) {
const streamData = this.activeStreams.get(streamId);
if (!streamData || streamData.type !== 'camera') return;
const filters = {
none: 'none',
blur: 'blur(2px)',
brightness: 'brightness(1.2)',
contrast: 'contrast(1.3)',
grayscale: 'grayscale(1)',
sepia: 'sepia(1)',
invert: 'invert(1)',
vintage: 'sepia(0.8) contrast(1.4) brightness(1.1)',
cool: 'hue-rotate(180deg) saturate(1.5)',
warm: 'hue-rotate(25deg) saturate(1.2)'
};
return {
filter: filters[filterName] || filterName,
apply: (videoElement) => {
videoElement.style.filter = filters[filterName] || filterName;
}
};
}
/**
* Get current status of all media operations
*/
getStatus() {
return {
activeStreams: this.activeStreams.size,
activeConnections: this.activeConnections.size,
audioContextState: this.audioContext?.state || 'none',
support: this.support,
streams: Array.from(this.activeStreams.entries()).map(([id, data]) => ({
id,
type: data.type,
tracks: data.tracks.length,
active: data.tracks.some(track => track.readyState === 'live')
}))
};
}
}

View File

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

View File

@@ -0,0 +1,756 @@
// modules/api-manager/PerformanceManager.js
import { Logger } from '../../core/logger.js';
/**
* Performance APIs Manager - Timing, Metrics, Observers, Optimization
*/
export class PerformanceManager {
constructor(config = {}) {
this.config = config;
this.marks = new Map();
this.measures = new Map();
this.observers = new Map();
this.metrics = new Map();
this.thresholds = {
fcp: 2000, // First Contentful Paint
lcp: 2500, // Largest Contentful Paint
fid: 100, // First Input Delay
cls: 0.1, // Cumulative Layout Shift
...config.thresholds
};
// Check API support
this.support = {
performance: 'performance' in window,
timing: 'timing' in (window.performance || {}),
navigation: 'navigation' in (window.performance || {}),
observer: 'PerformanceObserver' in window,
memory: 'memory' in (window.performance || {}),
userTiming: 'mark' in (window.performance || {}),
resourceTiming: 'getEntriesByType' in (window.performance || {}),
paintTiming: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('paint'),
layoutInstability: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('layout-shift'),
longTask: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('longtask')
};
Logger.info('[PerformanceManager] Initialized with support:', this.support);
// Auto-start core metrics collection
this.startCoreMetrics();
}
/**
* User Timing API - Marks and Measures
*/
timing = {
// Create performance mark
mark: (name, options = {}) => {
if (!this.support.userTiming) {
Logger.warn('[PerformanceManager] User Timing not supported');
return null;
}
try {
performance.mark(name, options);
this.marks.set(name, {
name,
timestamp: performance.now(),
options
});
Logger.info(`[PerformanceManager] Mark created: ${name}`);
return this.marks.get(name);
} catch (error) {
Logger.error('[PerformanceManager] Mark creation failed:', error);
return null;
}
},
// Create performance measure
measure: (name, startMark, endMark, options = {}) => {
if (!this.support.userTiming) {
Logger.warn('[PerformanceManager] User Timing not supported');
return null;
}
try {
performance.measure(name, startMark, endMark, options);
const entry = performance.getEntriesByName(name, 'measure')[0];
const measure = {
name,
startTime: entry.startTime,
duration: entry.duration,
startMark,
endMark,
options
};
this.measures.set(name, measure);
Logger.info(`[PerformanceManager] Measure created: ${name} (${measure.duration.toFixed(2)}ms)`);
return measure;
} catch (error) {
Logger.error('[PerformanceManager] Measure creation failed:', error);
return null;
}
},
// Clear marks and measures
clear: (name = null) => {
if (!this.support.userTiming) return;
try {
if (name) {
performance.clearMarks(name);
performance.clearMeasures(name);
this.marks.delete(name);
this.measures.delete(name);
} else {
performance.clearMarks();
performance.clearMeasures();
this.marks.clear();
this.measures.clear();
}
Logger.info(`[PerformanceManager] Cleared: ${name || 'all'}`);
} catch (error) {
Logger.error('[PerformanceManager] Clear failed:', error);
}
},
// Get all marks
getMarks: () => {
if (!this.support.userTiming) return [];
return Array.from(this.marks.values());
},
// Get all measures
getMeasures: () => {
if (!this.support.userTiming) return [];
return Array.from(this.measures.values());
}
};
/**
* Navigation Timing API
*/
navigation = {
// Get navigation timing data
get: () => {
if (!this.support.navigation) {
return this.getLegacyNavigationTiming();
}
try {
const entry = performance.getEntriesByType('navigation')[0];
if (!entry) return null;
return {
// Navigation phases
redirect: entry.redirectEnd - entry.redirectStart,
dns: entry.domainLookupEnd - entry.domainLookupStart,
connect: entry.connectEnd - entry.connectStart,
ssl: entry.connectEnd - entry.secureConnectionStart,
ttfb: entry.responseStart - entry.requestStart, // Time to First Byte
download: entry.responseEnd - entry.responseStart,
domProcessing: entry.domContentLoadedEventStart - entry.responseEnd,
domComplete: entry.domComplete - entry.domContentLoadedEventStart,
loadComplete: entry.loadEventEnd - entry.loadEventStart,
// Total times
totalTime: entry.loadEventEnd - entry.startTime,
// Enhanced metrics
navigationStart: entry.startTime,
unloadTime: entry.unloadEventEnd - entry.unloadEventStart,
redirectCount: entry.redirectCount,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
// Connection info
connectionInfo: {
nextHopProtocol: entry.nextHopProtocol,
renderBlockingStatus: entry.renderBlockingStatus
}
};
} catch (error) {
Logger.error('[PerformanceManager] Navigation timing failed:', error);
return null;
}
},
// Get performance insights
getInsights: () => {
const timing = this.navigation.get();
if (!timing) return null;
return {
insights: {
serverResponseTime: this.getInsight('ttfb', timing.ttfb, 200, 500),
domProcessing: this.getInsight('domProcessing', timing.domProcessing, 500, 1000),
totalLoadTime: this.getInsight('totalTime', timing.totalTime, 2000, 4000),
transferEfficiency: this.getTransferEfficiency(timing)
},
recommendations: this.getNavigationRecommendations(timing)
};
}
};
/**
* Resource Timing API
*/
resources = {
// Get resource timing data
get: (type = null) => {
if (!this.support.resourceTiming) {
Logger.warn('[PerformanceManager] Resource Timing not supported');
return [];
}
try {
const entries = performance.getEntriesByType('resource');
const resources = entries.map(entry => ({
name: entry.name,
type: this.getResourceType(entry),
startTime: entry.startTime,
duration: entry.duration,
size: {
transfer: entry.transferSize,
encoded: entry.encodedBodySize,
decoded: entry.decodedBodySize
},
timing: {
redirect: entry.redirectEnd - entry.redirectStart,
dns: entry.domainLookupEnd - entry.domainLookupStart,
connect: entry.connectEnd - entry.connectStart,
ssl: entry.connectEnd - entry.secureConnectionStart,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart
},
protocol: entry.nextHopProtocol,
cached: entry.transferSize === 0 && entry.decodedBodySize > 0
}));
return type ? resources.filter(r => r.type === type) : resources;
} catch (error) {
Logger.error('[PerformanceManager] Resource timing failed:', error);
return [];
}
},
// Get resource performance summary
getSummary: () => {
const resources = this.resources.get();
const summary = {
total: resources.length,
types: {},
totalSize: 0,
totalDuration: 0,
cached: 0,
slowResources: []
};
resources.forEach(resource => {
const type = resource.type;
if (!summary.types[type]) {
summary.types[type] = { count: 0, size: 0, duration: 0 };
}
summary.types[type].count++;
summary.types[type].size += resource.size.transfer;
summary.types[type].duration += resource.duration;
summary.totalSize += resource.size.transfer;
summary.totalDuration += resource.duration;
if (resource.cached) summary.cached++;
if (resource.duration > 1000) summary.slowResources.push(resource);
});
return summary;
}
};
/**
* Core Web Vitals monitoring
*/
vitals = {
// Start monitoring Core Web Vitals
start: (callback = null) => {
const vitalsData = {};
// First Contentful Paint
this.observePaint((entries) => {
entries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
vitalsData.fcp = entry.startTime;
this.checkThreshold('fcp', entry.startTime);
if (callback) callback('fcp', entry.startTime);
}
});
});
// Largest Contentful Paint
this.observeLCP((entries) => {
entries.forEach(entry => {
vitalsData.lcp = entry.startTime;
this.checkThreshold('lcp', entry.startTime);
if (callback) callback('lcp', entry.startTime);
});
});
// First Input Delay
this.observeFID((entries) => {
entries.forEach(entry => {
vitalsData.fid = entry.processingStart - entry.startTime;
this.checkThreshold('fid', vitalsData.fid);
if (callback) callback('fid', vitalsData.fid);
});
});
// Cumulative Layout Shift
this.observeCLS((entries) => {
let cls = 0;
entries.forEach(entry => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
vitalsData.cls = cls;
this.checkThreshold('cls', cls);
if (callback) callback('cls', cls);
});
return vitalsData;
},
// Get current vitals
get: () => {
return {
fcp: this.getMetric('fcp'),
lcp: this.getMetric('lcp'),
fid: this.getMetric('fid'),
cls: this.getMetric('cls'),
ratings: {
fcp: this.getRating('fcp', this.getMetric('fcp')),
lcp: this.getRating('lcp', this.getMetric('lcp')),
fid: this.getRating('fid', this.getMetric('fid')),
cls: this.getRating('cls', this.getMetric('cls'))
}
};
}
};
/**
* Memory monitoring
*/
memory = {
// Get memory usage
get: () => {
if (!this.support.memory) {
Logger.warn('[PerformanceManager] Memory API not supported');
return null;
}
try {
const memory = performance.memory;
return {
used: memory.usedJSHeapSize,
total: memory.totalJSHeapSize,
limit: memory.jsHeapSizeLimit,
percentage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100,
formatted: {
used: this.formatBytes(memory.usedJSHeapSize),
total: this.formatBytes(memory.totalJSHeapSize),
limit: this.formatBytes(memory.jsHeapSizeLimit)
}
};
} catch (error) {
Logger.error('[PerformanceManager] Memory get failed:', error);
return null;
}
},
// Monitor memory usage
monitor: (callback, interval = 5000) => {
if (!this.support.memory) return null;
const intervalId = setInterval(() => {
const memoryData = this.memory.get();
if (memoryData) {
callback(memoryData);
// Warn if memory usage is high
if (memoryData.percentage > 80) {
Logger.warn('[PerformanceManager] High memory usage detected:', memoryData.percentage.toFixed(1) + '%');
}
}
}, interval);
return {
stop: () => clearInterval(intervalId)
};
}
};
/**
* Long Task monitoring
*/
longTasks = {
// Start monitoring long tasks
start: (callback = null) => {
if (!this.support.longTask) {
Logger.warn('[PerformanceManager] Long Task API not supported');
return null;
}
try {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
const taskInfo = {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
attribution: entry.attribution || []
};
Logger.warn('[PerformanceManager] Long task detected:', taskInfo);
if (callback) callback(taskInfo);
});
});
observer.observe({ entryTypes: ['longtask'] });
const observerId = this.generateId('longtask');
this.observers.set(observerId, observer);
return {
id: observerId,
stop: () => {
observer.disconnect();
this.observers.delete(observerId);
}
};
} catch (error) {
Logger.error('[PerformanceManager] Long task monitoring failed:', error);
return null;
}
}
};
/**
* Performance optimization utilities
*/
optimize = {
// Defer non-critical JavaScript
deferScript: (src, callback = null) => {
const script = document.createElement('script');
script.src = src;
script.defer = true;
if (callback) script.onload = callback;
document.head.appendChild(script);
return script;
},
// Preload critical resources
preload: (href, as, crossorigin = false) => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = as;
if (crossorigin) link.crossOrigin = 'anonymous';
document.head.appendChild(link);
return link;
},
// Prefetch future resources
prefetch: (href) => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = href;
document.head.appendChild(link);
return link;
},
// Optimize images with lazy loading
lazyImages: (selector = 'img[data-src]') => {
if ('IntersectionObserver' in window) {
const images = document.querySelectorAll(selector);
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
return imageObserver;
} else {
// Fallback for older browsers
const images = document.querySelectorAll(selector);
images.forEach(img => {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
},
// Bundle size analyzer
analyzeBundles: () => {
const scripts = Array.from(document.querySelectorAll('script[src]'));
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
const analysis = {
scripts: scripts.map(script => ({
src: script.src,
async: script.async,
defer: script.defer
})),
styles: styles.map(style => ({
href: style.href,
media: style.media
})),
recommendations: []
};
// Add recommendations
if (scripts.length > 10) {
analysis.recommendations.push('Consider bundling JavaScript files');
}
if (styles.length > 5) {
analysis.recommendations.push('Consider bundling CSS files');
}
return analysis;
}
};
// Helper methods
startCoreMetrics() {
// Auto-start vitals monitoring
this.vitals.start((metric, value) => {
this.setMetric(metric, value);
});
// Start memory monitoring if supported
if (this.support.memory) {
this.memory.monitor((memoryData) => {
this.setMetric('memory', memoryData);
});
}
}
observePaint(callback) {
if (!this.support.paintTiming) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['paint'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] Paint observer failed:', error);
}
}
observeLCP(callback) {
if (!this.support.observer) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] LCP observer failed:', error);
}
}
observeFID(callback) {
if (!this.support.observer) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['first-input'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] FID observer failed:', error);
}
}
observeCLS(callback) {
if (!this.support.layoutInstability) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['layout-shift'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] CLS observer failed:', error);
}
}
getLegacyNavigationTiming() {
if (!this.support.timing) return null;
const timing = performance.timing;
const navigationStart = timing.navigationStart;
return {
redirect: timing.redirectEnd - timing.redirectStart,
dns: timing.domainLookupEnd - timing.domainLookupStart,
connect: timing.connectEnd - timing.connectStart,
ssl: timing.connectEnd - timing.secureConnectionStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domProcessing: timing.domContentLoadedEventStart - timing.responseEnd,
domComplete: timing.domComplete - timing.domContentLoadedEventStart,
loadComplete: timing.loadEventEnd - timing.loadEventStart,
totalTime: timing.loadEventEnd - navigationStart
};
}
getResourceType(entry) {
const url = new URL(entry.name);
const extension = url.pathname.split('.').pop().toLowerCase();
const typeMap = {
js: 'script',
css: 'stylesheet',
png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', svg: 'image', webp: 'image',
woff: 'font', woff2: 'font', ttf: 'font', eot: 'font',
json: 'fetch', xml: 'fetch'
};
return typeMap[extension] || entry.initiatorType || 'other';
}
getInsight(metric, value, good, needs) {
if (value < good) return { rating: 'good', message: `Excellent ${metric}` };
if (value < needs) return { rating: 'needs-improvement', message: `${metric} needs improvement` };
return { rating: 'poor', message: `Poor ${metric} performance` };
}
getTransferEfficiency(timing) {
const compressionRatio = timing.decodedBodySize > 0 ?
timing.encodedBodySize / timing.decodedBodySize : 1;
return {
ratio: compressionRatio,
rating: compressionRatio < 0.7 ? 'good' : compressionRatio < 0.9 ? 'fair' : 'poor'
};
}
getNavigationRecommendations(timing) {
const recommendations = [];
if (timing.ttfb > 500) {
recommendations.push('Server response time is slow. Consider optimizing backend performance.');
}
if (timing.dns > 100) {
recommendations.push('DNS lookup time is high. Consider using a faster DNS provider.');
}
if (timing.connect > 1000) {
recommendations.push('Connection time is slow. Check network latency.');
}
if (timing.domProcessing > 1000) {
recommendations.push('DOM processing is slow. Consider optimizing JavaScript execution.');
}
return recommendations;
}
checkThreshold(metric, value) {
const threshold = this.thresholds[metric];
if (threshold && value > threshold) {
Logger.warn(`[PerformanceManager] ${metric.toUpperCase()} threshold exceeded: ${value}ms (threshold: ${threshold}ms)`);
}
}
getRating(metric, value) {
const thresholds = {
fcp: { good: 1800, poor: 3000 },
lcp: { good: 2500, poor: 4000 },
fid: { good: 100, poor: 300 },
cls: { good: 0.1, poor: 0.25 }
};
const threshold = thresholds[metric];
if (!threshold || value === null || value === undefined) return 'unknown';
if (value <= threshold.good) return 'good';
if (value <= threshold.poor) return 'needs-improvement';
return 'poor';
}
setMetric(key, value) {
this.metrics.set(key, {
value,
timestamp: Date.now()
});
}
getMetric(key) {
const metric = this.metrics.get(key);
return metric ? metric.value : null;
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
generateId(prefix = 'perf') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Stop all observers and clear data
*/
cleanup() {
this.observers.forEach(observer => {
observer.disconnect();
});
this.observers.clear();
this.metrics.clear();
this.marks.clear();
this.measures.clear();
Logger.info('[PerformanceManager] Cleanup completed');
}
/**
* Get comprehensive performance report
*/
getReport() {
return {
support: this.support,
navigation: this.navigation.get(),
resources: this.resources.getSummary(),
vitals: this.vitals.get(),
memory: this.memory.get(),
marks: this.timing.getMarks(),
measures: this.timing.getMeasures(),
activeObservers: this.observers.size,
timestamp: Date.now()
};
}
}

View File

@@ -0,0 +1,687 @@
// modules/api-manager/PermissionManager.js
import { Logger } from '../../core/logger.js';
/**
* Permission Management System for Web APIs
*/
export class PermissionManager {
constructor(config = {}) {
this.config = config;
this.permissionCache = new Map();
this.permissionWatchers = new Map();
this.requestQueue = new Map();
// Check Permissions API support
this.support = {
permissions: 'permissions' in navigator,
query: navigator.permissions?.query !== undefined,
request: 'requestPermission' in Notification || 'getUserMedia' in (navigator.mediaDevices || {}),
geolocation: 'geolocation' in navigator,
notifications: 'Notification' in window,
camera: navigator.mediaDevices !== undefined,
microphone: navigator.mediaDevices !== undefined,
clipboard: 'clipboard' in navigator,
vibration: 'vibrate' in navigator
};
// Permission mappings for different APIs
this.permissionMap = {
camera: { name: 'camera', api: 'mediaDevices' },
microphone: { name: 'microphone', api: 'mediaDevices' },
geolocation: { name: 'geolocation', api: 'geolocation' },
notifications: { name: 'notifications', api: 'notification' },
'clipboard-read': { name: 'clipboard-read', api: 'clipboard' },
'clipboard-write': { name: 'clipboard-write', api: 'clipboard' },
'background-sync': { name: 'background-sync', api: 'serviceWorker' },
'persistent-storage': { name: 'persistent-storage', api: 'storage' },
'push': { name: 'push', api: 'serviceWorker' },
'midi': { name: 'midi', api: 'midi' },
'payment-handler': { name: 'payment-handler', api: 'payment' }
};
Logger.info('[PermissionManager] Initialized with support:', this.support);
// Auto-cache current permissions
this.cacheCurrentPermissions();
}
/**
* Check permission status for a specific permission
*/
async check(permission) {
if (!this.support.permissions || !this.support.query) {
Logger.warn('[PermissionManager] Permissions API not supported, using fallback');
return this.checkFallback(permission);
}
try {
// Check cache first
const cached = this.permissionCache.get(permission);
if (cached && Date.now() - cached.timestamp < 30000) { // 30s cache
return cached.status;
}
const permissionDescriptor = this.getPermissionDescriptor(permission);
const status = await navigator.permissions.query(permissionDescriptor);
const result = {
name: permission,
state: status.state,
timestamp: Date.now(),
supported: true
};
this.permissionCache.set(permission, result);
// Watch for changes
status.addEventListener('change', () => {
this.onPermissionChange(permission, status.state);
});
Logger.info(`[PermissionManager] Permission checked: ${permission} = ${status.state}`);
return result;
} catch (error) {
Logger.warn(`[PermissionManager] Permission check failed for ${permission}:`, error.message);
return this.checkFallback(permission);
}
}
/**
* Request permission for a specific API
*/
async request(permission, options = {}) {
const {
showRationale = true,
fallbackMessage = null,
timeout = 30000
} = options;
try {
// Check current status first
const currentStatus = await this.check(permission);
if (currentStatus.state === 'granted') {
return { granted: true, state: 'granted', fromCache: true };
}
if (currentStatus.state === 'denied') {
if (showRationale) {
await this.showPermissionRationale(permission, fallbackMessage);
}
return { granted: false, state: 'denied', reason: 'previously-denied' };
}
// Add to request queue to prevent multiple simultaneous requests
const queueKey = permission;
if (this.requestQueue.has(queueKey)) {
Logger.info(`[PermissionManager] Permission request already in progress: ${permission}`);
return this.requestQueue.get(queueKey);
}
const requestPromise = this.executePermissionRequest(permission, timeout);
this.requestQueue.set(queueKey, requestPromise);
const result = await requestPromise;
this.requestQueue.delete(queueKey);
// Update cache
this.permissionCache.set(permission, {
name: permission,
state: result.state,
timestamp: Date.now(),
supported: true
});
return result;
} catch (error) {
Logger.error(`[PermissionManager] Permission request failed: ${permission}`, error);
this.requestQueue.delete(permission);
return { granted: false, state: 'error', error: error.message };
}
}
/**
* Request multiple permissions at once
*/
async requestMultiple(permissions, options = {}) {
const results = {};
if (options.sequential) {
// Request permissions one by one
for (const permission of permissions) {
results[permission] = await this.request(permission, options);
// Stop if any critical permission is denied
if (options.requireAll && !results[permission].granted) {
break;
}
}
} else {
// Request permissions in parallel
const requests = permissions.map(permission =>
this.request(permission, options).then(result => [permission, result])
);
const responses = await Promise.allSettled(requests);
responses.forEach(response => {
if (response.status === 'fulfilled') {
const [permission, result] = response.value;
results[permission] = result;
} else {
Logger.error('[PermissionManager] Batch permission request failed:', response.reason);
}
});
}
const summary = {
results,
granted: Object.values(results).filter(r => r.granted).length,
denied: Object.values(results).filter(r => !r.granted).length,
total: Object.keys(results).length
};
Logger.info(`[PermissionManager] Batch permissions: ${summary.granted}/${summary.total} granted`);
return summary;
}
/**
* Check if all required permissions are granted
*/
async checkRequired(requiredPermissions) {
const statuses = {};
const missing = [];
for (const permission of requiredPermissions) {
const status = await this.check(permission);
statuses[permission] = status;
if (status.state !== 'granted') {
missing.push(permission);
}
}
return {
allGranted: missing.length === 0,
granted: Object.keys(statuses).filter(p => statuses[p].state === 'granted'),
missing,
statuses
};
}
/**
* Watch permission changes
*/
watch(permission, callback) {
const watcherId = this.generateId('watcher');
this.permissionWatchers.set(watcherId, {
permission,
callback,
active: true
});
// Set up the watcher
this.setupPermissionWatcher(permission, callback);
Logger.info(`[PermissionManager] Permission watcher created: ${permission}`);
return {
id: watcherId,
stop: () => {
const watcher = this.permissionWatchers.get(watcherId);
if (watcher) {
watcher.active = false;
this.permissionWatchers.delete(watcherId);
Logger.info(`[PermissionManager] Permission watcher stopped: ${permission}`);
}
}
};
}
/**
* Get permission recommendations based on app features
*/
getRecommendations(features = []) {
const recommendations = {
essential: [],
recommended: [],
optional: []
};
const featurePermissionMap = {
camera: { permissions: ['camera'], priority: 'essential' },
microphone: { permissions: ['microphone'], priority: 'essential' },
location: { permissions: ['geolocation'], priority: 'recommended' },
notifications: { permissions: ['notifications'], priority: 'recommended' },
clipboard: { permissions: ['clipboard-read', 'clipboard-write'], priority: 'optional' },
offline: { permissions: ['persistent-storage'], priority: 'recommended' },
background: { permissions: ['background-sync', 'push'], priority: 'optional' }
};
features.forEach(feature => {
const mapping = featurePermissionMap[feature];
if (mapping) {
recommendations[mapping.priority].push(...mapping.permissions);
}
});
// Remove duplicates
Object.keys(recommendations).forEach(key => {
recommendations[key] = [...new Set(recommendations[key])];
});
return recommendations;
}
/**
* Create permission onboarding flow
*/
createOnboardingFlow(permissions, options = {}) {
const {
title = 'Permissions Required',
descriptions = {},
onComplete = null,
onSkip = null
} = options;
return {
permissions,
title,
descriptions,
async start() {
Logger.info('[PermissionManager] Starting onboarding flow');
const results = [];
for (const permission of permissions) {
const description = descriptions[permission] || this.getDefaultDescription(permission);
// Show permission explanation
const userChoice = await this.showPermissionDialog(permission, description);
if (userChoice === 'grant') {
const result = await this.request(permission);
results.push({ permission, ...result });
} else if (userChoice === 'skip') {
results.push({ permission, granted: false, skipped: true });
} else {
// User cancelled the entire flow
if (onSkip) onSkip(results);
return { cancelled: true, results };
}
}
const summary = {
completed: true,
granted: results.filter(r => r.granted).length,
total: results.length,
results
};
if (onComplete) onComplete(summary);
return summary;
},
getDefaultDescription(permission) {
const descriptions = {
camera: 'Take photos and record videos for enhanced functionality',
microphone: 'Record audio for voice features and communication',
geolocation: 'Provide location-based services and content',
notifications: 'Send you important updates and reminders',
'clipboard-read': 'Access clipboard content for convenience features',
'clipboard-write': 'Copy content to clipboard for easy sharing'
};
return descriptions[permission] || `Allow access to ${permission} functionality`;
},
async showPermissionDialog(permission, description) {
return new Promise(resolve => {
// Create modal dialog
const modal = document.createElement('div');
modal.className = 'permission-dialog-modal';
modal.innerHTML = `
<div class="permission-dialog-backdrop" style="
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
">
<div class="permission-dialog" style="
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 400px;
margin: 1rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
">
<h3 style="margin: 0 0 1rem 0; color: #333;">
${this.getPermissionIcon(permission)} ${this.getPermissionTitle(permission)}
</h3>
<p style="margin: 0 0 2rem 0; color: #666; line-height: 1.5;">
${description}
</p>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button class="btn-skip" style="
background: #f5f5f5;
border: 1px solid #ddd;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
">Skip</button>
<button class="btn-grant" style="
background: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
">Allow</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const skipBtn = modal.querySelector('.btn-skip');
const grantBtn = modal.querySelector('.btn-grant');
const cleanup = () => document.body.removeChild(modal);
skipBtn.onclick = () => {
cleanup();
resolve('skip');
};
grantBtn.onclick = () => {
cleanup();
resolve('grant');
};
// Close on backdrop click
modal.querySelector('.permission-dialog-backdrop').onclick = (e) => {
if (e.target === e.currentTarget) {
cleanup();
resolve('cancel');
}
};
});
},
getPermissionIcon(permission) {
const icons = {
camera: '📷',
microphone: '🎤',
geolocation: '📍',
notifications: '🔔',
'clipboard-read': '📋',
'clipboard-write': '📋'
};
return icons[permission] || '🔐';
},
getPermissionTitle(permission) {
const titles = {
camera: 'Camera Access',
microphone: 'Microphone Access',
geolocation: 'Location Access',
notifications: 'Notifications',
'clipboard-read': 'Clipboard Access',
'clipboard-write': 'Clipboard Access'
};
return titles[permission] || `${permission} Permission`;
}
};
}
// Helper methods
async cacheCurrentPermissions() {
if (!this.support.permissions) return;
const commonPermissions = ['camera', 'microphone', 'geolocation', 'notifications'];
for (const permission of commonPermissions) {
try {
await this.check(permission);
} catch (error) {
// Ignore errors during initial caching
}
}
}
getPermissionDescriptor(permission) {
// Handle different permission descriptor formats
switch (permission) {
case 'camera':
return { name: 'camera' };
case 'microphone':
return { name: 'microphone' };
case 'geolocation':
return { name: 'geolocation' };
case 'notifications':
return { name: 'notifications' };
case 'clipboard-read':
return { name: 'clipboard-read' };
case 'clipboard-write':
return { name: 'clipboard-write' };
case 'persistent-storage':
return { name: 'persistent-storage' };
case 'background-sync':
return { name: 'background-sync' };
case 'push':
return { name: 'push', userVisibleOnly: true };
default:
return { name: permission };
}
}
async checkFallback(permission) {
// Fallback checks for browsers without Permissions API
const fallbacks = {
camera: () => navigator.mediaDevices !== undefined,
microphone: () => navigator.mediaDevices !== undefined,
geolocation: () => 'geolocation' in navigator,
notifications: () => 'Notification' in window,
'clipboard-read': () => 'clipboard' in navigator,
'clipboard-write': () => 'clipboard' in navigator
};
const check = fallbacks[permission];
const supported = check ? check() : false;
return {
name: permission,
state: supported ? 'prompt' : 'unsupported',
timestamp: Date.now(),
supported,
fallback: true
};
}
async executePermissionRequest(permission, timeout) {
return new Promise(async (resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Permission request timeout'));
}, timeout);
try {
let result;
switch (permission) {
case 'notifications':
if ('Notification' in window) {
const notificationPermission = await Notification.requestPermission();
result = { granted: notificationPermission === 'granted', state: notificationPermission };
} else {
result = { granted: false, state: 'unsupported' };
}
break;
case 'camera':
case 'microphone':
try {
const constraints = {};
constraints[permission === 'camera' ? 'video' : 'audio'] = true;
const stream = await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach(track => track.stop()); // Stop immediately
result = { granted: true, state: 'granted' };
} catch (error) {
result = {
granted: false,
state: error.name === 'NotAllowedError' ? 'denied' : 'error',
error: error.message
};
}
break;
case 'geolocation':
try {
await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
timeout: 10000,
maximumAge: 0
});
});
result = { granted: true, state: 'granted' };
} catch (error) {
result = {
granted: false,
state: error.code === 1 ? 'denied' : 'error',
error: error.message
};
}
break;
default:
// Try generic permission request
if (this.support.permissions) {
const status = await navigator.permissions.query(this.getPermissionDescriptor(permission));
result = { granted: status.state === 'granted', state: status.state };
} else {
result = { granted: false, state: 'unsupported' };
}
}
clearTimeout(timeoutId);
resolve(result);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
});
}
async setupPermissionWatcher(permission, callback) {
if (!this.support.permissions) return;
try {
const status = await navigator.permissions.query(this.getPermissionDescriptor(permission));
status.addEventListener('change', () => {
callback({
permission,
state: status.state,
timestamp: Date.now()
});
});
} catch (error) {
Logger.warn(`[PermissionManager] Could not set up watcher for ${permission}:`, error);
}
}
onPermissionChange(permission, newState) {
Logger.info(`[PermissionManager] Permission changed: ${permission}${newState}`);
// Update cache
this.permissionCache.set(permission, {
name: permission,
state: newState,
timestamp: Date.now(),
supported: true
});
// Notify watchers
this.permissionWatchers.forEach(watcher => {
if (watcher.permission === permission && watcher.active) {
watcher.callback({
permission,
state: newState,
timestamp: Date.now()
});
}
});
}
async showPermissionRationale(permission, customMessage = null) {
const rationales = {
camera: 'Camera access is needed to take photos and record videos. You can enable this in your browser settings.',
microphone: 'Microphone access is needed for audio recording and voice features. Please check your browser settings.',
geolocation: 'Location access helps provide relevant local content. You can manage this in your browser settings.',
notifications: 'Notifications keep you updated with important information. You can change this anytime in settings.'
};
const message = customMessage || rationales[permission] || `${permission} permission was denied. Please enable it in your browser settings to use this feature.`;
// Simple alert for now - could be enhanced with custom UI
if (typeof window !== 'undefined' && window.confirm) {
return window.confirm(message + '\n\nWould you like to try again?');
}
return false;
}
generateId(prefix = 'perm') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Get comprehensive permission status report
*/
async getPermissionReport() {
const report = {
support: this.support,
cached: Array.from(this.permissionCache.entries()).map(([name, data]) => ({
name,
...data
})),
watchers: Array.from(this.permissionWatchers.keys()),
timestamp: Date.now()
};
return report;
}
/**
* Clear all cached permissions
*/
clearCache() {
this.permissionCache.clear();
Logger.info('[PermissionManager] Permission cache cleared');
}
/**
* Stop all watchers and cleanup
*/
cleanup() {
this.permissionWatchers.forEach(watcher => {
watcher.active = false;
});
this.permissionWatchers.clear();
this.requestQueue.clear();
this.permissionCache.clear();
Logger.info('[PermissionManager] Cleanup completed');
}
}

View File

@@ -0,0 +1,761 @@
// modules/api-manager/StorageManager.js
import { Logger } from '../../core/logger.js';
/**
* Storage APIs Manager - IndexedDB, Cache API, Web Locks, Broadcast Channel
*/
export class StorageManager {
constructor(config = {}) {
this.config = config;
this.dbName = config.dbName || 'AppDatabase';
this.dbVersion = config.dbVersion || 1;
this.db = null;
this.cache = null;
this.channels = new Map();
// Check API support
this.support = {
indexedDB: 'indexedDB' in window,
cacheAPI: 'caches' in window,
webLocks: 'locks' in navigator,
broadcastChannel: 'BroadcastChannel' in window,
localStorage: 'localStorage' in window,
sessionStorage: 'sessionStorage' in window
};
Logger.info('[StorageManager] Initialized with support:', this.support);
// Initialize databases
this.initializeDB();
this.initializeCache();
}
/**
* Initialize IndexedDB
*/
async initializeDB() {
if (!this.support.indexedDB) {
Logger.warn('[StorageManager] IndexedDB not supported');
return;
}
try {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => {
Logger.error('[StorageManager] IndexedDB failed to open');
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create default object stores
if (!db.objectStoreNames.contains('keyValue')) {
const store = db.createObjectStore('keyValue', { keyPath: 'key' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
if (!db.objectStoreNames.contains('cache')) {
db.createObjectStore('cache', { keyPath: 'key' });
}
if (!db.objectStoreNames.contains('files')) {
const fileStore = db.createObjectStore('files', { keyPath: 'id', autoIncrement: true });
fileStore.createIndex('name', 'name', { unique: false });
fileStore.createIndex('type', 'type', { unique: false });
}
Logger.info('[StorageManager] IndexedDB schema updated');
};
request.onsuccess = (event) => {
this.db = event.target.result;
Logger.info('[StorageManager] IndexedDB connected');
};
} catch (error) {
Logger.error('[StorageManager] IndexedDB initialization failed:', error);
}
}
/**
* Initialize Cache API
*/
async initializeCache() {
if (!this.support.cacheAPI) {
Logger.warn('[StorageManager] Cache API not supported');
return;
}
try {
this.cache = await caches.open(this.config.cacheName || 'app-cache-v1');
Logger.info('[StorageManager] Cache API initialized');
} catch (error) {
Logger.error('[StorageManager] Cache API initialization failed:', error);
}
}
/**
* IndexedDB Operations
*/
db = {
// Set key-value data
set: async (key, value, expiration = null) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const data = {
key,
value,
timestamp: Date.now(),
expiration: expiration ? Date.now() + expiration : null
};
const request = store.put(data);
request.onsuccess = () => {
Logger.info(`[StorageManager] DB set: ${key}`);
resolve(data);
};
request.onerror = () => {
Logger.error(`[StorageManager] DB set failed: ${key}`);
reject(request.error);
};
});
},
// Get key-value data
get: async (key) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readonly');
const store = transaction.objectStore('keyValue');
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
if (!result) {
resolve(null);
return;
}
// Check expiration
if (result.expiration && Date.now() > result.expiration) {
this.db.delete(key); // Auto-cleanup expired data
resolve(null);
return;
}
resolve(result.value);
};
request.onerror = () => {
Logger.error(`[StorageManager] DB get failed: ${key}`);
reject(request.error);
};
});
},
// Delete key
delete: async (key) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const request = store.delete(key);
request.onsuccess = () => {
Logger.info(`[StorageManager] DB deleted: ${key}`);
resolve(true);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Get all keys
keys: async () => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readonly');
const store = transaction.objectStore('keyValue');
const request = store.getAllKeys();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Clear all data
clear: async () => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const request = store.clear();
request.onsuccess = () => {
Logger.info('[StorageManager] DB cleared');
resolve(true);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Store files/blobs
storeFile: async (name, file, metadata = {}) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['files'], 'readwrite');
const store = transaction.objectStore('files');
const fileData = {
name,
file,
type: file.type,
size: file.size,
timestamp: Date.now(),
metadata
};
const request = store.add(fileData);
request.onsuccess = () => {
Logger.info(`[StorageManager] File stored: ${name}`);
resolve({ id: request.result, ...fileData });
};
request.onerror = () => {
reject(request.error);
};
});
},
// Get file
getFile: async (id) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['files'], 'readonly');
const store = transaction.objectStore('files');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
};
/**
* Cache API Operations
*/
cache = {
// Add request/response to cache
add: async (request, response = null) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
if (response) {
await this.cache.put(request, response);
} else {
await this.cache.add(request);
}
Logger.info(`[StorageManager] Cache add: ${request}`);
} catch (error) {
Logger.error('[StorageManager] Cache add failed:', error);
throw error;
}
},
// Get from cache
get: async (request) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const response = await this.cache.match(request);
if (response) {
Logger.info(`[StorageManager] Cache hit: ${request}`);
} else {
Logger.info(`[StorageManager] Cache miss: ${request}`);
}
return response;
} catch (error) {
Logger.error('[StorageManager] Cache get failed:', error);
throw error;
}
},
// Delete from cache
delete: async (request) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const success = await this.cache.delete(request);
if (success) {
Logger.info(`[StorageManager] Cache deleted: ${request}`);
}
return success;
} catch (error) {
Logger.error('[StorageManager] Cache delete failed:', error);
throw error;
}
},
// Get all cached requests
keys: async () => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
return await this.cache.keys();
} catch (error) {
Logger.error('[StorageManager] Cache keys failed:', error);
throw error;
}
},
// Clear cache
clear: async () => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const keys = await this.cache.keys();
await Promise.all(keys.map(key => this.cache.delete(key)));
Logger.info('[StorageManager] Cache cleared');
} catch (error) {
Logger.error('[StorageManager] Cache clear failed:', error);
throw error;
}
}
};
/**
* Broadcast Channel for cross-tab communication
*/
channel = {
// Create or get channel
create: (channelName, onMessage = null) => {
if (!this.support.broadcastChannel) {
Logger.warn('[StorageManager] BroadcastChannel not supported');
return null;
}
if (this.channels.has(channelName)) {
return this.channels.get(channelName);
}
const channel = new BroadcastChannel(channelName);
if (onMessage) {
channel.addEventListener('message', onMessage);
}
const channelWrapper = {
name: channelName,
channel,
send: (data) => {
channel.postMessage({
data,
timestamp: Date.now(),
sender: 'current-tab'
});
Logger.info(`[StorageManager] Broadcast sent to ${channelName}`);
},
onMessage: (callback) => {
channel.addEventListener('message', (event) => {
callback(event.data);
});
},
close: () => {
channel.close();
this.channels.delete(channelName);
Logger.info(`[StorageManager] Channel closed: ${channelName}`);
}
};
this.channels.set(channelName, channelWrapper);
Logger.info(`[StorageManager] Channel created: ${channelName}`);
return channelWrapper;
},
// Get existing channel
get: (channelName) => {
return this.channels.get(channelName) || null;
},
// Close channel
close: (channelName) => {
const channel = this.channels.get(channelName);
if (channel) {
channel.close();
}
},
// Close all channels
closeAll: () => {
this.channels.forEach(channel => channel.close());
this.channels.clear();
Logger.info('[StorageManager] All channels closed');
}
};
/**
* Web Locks API for resource locking
*/
locks = {
// Acquire lock
acquire: async (lockName, callback, options = {}) => {
if (!this.support.webLocks) {
Logger.warn('[StorageManager] Web Locks not supported, executing without lock');
return await callback();
}
try {
return await navigator.locks.request(lockName, options, async (lock) => {
Logger.info(`[StorageManager] Lock acquired: ${lockName}`);
const result = await callback(lock);
Logger.info(`[StorageManager] Lock released: ${lockName}`);
return result;
});
} catch (error) {
Logger.error(`[StorageManager] Lock failed: ${lockName}`, error);
throw error;
}
},
// Query locks
query: async () => {
if (!this.support.webLocks) {
return { held: [], pending: [] };
}
try {
return await navigator.locks.query();
} catch (error) {
Logger.error('[StorageManager] Lock query failed:', error);
throw error;
}
}
};
/**
* Local/Session Storage helpers (with JSON support)
*/
local = {
set: (key, value, expiration = null) => {
if (!this.support.localStorage) return false;
try {
const data = {
value,
timestamp: Date.now(),
expiration: expiration ? Date.now() + expiration : null
};
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (error) {
Logger.error('[StorageManager] localStorage set failed:', error);
return false;
}
},
get: (key) => {
if (!this.support.localStorage) return null;
try {
const item = localStorage.getItem(key);
if (!item) return null;
const data = JSON.parse(item);
// Check expiration
if (data.expiration && Date.now() > data.expiration) {
localStorage.removeItem(key);
return null;
}
return data.value;
} catch (error) {
Logger.error('[StorageManager] localStorage get failed:', error);
return null;
}
},
delete: (key) => {
if (!this.support.localStorage) return false;
localStorage.removeItem(key);
return true;
},
clear: () => {
if (!this.support.localStorage) return false;
localStorage.clear();
return true;
},
keys: () => {
if (!this.support.localStorage) return [];
return Object.keys(localStorage);
}
};
session = {
set: (key, value) => {
if (!this.support.sessionStorage) return false;
try {
sessionStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
Logger.error('[StorageManager] sessionStorage set failed:', error);
return false;
}
},
get: (key) => {
if (!this.support.sessionStorage) return null;
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
Logger.error('[StorageManager] sessionStorage get failed:', error);
return null;
}
},
delete: (key) => {
if (!this.support.sessionStorage) return false;
sessionStorage.removeItem(key);
return true;
},
clear: () => {
if (!this.support.sessionStorage) return false;
sessionStorage.clear();
return true;
}
};
/**
* Smart caching with automatic expiration
*/
smartCache = {
set: async (key, value, options = {}) => {
const {
storage = 'indexedDB',
expiration = null,
fallback = true
} = options;
try {
if (storage === 'indexedDB' && this.db) {
return await this.db.set(key, value, expiration);
} else if (fallback) {
return this.local.set(key, value, expiration);
}
} catch (error) {
if (fallback) {
return this.local.set(key, value, expiration);
}
throw error;
}
},
get: async (key, options = {}) => {
const { storage = 'indexedDB', fallback = true } = options;
try {
if (storage === 'indexedDB' && this.db) {
return await this.db.get(key);
} else if (fallback) {
return this.local.get(key);
}
} catch (error) {
if (fallback) {
return this.local.get(key);
}
throw error;
}
},
delete: async (key, options = {}) => {
const { storage = 'indexedDB', fallback = true } = options;
try {
if (storage === 'indexedDB' && this.db) {
await this.db.delete(key);
}
if (fallback) {
this.local.delete(key);
}
} catch (error) {
if (fallback) {
this.local.delete(key);
}
throw error;
}
}
};
/**
* Get storage usage statistics
*/
async getStorageUsage() {
const stats = {
quota: 0,
usage: 0,
available: 0,
percentage: 0
};
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
stats.quota = estimate.quota || 0;
stats.usage = estimate.usage || 0;
stats.available = stats.quota - stats.usage;
stats.percentage = stats.quota > 0 ? Math.round((stats.usage / stats.quota) * 100) : 0;
} catch (error) {
Logger.error('[StorageManager] Storage estimate failed:', error);
}
}
return stats;
}
/**
* Cleanup expired data
*/
async cleanup() {
Logger.info('[StorageManager] Starting cleanup...');
try {
// Cleanup IndexedDB expired entries
if (this.db) {
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const index = store.index('timestamp');
const request = index.openCursor();
let cleaned = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
if (data.expiration && Date.now() > data.expiration) {
cursor.delete();
cleaned++;
}
cursor.continue();
} else {
Logger.info(`[StorageManager] Cleanup completed: ${cleaned} expired entries removed`);
}
};
}
// Cleanup localStorage expired entries
if (this.support.localStorage) {
const keys = Object.keys(localStorage);
let localCleaned = 0;
keys.forEach(key => {
try {
const item = localStorage.getItem(key);
const data = JSON.parse(item);
if (data.expiration && Date.now() > data.expiration) {
localStorage.removeItem(key);
localCleaned++;
}
} catch (error) {
// Ignore non-JSON items
}
});
if (localCleaned > 0) {
Logger.info(`[StorageManager] LocalStorage cleanup: ${localCleaned} expired entries removed`);
}
}
} catch (error) {
Logger.error('[StorageManager] Cleanup failed:', error);
}
}
/**
* Get comprehensive storage status
*/
async getStatus() {
const usage = await this.getStorageUsage();
return {
support: this.support,
usage,
activeChannels: this.channels.size,
dbConnected: !!this.db,
cacheConnected: !!this.cache,
channelNames: Array.from(this.channels.keys())
};
}
}

View File

@@ -0,0 +1,648 @@
// modules/api-manager/WorkerManager.js
import { Logger } from '../../core/logger.js';
/**
* Worker APIs Manager - Web Workers, Service Workers, Shared Workers
*/
export class WorkerManager {
constructor(config = {}) {
this.config = config;
this.activeWorkers = new Map();
this.messageHandlers = new Map();
this.workerScripts = new Map();
// Check API support
this.support = {
webWorkers: 'Worker' in window,
serviceWorker: 'serviceWorker' in navigator,
sharedWorker: 'SharedWorker' in window,
offscreenCanvas: 'OffscreenCanvas' in window
};
Logger.info('[WorkerManager] Initialized with support:', this.support);
}
/**
* Web Workers for background processing
*/
web = {
// Create a new Web Worker
create: (script, options = {}) => {
if (!this.support.webWorkers) {
throw new Error('Web Workers not supported');
}
let worker;
const workerId = this.generateId('worker');
try {
// Handle different script types
if (typeof script === 'string') {
// URL string
worker = new Worker(script, options);
} else if (typeof script === 'function') {
// Function to blob
const blob = this.createWorkerBlob(script);
worker = new Worker(URL.createObjectURL(blob), options);
} else if (script instanceof Blob) {
// Blob directly
worker = new Worker(URL.createObjectURL(script), options);
} else {
throw new Error('Invalid script type');
}
// Enhanced worker wrapper
const workerWrapper = {
id: workerId,
worker,
// Send message to worker
send: (message, transfer = null) => {
if (transfer) {
worker.postMessage(message, transfer);
} else {
worker.postMessage(message);
}
Logger.info(`[WorkerManager] Message sent to worker: ${workerId}`);
},
// Listen for messages
onMessage: (callback) => {
worker.addEventListener('message', (event) => {
callback(event.data, event);
});
},
// Handle errors
onError: (callback) => {
worker.addEventListener('error', callback);
worker.addEventListener('messageerror', callback);
},
// Terminate worker
terminate: () => {
worker.terminate();
this.activeWorkers.delete(workerId);
Logger.info(`[WorkerManager] Worker terminated: ${workerId}`);
},
// Execute function in worker
execute: (fn, data = null) => {
return new Promise((resolve, reject) => {
const messageId = this.generateId('msg');
const handler = (event) => {
if (event.data.id === messageId) {
worker.removeEventListener('message', handler);
if (event.data.error) {
reject(new Error(event.data.error));
} else {
resolve(event.data.result);
}
}
};
worker.addEventListener('message', handler);
worker.postMessage({
id: messageId,
type: 'execute',
function: fn.toString(),
data
});
// Timeout after 30 seconds
setTimeout(() => {
worker.removeEventListener('message', handler);
reject(new Error('Worker execution timeout'));
}, 30000);
});
}
};
this.activeWorkers.set(workerId, workerWrapper);
Logger.info(`[WorkerManager] Web Worker created: ${workerId}`);
return workerWrapper;
} catch (error) {
Logger.error('[WorkerManager] Worker creation failed:', error);
throw error;
}
},
// Create worker pool for parallel processing
createPool: (script, poolSize = navigator.hardwareConcurrency || 4, options = {}) => {
const workers = [];
for (let i = 0; i < poolSize; i++) {
workers.push(this.web.create(script, options));
}
let currentWorker = 0;
const pool = {
workers,
// Execute task on next available worker
execute: async (fn, data = null) => {
const worker = workers[currentWorker];
currentWorker = (currentWorker + 1) % workers.length;
return worker.execute(fn, data);
},
// Broadcast message to all workers
broadcast: (message) => {
workers.forEach(worker => {
worker.send(message);
});
},
// Terminate all workers
terminate: () => {
workers.forEach(worker => {
worker.terminate();
});
workers.length = 0;
Logger.info('[WorkerManager] Worker pool terminated');
}
};
Logger.info(`[WorkerManager] Worker pool created with ${poolSize} workers`);
return pool;
},
// Common worker tasks
tasks: {
// Heavy computation
compute: (fn, data) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, function: fnString, data } = e.data;
try {
const fn = new Function('return ' + fnString)();
const result = fn(data);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(fn, data);
},
// Image processing
processImage: (imageData, filters) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, data: { imageData, filters } } = e.data;
try {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
// Apply filters
if (filters.brightness) {
pixels[i] *= filters.brightness; // R
pixels[i + 1] *= filters.brightness; // G
pixels[i + 2] *= filters.brightness; // B
}
if (filters.contrast) {
const factor = (259 * (filters.contrast * 255 + 255)) / (255 * (259 - filters.contrast * 255));
pixels[i] = factor * (pixels[i] - 128) + 128;
pixels[i + 1] = factor * (pixels[i + 1] - 128) + 128;
pixels[i + 2] = factor * (pixels[i + 2] - 128) + 128;
}
if (filters.grayscale) {
const gray = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];
pixels[i] = gray;
pixels[i + 1] = gray;
pixels[i + 2] = gray;
}
}
self.postMessage({ id, result: imageData }, [imageData.data.buffer]);
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(null, { imageData, filters });
},
// Data processing
processData: (data, processor) => {
const workerCode = `
self.addEventListener('message', function(e) {
const { id, data, function: processorString } = e.data;
try {
const processor = new Function('return ' + processorString)();
const result = data.map(processor);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
`;
const worker = this.web.create(workerCode);
return worker.execute(processor, data);
}
}
};
/**
* Service Workers for caching and background sync
*/
service = {
// Register service worker
register: async (scriptURL, options = {}) => {
if (!this.support.serviceWorker) {
throw new Error('Service Worker not supported');
}
try {
const registration = await navigator.serviceWorker.register(scriptURL, options);
Logger.info('[WorkerManager] Service Worker registered:', scriptURL);
// Enhanced registration wrapper
return {
registration,
scope: registration.scope,
// Update service worker
update: () => registration.update(),
// Unregister service worker
unregister: () => registration.unregister(),
// Send message to service worker
postMessage: (message) => {
if (registration.active) {
registration.active.postMessage(message);
}
},
// Listen for updates
onUpdate: (callback) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
callback(newWorker);
}
});
});
},
// Check for updates
checkForUpdates: () => {
return registration.update();
}
};
} catch (error) {
Logger.error('[WorkerManager] Service Worker registration failed:', error);
throw error;
}
},
// Get registration
getRegistration: async (scope = '/') => {
if (!this.support.serviceWorker) {
return null;
}
try {
return await navigator.serviceWorker.getRegistration(scope);
} catch (error) {
Logger.error('[WorkerManager] Get registration failed:', error);
return null;
}
},
// Get all registrations
getRegistrations: async () => {
if (!this.support.serviceWorker) {
return [];
}
try {
return await navigator.serviceWorker.getRegistrations();
} catch (error) {
Logger.error('[WorkerManager] Get registrations failed:', error);
return [];
}
}
};
/**
* Shared Workers for cross-tab communication
*/
shared = {
// Create shared worker
create: (script, options = {}) => {
if (!this.support.sharedWorker) {
throw new Error('Shared Worker not supported');
}
try {
const sharedWorker = new SharedWorker(script, options);
const port = sharedWorker.port;
const workerId = this.generateId('shared');
// Start the port
port.start();
const workerWrapper = {
id: workerId,
worker: sharedWorker,
port,
// Send message
send: (message, transfer = null) => {
if (transfer) {
port.postMessage(message, transfer);
} else {
port.postMessage(message);
}
},
// Listen for messages
onMessage: (callback) => {
port.addEventListener('message', (event) => {
callback(event.data, event);
});
},
// Handle errors
onError: (callback) => {
sharedWorker.addEventListener('error', callback);
port.addEventListener('messageerror', callback);
},
// Close connection
close: () => {
port.close();
this.activeWorkers.delete(workerId);
Logger.info(`[WorkerManager] Shared Worker closed: ${workerId}`);
}
};
this.activeWorkers.set(workerId, workerWrapper);
Logger.info(`[WorkerManager] Shared Worker created: ${workerId}`);
return workerWrapper;
} catch (error) {
Logger.error('[WorkerManager] Shared Worker creation failed:', error);
throw error;
}
}
};
/**
* Offscreen Canvas for worker-based rendering
*/
offscreen = {
// Create offscreen canvas worker
create: (canvas, workerScript = null) => {
if (!this.support.offscreenCanvas) {
throw new Error('Offscreen Canvas not supported');
}
try {
const offscreenCanvas = canvas.transferControlToOffscreen();
const defaultWorkerScript = `
self.addEventListener('message', function(e) {
const { canvas, type, data } = e.data;
if (type === 'init') {
self.canvas = canvas;
self.ctx = canvas.getContext('2d');
}
if (type === 'draw' && self.ctx) {
// Basic drawing operations
const { operations } = data;
operations.forEach(op => {
switch (op.type) {
case 'fillRect':
self.ctx.fillRect(...op.args);
break;
case 'strokeRect':
self.ctx.strokeRect(...op.args);
break;
case 'fillText':
self.ctx.fillText(...op.args);
break;
case 'setFillStyle':
self.ctx.fillStyle = op.value;
break;
case 'setStrokeStyle':
self.ctx.strokeStyle = op.value;
break;
}
});
}
});
`;
const script = workerScript || defaultWorkerScript;
const worker = this.web.create(script);
// Initialize worker with canvas
worker.send({
type: 'init',
canvas: offscreenCanvas
}, [offscreenCanvas]);
return {
worker,
// Draw operations
draw: (operations) => {
worker.send({
type: 'draw',
data: { operations }
});
},
// Send custom message
send: (message) => worker.send(message),
// Listen for messages
onMessage: (callback) => worker.onMessage(callback),
// Terminate worker
terminate: () => worker.terminate()
};
} catch (error) {
Logger.error('[WorkerManager] Offscreen Canvas creation failed:', error);
throw error;
}
}
};
// Helper methods
createWorkerBlob(fn) {
const code = `
(function() {
const workerFunction = ${fn.toString()};
if (typeof workerFunction === 'function') {
// If function expects to be called immediately
if (workerFunction.length === 0) {
workerFunction();
}
}
// Standard worker message handling
self.addEventListener('message', function(e) {
if (e.data.type === 'execute' && e.data.function) {
try {
const fn = new Function('return ' + e.data.function)();
const result = fn(e.data.data);
self.postMessage({
id: e.data.id,
result
});
} catch (error) {
self.postMessage({
id: e.data.id,
error: error.message
});
}
}
});
})();
`;
return new Blob([code], { type: 'application/javascript' });
}
generateId(prefix = 'worker') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Terminate all active workers
*/
terminateAll() {
this.activeWorkers.forEach(worker => {
if (worker.terminate) {
worker.terminate();
} else if (worker.close) {
worker.close();
}
});
this.activeWorkers.clear();
Logger.info('[WorkerManager] All workers terminated');
}
/**
* Get worker statistics
*/
getStats() {
const workers = Array.from(this.activeWorkers.values());
return {
total: workers.length,
byType: workers.reduce((acc, worker) => {
const type = worker.id.split('_')[0];
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {}),
support: this.support,
hardwareConcurrency: navigator.hardwareConcurrency || 'unknown'
};
}
/**
* Create common worker utilities
*/
utils = {
// Benchmark function performance
benchmark: async (fn, data, iterations = 1000) => {
const worker = this.web.create(() => {
self.addEventListener('message', (e) => {
const { id, function: fnString, data, iterations } = e.data;
try {
const fn = new Function('return ' + fnString)();
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn(data);
}
const end = performance.now();
const totalTime = end - start;
const avgTime = totalTime / iterations;
self.postMessage({
id,
result: {
totalTime,
avgTime,
iterations,
opsPerSecond: 1000 / avgTime
}
});
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
});
const result = await worker.execute(fn, data, iterations);
worker.terminate();
return result;
},
// Parallel array processing
parallelMap: async (array, fn, chunkSize = null) => {
const chunks = chunkSize || Math.ceil(array.length / (navigator.hardwareConcurrency || 4));
const pool = this.web.createPool(() => {
self.addEventListener('message', (e) => {
const { id, data: { chunk, function: fnString } } = e.data;
try {
const fn = new Function('return ' + fnString)();
const result = chunk.map(fn);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
});
const promises = [];
for (let i = 0; i < array.length; i += chunks) {
const chunk = array.slice(i, i + chunks);
promises.push(pool.execute(fn, chunk));
}
const results = await Promise.all(promises);
pool.terminate();
return results.flat();
}
};
}

View File

@@ -0,0 +1,265 @@
// modules/api-manager/index.js
import { Logger } from '../../core/logger.js';
import { ObserverManager } from './ObserverManager.js';
import { MediaManager } from './MediaManager.js';
import { StorageManager } from './StorageManager.js';
import { DeviceManager } from './DeviceManager.js';
import { AnimationManager } from './AnimationManager.js';
import { WorkerManager } from './WorkerManager.js';
import { PerformanceManager } from './PerformanceManager.js';
import { PermissionManager } from './PermissionManager.js';
import { BiometricAuthManager } from './BiometricAuthManager.js';
/**
* Centralized API Manager for all Web APIs
* Provides unified, simplified access to modern browser APIs
*/
const APIManagerModule = {
name: 'api-manager',
// Module-level init (called by module system)
init(config = {}, state = null) {
Logger.info('[APIManager] Module initialized - All Web APIs available');
this.initializeAPIManagers(config);
this.exposeGlobalAPI();
return this;
},
/**
* Initialize all API managers
*/
initializeAPIManagers(config) {
this.observers = new ObserverManager(config.observers || {});
this.media = new MediaManager(config.media || {});
this.storage = new StorageManager(config.storage || {});
this.device = new DeviceManager(config.device || {});
this.animation = new AnimationManager(config.animation || {});
this.worker = new WorkerManager(config.worker || {});
this.performance = new PerformanceManager(config.performance || {});
this.permissions = new PermissionManager(config.permissions || {});
this.biometric = new BiometricAuthManager(config.biometric || {});
Logger.info('[APIManager] All API managers initialized');
},
/**
* Expose global API for easy access
*/
exposeGlobalAPI() {
// Make API managers globally available
if (typeof window !== 'undefined') {
window.API = {
observers: this.observers,
media: this.media,
storage: this.storage,
device: this.device,
animation: this.animation,
worker: this.worker,
performance: this.performance,
permissions: this.permissions,
biometric: this.biometric
};
Logger.info('[APIManager] Global API exposed at window.API');
}
},
/**
* Get specific API manager
*/
getAPI(name) {
return this[name] || null;
},
/**
* Check if specific Web API is supported
*/
isSupported(apiName) {
const supportMap = {
// Observer APIs
'IntersectionObserver': 'IntersectionObserver' in window,
'ResizeObserver': 'ResizeObserver' in window,
'MutationObserver': 'MutationObserver' in window,
'PerformanceObserver': 'PerformanceObserver' in window,
// Media APIs
'MediaDevices': navigator.mediaDevices !== undefined,
'WebRTC': 'RTCPeerConnection' in window,
'WebAudio': 'AudioContext' in window || 'webkitAudioContext' in window,
'MediaRecorder': 'MediaRecorder' in window,
// Storage APIs
'IndexedDB': 'indexedDB' in window,
'CacheAPI': 'caches' in window,
'WebLocks': 'locks' in navigator,
'BroadcastChannel': 'BroadcastChannel' in window,
// Device APIs
'Geolocation': 'geolocation' in navigator,
'DeviceMotion': 'DeviceMotionEvent' in window,
'DeviceOrientation': 'DeviceOrientationEvent' in window,
'Vibration': 'vibrate' in navigator,
'Battery': 'getBattery' in navigator,
'NetworkInfo': 'connection' in navigator,
// Animation APIs
'WebAnimations': 'animate' in Element.prototype,
'VisualViewport': 'visualViewport' in window,
// Worker APIs
'WebWorkers': 'Worker' in window,
'ServiceWorker': 'serviceWorker' in navigator,
'SharedWorker': 'SharedWorker' in window,
// Performance APIs
'PerformanceAPI': 'performance' in window,
'NavigationTiming': 'getEntriesByType' in performance,
// Permission APIs
'Permissions': 'permissions' in navigator,
'WebAuthn': 'credentials' in navigator && 'create' in navigator.credentials
};
return supportMap[apiName] || false;
},
/**
* Get browser capabilities report
*/
getCapabilities() {
const capabilities = {};
// Check all API support
Object.keys(this.getSupportMap()).forEach(api => {
capabilities[api] = this.isSupported(api);
});
return {
capabilities,
modernBrowser: this.isModernBrowser(),
recommendedAPIs: this.getRecommendedAPIs(),
summary: this.getCapabilitySummary(capabilities)
};
},
/**
* Check if browser is considered modern
*/
isModernBrowser() {
const requiredAPIs = [
'IntersectionObserver',
'ResizeObserver',
'WebAnimations',
'IndexedDB'
];
return requiredAPIs.every(api => this.isSupported(api));
},
/**
* Get recommended APIs for current browser
*/
getRecommendedAPIs() {
const recommendations = [];
if (this.isSupported('IntersectionObserver')) {
recommendations.push({
api: 'IntersectionObserver',
use: 'Lazy loading, scroll triggers, viewport detection',
example: 'API.observers.intersection(elements, callback)'
});
}
if (this.isSupported('WebAnimations')) {
recommendations.push({
api: 'Web Animations',
use: 'High-performance animations with timeline control',
example: 'API.animation.animate(element, keyframes, options)'
});
}
if (this.isSupported('IndexedDB')) {
recommendations.push({
api: 'IndexedDB',
use: 'Client-side database for complex data',
example: 'API.storage.db.set(key, value)'
});
}
if (this.isSupported('MediaDevices')) {
recommendations.push({
api: 'Media Devices',
use: 'Camera, microphone, screen sharing',
example: 'API.media.getUserCamera()'
});
}
return recommendations;
},
/**
* Get capability summary
*/
getCapabilitySummary(capabilities) {
const total = Object.keys(capabilities).length;
const supported = Object.values(capabilities).filter(Boolean).length;
const percentage = Math.round((supported / total) * 100);
return {
total,
supported,
percentage,
grade: this.getGrade(percentage)
};
},
/**
* Get grade based on API support percentage
*/
getGrade(percentage) {
if (percentage >= 90) return 'A+';
if (percentage >= 80) return 'A';
if (percentage >= 70) return 'B';
if (percentage >= 60) return 'C';
return 'D';
},
/**
* Get support map for reference
*/
getSupportMap() {
return {
'IntersectionObserver': 'Viewport intersection detection',
'ResizeObserver': 'Element resize detection',
'MutationObserver': 'DOM change detection',
'PerformanceObserver': 'Performance metrics monitoring',
'MediaDevices': 'Camera and microphone access',
'WebRTC': 'Real-time communication',
'WebAudio': 'Audio processing and synthesis',
'MediaRecorder': 'Audio/video recording',
'IndexedDB': 'Client-side database',
'CacheAPI': 'HTTP cache management',
'WebLocks': 'Resource locking',
'BroadcastChannel': 'Cross-tab communication',
'Geolocation': 'GPS and location services',
'DeviceMotion': 'Accelerometer and gyroscope',
'DeviceOrientation': 'Device orientation',
'Vibration': 'Haptic feedback',
'Battery': 'Battery status',
'NetworkInfo': 'Network connection info',
'WebAnimations': 'High-performance animations',
'VisualViewport': 'Viewport information',
'WebWorkers': 'Background processing',
'ServiceWorker': 'Background sync and caching',
'SharedWorker': 'Shared background processing',
'PerformanceAPI': 'Performance measurement',
'NavigationTiming': 'Navigation timing metrics',
'Permissions': 'Browser permission management',
'WebAuthn': 'Biometric authentication'
};
}
};
// Export für module system
export const init = APIManagerModule.init.bind(APIManagerModule);
export default APIManagerModule;