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) • 55.9 kB
JavaScript
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 =