Files
breakpilot-compliance/breakpilot-compliance-sdk/packages/react/src/provider.tsx
T
Sharang Parnerkar 9ecd3b2d84 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>
2026-04-18 08:40:20 +02:00

248 lines
7.3 KiB
TypeScript

'use client'
import React, {
useReducer,
useMemo,
useRef,
useState,
} from 'react'
import {
ComplianceClient,
sdkReducer,
initialState,
StateSyncManager,
createDSGVOModule,
createComplianceModule,
createRAGModule,
createSecurityModule,
} from '@breakpilot/compliance-sdk-core'
import type { SyncState } from '@breakpilot/compliance-sdk-types'
import {
getStepById,
getNextStep,
getPreviousStep,
getCompletionPercentage,
getPhaseCompletionPercentage,
} from '@breakpilot/compliance-sdk-types'
import {
ComplianceContext,
type ComplianceContextValue,
type ComplianceProviderProps,
} from './provider-context'
import {
useSyncManagerEffect,
useLoadInitialStateEffect,
useAutoSaveEffect,
useKeyboardShortcutsEffect,
} from './provider-effects'
import {
useValidateCheckpoint,
useOverrideCheckpoint,
useGetCheckpointStatus,
useUpdateUseCase,
useAddRisk,
useUpdateControl,
useSaveState,
useLoadState,
useResetState,
useForceSyncToServer,
useExportState,
} from './provider-callbacks'
import { useCallback, useMemo as useMemoReact } from 'react'
export {
ComplianceContext,
type ComplianceContextValue,
type ComplianceProviderProps,
} from './provider-context'
// Re-export useCompliance so legacy component imports (`from '../provider'`)
// keep resolving. Pre-existing cross-file import that was broken in baseline.
export { useCompliance } from './hooks'
// =============================================================================
// PROVIDER
// =============================================================================
export function ComplianceProvider({
children,
apiEndpoint,
apiKey,
tenantId,
userId = 'default',
enableBackendSync = true,
onNavigate,
onError,
}: ComplianceProviderProps) {
const [state, dispatch] = useReducer(sdkReducer, {
...initialState,
tenantId,
userId,
})
const [isCommandBarOpen, setCommandBarOpenRaw] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const [syncState, setSyncState] = useState<SyncState>({
status: 'idle',
lastSyncedAt: null,
localVersion: 0,
serverVersion: 0,
pendingChanges: 0,
error: null,
})
const [isOnline, setIsOnline] = useState(true)
// Refs
const clientRef = useRef<ComplianceClient | null>(null)
const syncManagerRef = useRef<StateSyncManager | null>(null)
// Initialize client (once)
if (!clientRef.current) {
clientRef.current = new ComplianceClient({
apiEndpoint,
apiKey,
tenantId,
onError: err => {
setError(err)
onError?.(err)
},
})
}
const client = clientRef.current
// Modules
const dsgvo = useMemo(() => createDSGVOModule(client, () => state), [client, state])
const compliance = useMemo(() => createComplianceModule(client, () => state), [client, state])
const rag = useMemo(() => createRAGModule(client), [client])
const security = useMemo(() => createSecurityModule(client, () => state), [client, state])
// -------------------------------------------------------------------------
// Effects (extracted to provider-effects.ts)
// -------------------------------------------------------------------------
const syncStateSetters = useMemo(
() => ({ setSyncState, setIsOnline, setError, dispatch }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
useSyncManagerEffect(enableBackendSync, tenantId, client, state, syncManagerRef, syncStateSetters)
useLoadInitialStateEffect(tenantId, enableBackendSync, syncManagerRef, {
setIsLoading,
setIsInitialized,
setError,
dispatch,
onError,
})
useAutoSaveEffect(state, tenantId, isInitialized, enableBackendSync, syncManagerRef)
const setCommandBarOpen = useCallback(
(fn: boolean | ((prev: boolean) => boolean)) => {
setCommandBarOpenRaw(typeof fn === 'function' ? fn : () => fn)
},
[]
)
useKeyboardShortcutsEffect(isCommandBarOpen, setCommandBarOpen as (fn: (prev: boolean) => boolean) => void)
// -------------------------------------------------------------------------
// Navigation
// -------------------------------------------------------------------------
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
const goToStep = useCallback(
(stepId: string) => {
const step = getStepById(stepId)
if (step) {
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
onNavigate?.(step.url)
}
},
[onNavigate]
)
const goToNextStep = useCallback(() => {
const nextStep = getNextStep(state.currentStep)
if (nextStep) goToStep(nextStep.id)
}, [state.currentStep, goToStep])
const goToPreviousStep = useCallback(() => {
const prevStep = getPreviousStep(state.currentStep)
if (prevStep) goToStep(prevStep.id)
}, [state.currentStep, goToStep])
const canGoNext = useMemo(() => getNextStep(state.currentStep) !== undefined, [state.currentStep])
const canGoPrevious = useMemo(
() => getPreviousStep(state.currentStep) !== undefined,
[state.currentStep]
)
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
// -------------------------------------------------------------------------
// Callbacks (extracted to provider-callbacks.ts)
// -------------------------------------------------------------------------
const validateCheckpoint = useValidateCheckpoint(state, enableBackendSync, client, dispatch)
const overrideCheckpoint = useOverrideCheckpoint(state, dispatch)
const getCheckpointStatus = useGetCheckpointStatus(state)
const updateUseCase = useUpdateUseCase(dispatch)
const addRisk = useAddRisk(dispatch)
const updateControl = useUpdateControl(dispatch)
const saveState = useSaveState(state, tenantId, enableBackendSync, syncManagerRef, e => setError(e))
const loadState = useLoadState(tenantId, enableBackendSync, syncManagerRef, setIsLoading, dispatch, e => setError(e))
const resetState = useResetState(tenantId, dispatch)
const forceSyncToServer = useForceSyncToServer(state, enableBackendSync, syncManagerRef)
const exportState = useExportState(state, client)
// -------------------------------------------------------------------------
// Context value
// -------------------------------------------------------------------------
const value: ComplianceContextValue = {
state,
dispatch,
client,
dsgvo,
compliance,
rag,
security,
currentStep,
goToStep,
goToNextStep,
goToPreviousStep,
canGoNext,
canGoPrevious,
completionPercentage,
phase1Completion,
phase2Completion,
validateCheckpoint,
overrideCheckpoint,
getCheckpointStatus,
updateUseCase,
addRisk,
updateControl,
saveState,
loadState,
resetState,
syncState,
forceSyncToServer,
isOnline,
exportState,
isCommandBarOpen,
setCommandBarOpen: setCommandBarOpenRaw,
isInitialized,
isLoading,
error,
}
return <ComplianceContext.Provider value={value}>{children}</ComplianceContext.Provider>
}