UNPKG

awake-lock

Version:

A comprehensive wake lock library for preventing device sleep with intelligent fallback strategies and exceptional performance

1,396 lines (1,386 loc) 56 kB
class WakeLockError extends Error { constructor(message, code, strategy, originalError) { super(message); this.name = 'WakeLockError'; this.code = code; this.strategy = strategy; this.originalError = originalError; } } var WakeLockErrorCode; (function (WakeLockErrorCode) { WakeLockErrorCode["NOT_SUPPORTED"] = "NOT_SUPPORTED"; WakeLockErrorCode["PERMISSION_DENIED"] = "PERMISSION_DENIED"; WakeLockErrorCode["INVALID_STATE"] = "INVALID_STATE"; WakeLockErrorCode["TIMEOUT"] = "TIMEOUT"; WakeLockErrorCode["BATTERY_LOW"] = "BATTERY_LOW"; WakeLockErrorCode["STRATEGY_FAILED"] = "STRATEGY_FAILED"; WakeLockErrorCode["UNKNOWN"] = "UNKNOWN"; })(WakeLockErrorCode || (WakeLockErrorCode = {})); class EventEmitter { constructor() { this.listeners = new Map(); this.onceListeners = new Map(); } addEventListener(type, listener, options) { const once = typeof options === 'object' ? options.once : false; const listenerMap = once ? this.onceListeners : this.listeners; if (!listenerMap.has(type)) { listenerMap.set(type, new Set()); } listenerMap.get(type).add(listener); } removeEventListener(type, listener) { this.listeners.get(type)?.delete(listener); this.onceListeners.get(type)?.delete(listener); } dispatchEvent(type, event) { const regularListeners = this.listeners.get(type); const onceListeners = this.onceListeners.get(type); if (regularListeners) { for (const listener of regularListeners) { try { listener(event); } catch (error) { console.error(`Error in event listener for '${String(type)}':`, error); } } } if (onceListeners) { for (const listener of onceListeners) { try { listener(event); } catch (error) { console.error(`Error in event listener for '${String(type)}':`, error); } } this.onceListeners.delete(type); } return true; } on(type, listener) { this.addEventListener(type, listener); } off(type, listener) { this.removeEventListener(type, listener); } once(type, listener) { this.addEventListener(type, listener, { once: true }); } emit(type, event) { return this.dispatchEvent(type, event); } listenerCount(type) { const regular = this.listeners.get(type)?.size ?? 0; const once = this.onceListeners.get(type)?.size ?? 0; return regular + once; } hasListeners(type) { return this.listenerCount(type) > 0; } removeAllListeners(type) { if (type !== undefined) { this.listeners.delete(type); this.onceListeners.delete(type); } else { this.listeners.clear(); this.onceListeners.clear(); } } eventNames() { const names = new Set(); for (const name of this.listeners.keys()) { names.add(name); } for (const name of this.onceListeners.keys()) { names.add(name); } return Array.from(names); } } function isSSR() { return typeof window === 'undefined' || typeof document === 'undefined'; } function isWakeLockSupported() { if (isSSR()) return false; return 'wakeLock' in navigator && typeof navigator.wakeLock?.request === 'function'; } function isPermissionsSupported() { if (isSSR()) return false; return 'permissions' in navigator && typeof navigator.permissions?.query === 'function'; } function isBatterySupported() { if (isSSR()) return false; const nav = navigator; return typeof nav.getBattery === 'function'; } async function getBattery() { if (!isBatterySupported()) return null; try { const nav = navigator; return await nav.getBattery(); } catch { return null; } } async function checkPermission(name) { if (!isPermissionsSupported()) return null; try { const result = await navigator.permissions.query({ name }); return result.state; } catch { return null; } } function createTimeout(ms, signal) { return new Promise((_, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Operation timed out after ${ms}ms`)); }, ms); signal?.addEventListener('abort', () => { clearTimeout(timeoutId); reject(new Error('Operation was aborted')); }); }); } function withTimeout(promise, ms, signal) { return Promise.race([promise, createTimeout(ms, signal)]); } function throttle(fn, limit) { let inThrottle; return (...args) => { if (!inThrottle) { fn(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } function isLowPowerMode() { if (isSSR()) return false; try { if ('connection' in navigator) { const connection = navigator.connection; if (connection?.saveData) return true; } if ('deviceMemory' in navigator) { const memory = navigator.deviceMemory; if (memory && memory <= 2) return true; } if (navigator.hardwareConcurrency <= 2) return true; return false; } catch { return false; } } function generateUniqueId() { return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } function isDocumentHidden() { if (isSSR()) return false; return document.visibilityState === 'hidden'; } function getPerformanceNow() { if (isSSR()) return Date.now(); return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); } function getMemoryUsage() { if (isSSR()) return 0; try { if ('memory' in performance) { const memory = performance.memory; return memory?.usedJSHeapSize ?? 0; } return 0; } catch { return 0; } } class PermissionManager { constructor() { this.permissionCache = new Map(); this.cacheExpiryTime = 5 * 60 * 1000; this.cacheTimestamps = new Map(); } async checkWakeLockPermission(type, passive = false) { if (isSSR()) return null; const cacheKey = `wake-lock-${type}`; const cached = this.getCachedPermission(cacheKey); if (cached) return cached; try { const wakeLockPermission = await this.checkSpecificPermission('wake-lock'); if (wakeLockPermission) { this.setCachedPermission(cacheKey, wakeLockPermission); return wakeLockPermission; } if (type === 'screen') { const screenPermission = await this.checkSpecificPermission('screen-wake-lock'); if (screenPermission) { this.setCachedPermission(cacheKey, screenPermission); return screenPermission; } } if (passive) { this.setCachedPermission(cacheKey, 'granted'); return 'granted'; } return null; } catch (error) { if (passive) { return null; } throw new WakeLockError(`Failed to check ${type} wake lock permission`, WakeLockErrorCode.PERMISSION_DENIED, 'permission-manager', error instanceof Error ? error : undefined); } } async requestWakeLockPermission(type) { if (isSSR()) { throw new WakeLockError('Wake lock not supported in server-side rendering', WakeLockErrorCode.NOT_SUPPORTED, 'permission-manager'); } const currentPermission = await this.checkWakeLockPermission(type, true); if (currentPermission === 'granted') { return 'granted'; } try { if ('wakeLock' in navigator && navigator.wakeLock) { const wakeLock = await navigator.wakeLock.request(type); await wakeLock.release(); const cacheKey = `wake-lock-${type}`; this.setCachedPermission(cacheKey, 'granted'); return 'granted'; } } catch (error) { if (error instanceof Error) { if (error.name === 'NotAllowedError') { const cacheKey = `wake-lock-${type}`; this.setCachedPermission(cacheKey, 'denied'); return 'denied'; } throw new WakeLockError(`Failed to request ${type} wake lock permission: ${error.message}`, WakeLockErrorCode.PERMISSION_DENIED, 'permission-manager', error); } } throw new WakeLockError(`Wake lock not supported for type: ${type}`, WakeLockErrorCode.NOT_SUPPORTED, 'permission-manager'); } async canRequestWithoutPrompt(type) { if (isSSR()) return false; try { const permission = await this.checkWakeLockPermission(type, true); if (permission === 'granted' || permission === 'denied') { return true; } return false; } catch { return false; } } isPassiveModeRecommended() { if (isSSR()) return true; try { if (window !== window.top) return true; if (document.visibilityState === 'hidden') return true; if (!this.hasRecentUserInteraction()) return true; if (this.isMobileDevice()) return true; return false; } catch { return true; } } clearPermissionCache() { this.permissionCache.clear(); this.cacheTimestamps.clear(); } async checkSpecificPermission(name) { if (!isPermissionsSupported()) return null; try { return await checkPermission(name); } catch { return null; } } getCachedPermission(key) { const timestamp = this.cacheTimestamps.get(key); if (!timestamp || Date.now() - timestamp > this.cacheExpiryTime) { this.permissionCache.delete(key); this.cacheTimestamps.delete(key); return null; } return this.permissionCache.get(key) ?? null; } setCachedPermission(key, permission) { this.permissionCache.set(key, permission); this.cacheTimestamps.set(key, Date.now()); } hasRecentUserInteraction() { return document.hasFocus() && Date.now() - this.getLastUserActivity() < 30000; } getLastUserActivity() { return Date.now() - 1000; } isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } } class PerformanceMonitor extends EventEmitter { constructor(options = {}) { super(); this.monitoringTimer = null; this.battery = null; this.lastMetrics = null; this.isMonitoring = false; this.throttledEmitPerformance = throttle((metrics) => this.emit('performance', metrics), 1000); this.enabled = options.enabled ?? true; this.monitoringInterval = options.monitoringInterval ?? 5000; this.batteryThreshold = options.batteryThreshold ?? 0.2; this.performanceThreshold = options.performanceThreshold ?? 80; } async start() { if (!this.enabled || this.isMonitoring || isSSR()) return; this.isMonitoring = true; try { await this.initializeBatteryMonitoring(); this.startPerformanceMonitoring(); const initialMetrics = await this.collectMetrics(); this.lastMetrics = initialMetrics; this.throttledEmitPerformance(initialMetrics); } catch (error) { console.warn('Failed to start performance monitoring:', error); this.isMonitoring = false; } } stop() { if (!this.isMonitoring) return; this.isMonitoring = false; if (this.monitoringTimer) { clearInterval(this.monitoringTimer); this.monitoringTimer = null; } if (this.battery) { this.battery.onlevelchange = null; this.battery.onchargingchange = null; } } async getCurrentMetrics() { return await this.collectMetrics(); } getLastMetrics() { return this.lastMetrics; } isLowBattery() { if (!this.battery) return false; return this.battery.level <= this.batteryThreshold; } isHighCpuUsage() { if (!this.lastMetrics) return false; return this.lastMetrics.cpuUsage >= this.performanceThreshold; } shouldOptimizePerformance() { return this.isLowBattery() || this.isHighCpuUsage() || isLowPowerMode(); } async initializeBatteryMonitoring() { try { this.battery = await getBattery(); if (!this.battery) return; this.battery.onlevelchange = () => { if (this.battery) { this.emit('battery-change', { level: this.battery.level, charging: this.battery.charging, }); if (this.isLowBattery()) { this.handleLowBattery(); } } }; this.battery.onchargingchange = () => { if (this.battery) { this.emit('battery-change', { level: this.battery.level, charging: this.battery.charging, }); } }; this.emit('battery-change', { level: this.battery.level, charging: this.battery.charging, }); } catch (error) { console.warn('Failed to initialize battery monitoring:', error); } } startPerformanceMonitoring() { this.monitoringTimer = setInterval(async () => { if (!this.isMonitoring) return; try { const metrics = await this.collectMetrics(); this.lastMetrics = metrics; this.throttledEmitPerformance(metrics); if (this.shouldOptimizePerformance()) { this.handlePerformanceOptimization(); } } catch (error) { console.warn('Error collecting performance metrics:', error); } }, this.monitoringInterval); } async collectMetrics() { const timestamp = getPerformanceNow(); const cpuUsage = await this.estimateCpuUsage(); const memoryUsage = getMemoryUsage(); const batteryDrain = this.estimateBatteryDrain(); return { cpuUsage, memoryUsage, batteryDrain, timestamp, }; } async estimateCpuUsage() { if (isSSR()) return 0; try { const start = getPerformanceNow(); const iterations = 100000; let result = 0; for (let i = 0; i < iterations; i++) { result += Math.random() * Math.sin(i); } const end = getPerformanceNow(); const duration = end - start; const normalizedUsage = Math.min(100, Math.max(0, (duration / 10) * 100)); globalThis.__performanceTestResult = result; return normalizedUsage; } catch { return 0; } } estimateBatteryDrain() { if (!this.battery || !this.lastMetrics) return 0; try { const timeDiff = Date.now() - this.lastMetrics.timestamp; if (timeDiff === 0) return 0; const baselineDrain = 0.01; const wakeLockMultiplier = 1.5; return baselineDrain * wakeLockMultiplier; } catch { return 0; } } handleLowBattery() { console.warn('Low battery detected, consider optimizing wake lock usage'); } handlePerformanceOptimization() { console.info('Performance optimization recommended'); if (this.lastMetrics) { this.throttledEmitPerformance(this.lastMetrics); } } getOptimizationRecommendations() { const recommendations = []; if (this.isLowBattery()) { recommendations.push('Consider releasing wake lock due to low battery'); } if (this.isHighCpuUsage()) { recommendations.push('High CPU usage detected, consider using less aggressive wake lock strategy'); } if (isLowPowerMode()) { recommendations.push('Device is in low power mode, consider passive wake lock mode'); } if (this.lastMetrics && this.lastMetrics.memoryUsage > 50 * 1024 * 1024) { recommendations.push('High memory usage detected, consider optimizing wake lock strategy'); } return recommendations; } async measureStrategy(strategyName, operation) { const start = getPerformanceNow(); const cpuBefore = await this.estimateCpuUsage(); const memoryBefore = getMemoryUsage(); await operation(); const end = getPerformanceNow(); const cpuAfter = await this.estimateCpuUsage(); const memoryAfter = getMemoryUsage(); const measurement = { duration: end - start, cpuBefore, cpuAfter, memoryBefore, memoryAfter, }; console.debug(`Strategy ${strategyName} performance:`, measurement); return measurement; } } class ScreenWakeLockStrategy { constructor() { this.name = 'screen-wake-lock'; this.priority = 1; this.activeSentinels = new Set(); } isSupported() { return !isSSR() && isWakeLockSupported(); } async request(type, options = {}) { if (!this.isSupported()) { throw new WakeLockError('Screen Wake Lock API is not supported', WakeLockErrorCode.NOT_SUPPORTED, this.name); } if (type === 'system') { throw new WakeLockError('System wake locks are not yet supported by the Screen Wake Lock API', WakeLockErrorCode.NOT_SUPPORTED, this.name); } try { const requestPromise = navigator.wakeLock.request(type); const timeout = options.timeout ?? 10000; const nativeSentinel = timeout > 0 ? await withTimeout(requestPromise, timeout, options.signal) : await requestPromise; const wrappedSentinel = this.createWrappedSentinel(nativeSentinel); this.activeSentinels.add(wrappedSentinel); return wrappedSentinel; } catch (error) { if (error instanceof Error) { let errorCode = WakeLockErrorCode.UNKNOWN; switch (error.name) { case 'NotAllowedError': errorCode = WakeLockErrorCode.PERMISSION_DENIED; break; case 'NotSupportedError': errorCode = WakeLockErrorCode.NOT_SUPPORTED; break; case 'AbortError': errorCode = WakeLockErrorCode.TIMEOUT; break; default: if (error.message.includes('timeout')) { errorCode = WakeLockErrorCode.TIMEOUT; } } throw new WakeLockError(`Screen Wake Lock request failed: ${error.message}`, errorCode, this.name, error); } throw new WakeLockError('Unknown error occurred while requesting screen wake lock', WakeLockErrorCode.UNKNOWN, this.name); } } async release() { const releasePromises = Array.from(this.activeSentinels).map(sentinel => { return sentinel.release().catch(error => { console.warn('Failed to release wake lock sentinel:', error); }); }); await Promise.allSettled(releasePromises); this.activeSentinels.clear(); } getActiveSentinels() { return this.activeSentinels; } createWrappedSentinel(nativeSentinel) { const wrappedSentinel = { type: nativeSentinel.type, get released() { return this._released || nativeSentinel.released; }, _strategy: this, _releaseListeners: new Set(), _released: false, async release() { if (this._released) return; try { await nativeSentinel.release(); } finally { this._released = true; this._strategy.activeSentinels.delete(this); for (const listener of this._releaseListeners) { try { listener(); } catch (error) { console.error('Error in release listener:', error); } } this._releaseListeners.clear(); } }, addEventListener(type, listener) { if (type === 'release') { this._releaseListeners.add(listener); nativeSentinel.addEventListener('release', listener); } }, removeEventListener(type, listener) { if (type === 'release') { this._releaseListeners.delete(listener); nativeSentinel.removeEventListener('release', listener); } }, }; nativeSentinel.addEventListener('release', () => { if (!wrappedSentinel._released) { wrappedSentinel._released = true; this.activeSentinels.delete(wrappedSentinel); } }); return wrappedSentinel; } } class VideoElementStrategy { constructor() { this.name = 'video-element'; this.priority = 2; this.activeElements = new Map(); this.mediaSource = null; } isSupported() { if (isSSR()) return false; try { const video = document.createElement('video'); const canPlayType = video.canPlayType('video/mp4'); const hasMediaSource = 'MediaSource' in window; return canPlayType !== '' && hasMediaSource; } catch { return false; } } async request(type, options = {}) { if (!this.isSupported()) { throw new WakeLockError('Video element strategy is not supported', WakeLockErrorCode.NOT_SUPPORTED, this.name); } if (type === 'system') { throw new WakeLockError('System wake locks are not supported by video element strategy', WakeLockErrorCode.NOT_SUPPORTED, this.name); } try { const timeout = options.timeout ?? 15000; const setupPromise = this.setupVideoElement(); const { video, sentinelId } = timeout > 0 ? await withTimeout(setupPromise, timeout, options.signal) : await setupPromise; const sentinel = this.createSentinel(sentinelId, type); this.activeElements.set(sentinelId, { video, sentinel }); return sentinel; } catch (error) { throw new WakeLockError(`Video element wake lock failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error && error.message.includes('timeout') ? WakeLockErrorCode.TIMEOUT : WakeLockErrorCode.STRATEGY_FAILED, this.name, error instanceof Error ? error : undefined); } } async release() { const releasePromises = Array.from(this.activeElements.entries()).map(([id, { video, sentinel }]) => { return this.releaseSentinel(id, video, sentinel).catch(error => { console.warn(`Failed to release video element ${id}:`, error); }); }); await Promise.allSettled(releasePromises); this.activeElements.clear(); } async setupVideoElement() { const video = document.createElement('video'); const sentinelId = generateUniqueId(); video.style.position = 'fixed'; video.style.top = '-1000px'; video.style.left = '-1000px'; video.style.width = '1px'; video.style.height = '1px'; video.style.opacity = '0.01'; video.style.pointerEvents = 'none'; video.style.zIndex = '-1000'; video.muted = true; video.loop = true; video.playsInline = true; video.controls = false; video.setAttribute('webkit-playsinline', 'true'); video.setAttribute('playsinline', 'true'); const mediaSource = await this.createMediaSource(); video.src = URL.createObjectURL(mediaSource); document.body.appendChild(video); await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error('Video setup timeout')); }, 10000); const cleanup = () => { clearTimeout(timeoutId); video.removeEventListener('canplay', onCanPlay); video.removeEventListener('error', onError); }; const onCanPlay = () => { cleanup(); video .play() .then(() => resolve()) .catch(reject); }; const onError = () => { cleanup(); reject(new Error('Video failed to load')); }; video.addEventListener('canplay', onCanPlay); video.addEventListener('error', onError); video.load(); }); return { video, sentinelId }; } async createMediaSource() { if (this.mediaSource && this.mediaSource.readyState === 'open') { return this.mediaSource; } const mediaSource = new MediaSource(); return new Promise((resolve, reject) => { const onSourceOpen = () => { try { const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E"'); const videoData = this.createMinimalVideoData(); sourceBuffer.appendBuffer(videoData); sourceBuffer.addEventListener('updateend', () => { try { mediaSource.endOfStream(); resolve(mediaSource); } catch (error) { reject(error); } }); } catch (error) { reject(error); } }; mediaSource.addEventListener('sourceopen', onSourceOpen); }); } createMinimalVideoData() { const mp4Data = new Uint8Array([ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, 0x69, 0x73, 0x6f, 0x6d, 0x69, 0x73, 0x6f, 0x32, 0x61, 0x76, 0x63, 0x31, 0x6d, 0x70, 0x34, 0x31, ]); return mp4Data.buffer; } createSentinel(sentinelId, type) { const sentinel = { type, get released() { return this._released; }, _strategy: this, _releaseListeners: new Set(), _released: false, async release() { if (this._released) return; const element = this._strategy.activeElements.get(sentinelId); if (element) { await this._strategy.releaseSentinel(sentinelId, element.video, this); } }, addEventListener(type, listener) { if (type === 'release') { this._releaseListeners.add(listener); } }, removeEventListener(type, listener) { if (type === 'release') { this._releaseListeners.delete(listener); } }, }; return sentinel; } async releaseSentinel(sentinelId, video, sentinel) { if (sentinel._released) return; try { video.pause(); video.src = ''; video.load(); if (video.parentNode) { video.parentNode.removeChild(video); } if (video.src.startsWith('blob:')) { URL.revokeObjectURL(video.src); } } catch (error) { console.warn('Error cleaning up video element:', error); } finally { sentinel._released = true; this.activeElements.delete(sentinelId); for (const listener of sentinel._releaseListeners) { try { listener(); } catch (error) { console.error('Error in release listener:', error); } } sentinel._releaseListeners.clear(); } } } class AudioContextStrategy { constructor() { this.name = 'audio-context'; this.priority = 3; this.activeContexts = new Map(); } isSupported() { if (isSSR()) return false; try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; return typeof AudioContextClass === 'function'; } catch { return false; } } async request(type, options = {}) { if (!this.isSupported()) { throw new WakeLockError('Audio context strategy is not supported', WakeLockErrorCode.NOT_SUPPORTED, this.name); } if (type === 'system') { throw new WakeLockError('System wake locks are not supported by audio context strategy', WakeLockErrorCode.NOT_SUPPORTED, this.name); } try { const timeout = options.timeout ?? 10000; const setupPromise = this.setupAudioContext(); const { context, oscillator, gainNode, sentinelId } = timeout > 0 ? await withTimeout(setupPromise, timeout, options.signal) : await setupPromise; const sentinel = this.createSentinel(sentinelId, type); this.activeContexts.set(sentinelId, { context, oscillator, gainNode, sentinel }); return sentinel; } catch (error) { throw new WakeLockError(`Audio context wake lock failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error && error.message.includes('timeout') ? WakeLockErrorCode.TIMEOUT : WakeLockErrorCode.STRATEGY_FAILED, this.name, error instanceof Error ? error : undefined); } } async release() { const releasePromises = Array.from(this.activeContexts.entries()).map(([id, audio]) => { return this.releaseSentinel(id, audio).catch(error => { console.warn(`Failed to release audio context ${id}:`, error); }); }); await Promise.allSettled(releasePromises); this.activeContexts.clear(); } async setupAudioContext() { const AudioContextClass = window.AudioContext || window.webkitAudioContext; const context = new AudioContextClass(); const sentinelId = generateUniqueId(); try { if (context.state === 'suspended') { await context.resume(); } const oscillator = context.createOscillator(); const gainNode = context.createGain(); oscillator.frequency.setValueAtTime(20000, context.currentTime); gainNode.gain.setValueAtTime(0.0001, context.currentTime); oscillator.connect(gainNode); gainNode.connect(context.destination); oscillator.start(); await new Promise((resolve, reject) => { const startTime = Date.now(); const checkRunning = () => { if (context.state === 'running') { resolve(); } else if (Date.now() - startTime > 5000) { reject(new Error('Audio context failed to start')); } else { setTimeout(checkRunning, 100); } }; checkRunning(); }); return { context, oscillator, gainNode, sentinelId }; } catch (error) { try { await context.close(); } catch { } throw error; } } createSentinel(sentinelId, type) { const sentinel = { type, get released() { return this._released; }, _strategy: this, _releaseListeners: new Set(), _released: false, async release() { if (this._released) return; const audio = this._strategy.activeContexts.get(sentinelId); if (audio) { await this._strategy.releaseSentinel(sentinelId, audio); } }, addEventListener(type, listener) { if (type === 'release') { this._releaseListeners.add(listener); } }, removeEventListener(type, listener) { if (type === 'release') { this._releaseListeners.delete(listener); } }, }; return sentinel; } async releaseSentinel(sentinelId, audio) { if (audio.sentinel._released) return; try { if (audio.oscillator) { try { audio.oscillator.stop(); } catch { } } if (audio.gainNode) { try { audio.gainNode.disconnect(); } catch { } } if (audio.context && audio.context.state !== 'closed') { await audio.context.close(); } } catch (error) { console.warn('Error cleaning up audio context:', error); } finally { audio.sentinel._released = true; this.activeContexts.delete(sentinelId); for (const listener of audio.sentinel._releaseListeners) { try { listener(); } catch (error) { console.error('Error in release listener:', error); } } audio.sentinel._releaseListeners.clear(); } } getActiveContextsCount() { return this.activeContexts.size; } async suspendAll() { const suspendPromises = Array.from(this.activeContexts.values()).map(async (audio) => { if (audio.context.state === 'running') { try { await audio.context.suspend(); } catch (error) { console.warn('Failed to suspend audio context:', error); } } }); await Promise.allSettled(suspendPromises); } async resumeAll() { const resumePromises = Array.from(this.activeContexts.values()).map(async (audio) => { if (audio.context.state === 'suspended') { try { await audio.context.resume(); } catch (error) { console.warn('Failed to resume audio context:', error); } } }); await Promise.allSettled(resumePromises); } } class TimerStrategy { constructor() { this.name = 'timer'; this.priority = 4; this.activeTimers = new Map(); this.DEFAULT_INTERVAL = 1000; this.AGGRESSIVE_INTERVAL = 500; this.WORKER_SCRIPT = ` let intervalId; self.onmessage = function(e) { if (e.data.action === 'start') { intervalId = setInterval(() => { self.postMessage({ type: 'tick', timestamp: Date.now() }); }, e.data.interval || 1000); } else if (e.data.action === 'stop') { if (intervalId) { clearInterval(intervalId); intervalId = null; } self.close(); } }; `; } isSupported() { return !isSSR(); } async request(type, _options = {}) { if (!this.isSupported()) { throw new WakeLockError('Timer strategy is not supported in SSR environment', WakeLockErrorCode.NOT_SUPPORTED, this.name); } const sentinelId = generateUniqueId(); const sentinel = this.createSentinel(sentinelId, type); try { const useWorker = this.shouldUseWorker(); const interval = this.determineInterval(); if (useWorker) { await this.setupWorkerTimer(sentinelId, interval, sentinel); } else { this.setupMainThreadTimer(sentinelId, interval, sentinel); } this.setupVisibilityHandling(sentinelId); return sentinel; } catch (error) { throw new WakeLockError(`Timer strategy failed to start: ${error instanceof Error ? error.message : 'Unknown error'}`, WakeLockErrorCode.STRATEGY_FAILED, this.name, error instanceof Error ? error : undefined); } } async release() { const releasePromises = Array.from(this.activeTimers.entries()).map(([id, timer]) => { return this.releaseSentinel(id, timer).catch(error => { console.warn(`Failed to release timer ${id}:`, error); }); }); await Promise.allSettled(releasePromises); this.activeTimers.clear(); } shouldUseWorker() { try { return (typeof Worker !== 'undefined' && typeof window !== 'undefined' && typeof window.importScripts === 'undefined'); } catch { return false; } } determineInterval() { const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); return isMobile ? this.AGGRESSIVE_INTERVAL : this.DEFAULT_INTERVAL; } async setupWorkerTimer(sentinelId, interval, sentinel) { try { const blob = new Blob([this.WORKER_SCRIPT], { type: 'application/javascript' }); const workerScript = URL.createObjectURL(blob); const worker = new Worker(workerScript); worker.onmessage = (e) => { if (e.data.type === 'tick') { const now = Date.now(); globalThis.__wakeLockTimestamp = now; } }; worker.onerror = (error) => { console.warn('Wake lock worker error:', error); this.setupMainThreadTimer(sentinelId, interval, sentinel); }; worker.postMessage({ action: 'start', interval }); const timerInfo = this.activeTimers.get(sentinelId); if (timerInfo) { timerInfo.worker = worker; timerInfo.workerScript = workerScript; } } catch (error) { console.warn('Failed to setup worker timer, falling back to main thread:', error); this.setupMainThreadTimer(sentinelId, interval, sentinel); } } setupMainThreadTimer(sentinelId, interval, sentinel) { const intervalId = setInterval(() => { if (sentinel._released) { clearInterval(intervalId); return; } const now = Date.now(); if (!isDocumentHidden()) { try { const el = document.createElement('div'); el.style.position = 'absolute'; el.style.top = '-1px'; el.style.left = '-1px'; el.style.width = '1px'; el.style.height = '1px'; el.style.opacity = '0'; document.body.appendChild(el); document.body.removeChild(el); } catch { } } globalThis.__wakeLockTimestamp = now; }, interval); const timerInfo = this.activeTimers.get(sentinelId); if (timerInfo) { timerInfo.intervalId = intervalId; } } setupVisibilityHandling(sentinelId) { const handleVisibilityChange = () => { const timer = this.activeTimers.get(sentinelId); if (!timer || timer.sentinel._released) return; if (isDocumentHidden()) { this.adjustTimerAggression(sentinelId, true); } else { this.adjustTimerAggression(sentinelId, false); } }; document.addEventListener('visibilitychange', handleVisibilityChange); const timer = this.activeTimers.get(sentinelId); if (timer) { timer.cleanupVisibility = () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; } } adjustTimerAggression(sentinelId, aggressive) { const timer = this.activeTimers.get(sentinelId); if (!timer || timer.sentinel._released) return; const newInterval = aggressive ? this.AGGRESSIVE_INTERVAL : this.DEFAULT_INTERVAL; if (timer.worker) { timer.worker.postMessage({ action: 'stop' }); timer.worker.postMessage({ action: 'start', interval: newInterval }); } else { clearInterval(timer.intervalId); this.setupMainThreadTimer(sentinelId, newInterval, timer.sentinel); } } createSentinel(sentinelId, type) { const sentinel = { type, get released() { return this._released; }, _strategy: this, _releaseListeners: new Set(), _released: false, async release() { if (this._released) return; const timer = this._strategy.activeTimers.get(sentinelId); if (timer) { await this._strategy.releaseSentinel(sentinelId, timer); } }, addEventListener(type, listener) { if (type === 'release') { this._releaseListeners.add(listener); } }, removeEventListener(type, listener) { if (type === 'release') { this._releaseListeners.delete(listener); } }, }; this.activeTimers.set(sentinelId, { intervalId: setInterval(() => { }, 0), sentinel, }); return sentinel; } async releaseSentinel(sentinelId, timer) { if (timer.sentinel._released) return; try { if (timer.worker) { timer.worker.postMessage({ action: 'stop' }); timer.worker.terminate(); delete timer.worker; } if (timer.intervalId) { clearInterval(timer.intervalId); } if (timer.workerScript) { URL.revokeObjectURL(timer.workerScript); } if (timer.cleanupVisibility) { timer.cleanupVisibility(); } } catch (error) { console.warn('Error cleaning up timer:', error); } finally { timer.sentinel._released = true; this.activeTimers.delete(sentinelId); for (const listener of timer.sentinel._releaseListeners) { try { listener(); } catch (error) { console.error('Error in release listener:', error); } } timer.sentinel._releaseListeners.clear(); } } getActiveTimersCount() { return this.activeTimers.size; } } class WakeLock extends EventEmitter { constructor(options = {}) { super(); this.activeSentinel = null; this.currentStrategy = null; this.isReleasing = false; this.maxRetries = 3; this.options = { strategies: options.strategies || this.getDefaultStrategies(), debug: options.debug ?? false, batteryOptimization: options.batteryOptimization ?? true, performanceMonitoring: options.performanceMonitoring ?? true, passive: options.passive ?? false, }; this.permissionManager = new PermissionManager(); this.performanceMonitor = new PerformanceMonitor({ enabled: this.options.performanceMonitoring, batteryThreshold: 0.15, performanceThreshold: 85, }); this.strategies = this.options.strategies .sort((a, b) => a.priority - b.priority) .filter(strategy => strategy.isSupported()); this.initializeEventListeners(); this.initializeVisibilityHandling(); } async request(type = 'screen', options = {}) { if (isSSR()) { throw new WakeLockError('Wake lock is not supported in server-side rendering', WakeLockErrorCode.NOT_SUPPORTED); } if (this.activeSentinel && !this.activeSentinel.released) { if (this.options.debug) { console.info('Wake lock already active, returning existing sentinel'); } return this.activeSentinel; } const requestOptions = { passive: options.passive ?? this.options.passive, timeout: options.timeout ?? 30000, retryAttempts: options.retryAttempts ?? this.maxRetries, ...options, }; try { if (requestOptions.passive) { const canRequest = await this.permissionManager.canRequestWithoutPrompt(type); if (!canRequest && this.permissionManager.isPassiveModeRecommended()) { throw new WakeLockError('Wake lock would require user prompt, but passive mode is enabled', WakeLockErrorCode.PERMISSION_DENIED); } } if (this.options.performanceMonitoring) { await this.performanceMonitor.start(); } const sentinel = await this.tryStrategies(type, requestOptions); this.activeSentinel = sentinel; this.setupSentinelListeners(sentinel); this.emit('enabled', { type, strategy: this.currentStrategy?.name ?? 'unknown', }); if (this.options.debug) { console.info(`Wake lock acquired using ${this.currentStrategy?.name} strategy`); } return sentinel; } catch (error) { this.emit('error', { error: error instanceof WakeLockError ? error : new WakeLockError(error instanceof Error ? error.message : 'Unknown error', WakeLockErrorCode.UNKNOWN), }); throw error; } } async release() { if (!this.activeSentinel || this.activeSentinel.released || this.isReleasing) { return; } this.isReleasing = true; try { await this.activeSentinel.release(); this.emit('disabled', { type: this.activeSentinel.type, reason: 'manual-release', }); if (this.options.debug) { console.info('Wake lock released'); } } catch (error) { this.emit('error', { error: error instanceof WakeLockError ? error : new WakeLockError(error instanceof Error ? error.message : 'Failed to release wake lock', WakeLockErrorCode.UNKNOWN), }); } finally { this.activeSentinel = null; this.currentStrategy = null; this.isReleasing =