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:
203
breakpilot-compliance-sdk/packages/react/src/provider-effects.ts
Normal file
203
breakpilot-compliance-sdk/packages/react/src/provider-effects.ts
Normal 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])
|
||||
}
|
||||
Reference in New Issue
Block a user