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