import { useEffect, useRef } from 'react'; /** * Extended Navigator type that includes the Screen Wake Lock API * @see https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API */ type WakeLockCapableNavigator = Navigator & { wakeLock?: { request: (type: WakeLockType) => Promise; }; }; /** * Checks if we're in a client-side environment (browser) * Prevents SSR issues by verifying window, navigator, and document exist */ const isClientEnvironment = typeof window !== 'undefined' && typeof navigator !== 'undefined' && typeof document !== 'undefined'; const getNavigator = () => navigator as WakeLockCapableNavigator; /** * Determines if the browser supports the Screen Wake Lock API * Checking outside component scope for better performance */ const supportsWakeLock = isClientEnvironment && 'wakeLock' in navigator; /** * Enable/disable debug logging for wake lock operations * Set to true during development to see wake lock lifecycle events */ const DEBUG_WAKE_LOCK = false; /** * Custom hook to prevent screen from sleeping during critical operations * Uses the Screen Wake Lock API to keep the device screen active * * @param shouldHold - Boolean flag indicating whether to acquire/hold the wake lock * @returns void - This hook manages wake lock state internally * * @example * ```tsx * const isGeneratingResponse = useRecoilValue(anySubmittingSelector); * useWakeLock(isGeneratingResponse); * ``` * * @remarks * - Automatically handles page visibility changes (reacquires lock when tab becomes visible) * - Properly cleans up lock on unmount or when shouldHold becomes false * - Gracefully degrades on browsers without Wake Lock API support * - Wake locks are automatically released when user switches tabs */ export const useWakeLock = (shouldHold: boolean) => { const wakeLockRef = useRef(null); useEffect(() => { if (!supportsWakeLock) { if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] API not supported in this browser'); } return; } /** * Flag to prevent operations after effect cleanup * Essential for avoiding race conditions when: * - Component unmounts while lock is being acquired * - shouldHold changes while async operations are in flight * - Multiple visibility change events fire in quick succession */ let cancelled = false; const { wakeLock } = getNavigator(); if (!wakeLock) { return; } /** * Releases the currently held wake lock * Called when: shouldHold becomes false, component unmounts, or before acquiring new lock */ const releaseLock = async () => { if (!wakeLockRef.current) { return; } try { await wakeLockRef.current.release(); if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] Lock released successfully'); } } catch (error) { console.warn('[WakeLock] release failed', error); } finally { wakeLockRef.current = null; } }; /** * Requests a new wake lock from the browser * Checks multiple conditions before requesting to avoid unnecessary API calls: * - shouldHold must be true (user wants lock) * - cancelled must be false (effect still active) * - document must be visible (API requirement - locks only work in visible tabs) * - no existing lock (prevent duplicate locks) */ const requestLock = async () => { if ( !shouldHold || cancelled || document.visibilityState !== 'visible' || wakeLockRef.current ) { return; } try { const sentinel = await wakeLock.request('screen'); wakeLockRef.current = sentinel; if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] Lock acquired successfully'); } /** * CRITICAL: Recursive re-acquire logic for automatic lock restoration * * The browser automatically releases wake locks when: * - User switches to a different tab * - User minimizes the browser window * - Device goes to sleep * - User navigates to a different page * * This handler automatically re-acquires the lock when: * 1. The lock is released by the browser * 2. The effect is still active (not cancelled) * 3. The component still wants to hold the lock (shouldHold is true) * 4. The tab is visible again (document.visibilityState === 'visible') * * This ensures users don't need to manually restart their work after * switching tabs during long-running operations like AI response generation. */ const handleRelease = () => { wakeLockRef.current = null; sentinel.removeEventListener('release', handleRelease); if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] Lock released, checking if re-acquire needed'); } if (!cancelled && shouldHold && document.visibilityState === 'visible') { if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] Re-acquiring lock'); } void requestLock(); } }; sentinel.addEventListener('release', handleRelease); } catch (error) { console.warn('[WakeLock] request failed', error); } }; /** * Handles browser tab visibility changes * When user returns to the tab, re-acquire the lock if it's still needed * This is necessary because wake locks are automatically released when tab becomes hidden */ const handleVisibilityChange = () => { if (cancelled) { return; } if (DEBUG_WAKE_LOCK) { console.log('[WakeLock] Visibility changed:', document.visibilityState); } if (document.visibilityState === 'visible' && shouldHold) { void requestLock(); } }; if (shouldHold) { void requestLock(); document.addEventListener('visibilitychange', handleVisibilityChange); } else { void releaseLock(); } /** * Cleanup function runs when: * - Component unmounts * - shouldHold changes * - Effect dependencies change * * Sets cancelled flag first to prevent any in-flight operations from completing * Removes event listeners to prevent memory leaks * Releases any held wake lock */ return () => { cancelled = true; document.removeEventListener('visibilitychange', handleVisibilityChange); void releaseLock(); }; }, [shouldHold]); }; export default useWakeLock;