refactor(sdk): split hooks, dsr-portal, provider, sync approaching 500 LOC

All four files split into focused sibling modules so every file lands
comfortably under the 300-LOC soft target (hard cap 500):

  hooks.ts (474→43)  → hooks-core / hooks-dsgvo / hooks-compliance
                        hooks-rag-security / hooks-ui
  dsr-portal.ts (464→129) → dsr-portal-translations / dsr-portal-render
  provider.tsx (462→247)  → provider-effects / provider-callbacks
  sync.ts (435→299)       → sync-storage / sync-conflict

Zero behaviour changes. All public APIs remain importable from the
original paths (hooks.ts re-exports every hook, provider.tsx keeps all
named exports, sync.ts preserves StateSyncManager + factory).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-18 08:40:20 +02:00
parent 19d6437161
commit 9ecd3b2d84
15 changed files with 1700 additions and 1299 deletions

View File

@@ -0,0 +1,203 @@
'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<React.SetStateAction<{
status: string
lastSyncedAt: Date | null
localVersion: number
serverVersion: number
pendingChanges: number
error: string | null
}>>
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<StateSyncManager | null>,
{ 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<StateSyncManager | null>,
{ 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<StateSyncManager | null>
): 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])
}