'use client' /** * Side-effect hooks extracted from ComplianceProvider. * * Each function encapsulates one useEffect concern so provider.tsx * stays under 300 LOC. */ import { useEffect, RefObject } from 'react' import type { SDKState } from '@breakpilot/compliance-sdk-types' import { ComplianceClient, StateSyncManager, createStateSyncManager, } from '@breakpilot/compliance-sdk-core' import { SDK_STORAGE_KEY } from './provider-context' // ============================================================================= // TYPES // ============================================================================= export interface SyncStateSetters { setSyncState: React.Dispatch> setIsOnline: (v: boolean) => void setError: (e: Error | null) => void dispatch: React.Dispatch<{ type: string; payload?: unknown }> } // ============================================================================= // SYNC MANAGER EFFECT // ============================================================================= export function useSyncManagerEffect( enableBackendSync: boolean, tenantId: string, client: ComplianceClient, state: SDKState, syncManagerRef: RefObject, { setSyncState, setIsOnline, setError, dispatch }: SyncStateSetters ): void { useEffect(() => { if (enableBackendSync && typeof window !== 'undefined') { syncManagerRef.current = createStateSyncManager( client, tenantId, { debounceMs: 2000, maxRetries: 3 }, { onSyncStart: () => { setSyncState(prev => ({ ...prev, status: 'syncing' })) }, onSyncComplete: syncedState => { setSyncState(prev => ({ ...prev, status: 'idle', lastSyncedAt: new Date(), pendingChanges: 0, })) if (new Date(syncedState.lastModified) > new Date(state.lastModified)) { dispatch({ type: 'SET_STATE', payload: syncedState }) } }, onSyncError: err => { setSyncState(prev => ({ ...prev, status: 'error', error: err.message })) setError(err) }, onConflict: () => { setSyncState(prev => ({ ...prev, status: 'conflict' })) }, onOffline: () => { setIsOnline(false) setSyncState(prev => ({ ...prev, status: 'offline' })) }, onOnline: () => { setIsOnline(true) setSyncState(prev => ({ ...prev, status: 'idle' })) }, } ) } return () => { syncManagerRef.current?.destroy() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [enableBackendSync, tenantId, client]) } // ============================================================================= // LOAD INITIAL STATE EFFECT // ============================================================================= export interface LoadStateSetters { setIsLoading: (v: boolean) => void setIsInitialized: (v: boolean) => void setError: (e: Error | null) => void dispatch: React.Dispatch<{ type: string; payload?: unknown }> onError?: (e: Error) => void } export function useLoadInitialStateEffect( tenantId: string, enableBackendSync: boolean, syncManagerRef: RefObject, { setIsLoading, setIsInitialized, setError, dispatch, onError }: LoadStateSetters ): void { useEffect(() => { const loadInitialState = async () => { setIsLoading(true) try { if (typeof window !== 'undefined') { const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`) if (stored) { const parsed = JSON.parse(stored) if (parsed.lastModified) { parsed.lastModified = new Date(parsed.lastModified) } dispatch({ type: 'SET_STATE', payload: parsed }) } } if (enableBackendSync && syncManagerRef.current) { const serverState = await syncManagerRef.current.loadFromServer() if (serverState) { dispatch({ type: 'SET_STATE', payload: serverState }) } } } catch (err) { setError(err as Error) onError?.(err as Error) } finally { setIsLoading(false) setIsInitialized(true) } } loadInitialState() // eslint-disable-next-line react-hooks/exhaustive-deps }, [tenantId, enableBackendSync]) } // ============================================================================= // AUTO-SAVE EFFECT // ============================================================================= export function useAutoSaveEffect( state: SDKState, tenantId: string, isInitialized: boolean, enableBackendSync: boolean, syncManagerRef: RefObject ): void { useEffect(() => { if (!isInitialized || !state.preferences.autoSave) return const saveTimeout = setTimeout(() => { try { if (typeof window !== 'undefined') { localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state)) } if (enableBackendSync && syncManagerRef.current) { syncManagerRef.current.queueSync(state) } } catch (err) { console.error('Failed to save state:', err) } }, 1000) return () => clearTimeout(saveTimeout) }, [state, tenantId, isInitialized, enableBackendSync]) } // ============================================================================= // KEYBOARD SHORTCUTS EFFECT // ============================================================================= export function useKeyboardShortcutsEffect( isCommandBarOpen: boolean, setCommandBarOpen: (fn: (prev: boolean) => boolean) => void ): void { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setCommandBarOpen(prev => !prev) } if (e.key === 'Escape' && isCommandBarOpen) { setCommandBarOpen(() => false) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [isCommandBarOpen, setCommandBarOpen]) }