story-research-zapwall/lib/hooks/useSyncProgress.ts
2026-01-13 14:49:19 +01:00

148 lines
5.7 KiB
TypeScript

/**
* Custom hook for monitoring sync progress
* Centralizes the pattern of polling syncProgressManager and updating state
*/
import { useState, useEffect, useRef, useCallback, type MutableRefObject } from 'react'
import type { SyncProgress } from '../helpers/syncProgressHelper'
export interface UseSyncProgressOptions {
onComplete?: () => void | Promise<void>
pollInterval?: number
maxDuration?: number
}
export interface UseSyncProgressResult {
syncProgress: SyncProgress | null
isSyncing: boolean
startMonitoring: () => void
stopMonitoring: () => void
}
/**
* Hook to monitor sync progress from syncProgressManager
*/
export function useSyncProgress(options: UseSyncProgressOptions = {}): UseSyncProgressResult {
const config = normalizeSyncProgressOptions(options)
const state = useSyncProgressState({ onComplete: config.onComplete })
const checkProgress = useCheckProgress({ onCompleteRef: state.onCompleteRef, setSyncProgress: state.setSyncProgress, setIsSyncing: state.setIsSyncing, stopMonitoring: state.stopMonitoring })
const startMonitoring = useStartMonitoring({ pollInterval: config.pollInterval, maxDuration: config.maxDuration, checkProgress, stopMonitoring: state.stopMonitoring, setIsSyncing: state.setIsSyncing, setSyncProgress: state.setSyncProgress, isMonitoringRef: state.isMonitoringRef, intervalRef: state.intervalRef, timeoutRef: state.timeoutRef })
useUnmountCleanup({ stopMonitoring: state.stopMonitoring })
return { syncProgress: state.syncProgress, isSyncing: state.isSyncing, startMonitoring, stopMonitoring: state.stopMonitoring }
}
function normalizeSyncProgressOptions(options: UseSyncProgressOptions): { onComplete: UseSyncProgressOptions['onComplete']; pollInterval: number; maxDuration: number } {
return { onComplete: options.onComplete, pollInterval: options.pollInterval ?? 500, maxDuration: options.maxDuration ?? 60000 }
}
function useSyncProgressState(params: { onComplete: UseSyncProgressOptions['onComplete'] }): {
syncProgress: SyncProgress | null
setSyncProgress: (value: SyncProgress | null) => void
isSyncing: boolean
setIsSyncing: (value: boolean) => void
intervalRef: MutableRefObject<ReturnType<typeof setInterval> | null>
timeoutRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
isMonitoringRef: MutableRefObject<boolean>
onCompleteRef: MutableRefObject<UseSyncProgressOptions['onComplete']>
stopMonitoring: () => void
} {
const [syncProgress, setSyncProgress] = useState<SyncProgress | null>(null)
const [isSyncing, setIsSyncing] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isMonitoringRef = useRef(false)
const onCompleteRef = useRef(params.onComplete)
useEffect(() => {
onCompleteRef.current = params.onComplete
}, [params.onComplete])
const stopMonitoring = useCallback((): void => {
if (!isMonitoringRef.current) {
return
}
isMonitoringRef.current = false
setIsSyncing(false)
intervalRef.current = clearIntervalAndReturnNull(intervalRef.current)
timeoutRef.current = clearTimeoutAndReturnNull(timeoutRef.current)
}, [])
return { syncProgress, setSyncProgress, isSyncing, setIsSyncing, intervalRef, timeoutRef, isMonitoringRef, onCompleteRef, stopMonitoring }
}
function clearIntervalAndReturnNull(current: ReturnType<typeof setInterval> | null): null {
if (current) {
clearInterval(current)
}
return null
}
function clearTimeoutAndReturnNull(current: ReturnType<typeof setTimeout> | null): null {
if (current) {
clearTimeout(current)
}
return null
}
function useCheckProgress(params: {
onCompleteRef: MutableRefObject<UseSyncProgressOptions['onComplete']>
setSyncProgress: (value: SyncProgress | null) => void
setIsSyncing: (value: boolean) => void
stopMonitoring: () => void
}): () => Promise<void> {
const { onCompleteRef, setSyncProgress, setIsSyncing, stopMonitoring } = params
return useCallback(async (): Promise<void> => {
const { syncProgressManager } = await import('../syncProgressManager')
const current = syncProgressManager.getProgress()
if (!current) {
return
}
setSyncProgress(current)
if (!current.completed) {
return
}
setIsSyncing(false)
await onCompleteRef.current?.()
stopMonitoring()
}, [onCompleteRef, setIsSyncing, setSyncProgress, stopMonitoring])
}
function useStartMonitoring(params: {
pollInterval: number
maxDuration: number
checkProgress: () => Promise<void>
stopMonitoring: () => void
setIsSyncing: (value: boolean) => void
setSyncProgress: (value: SyncProgress | null) => void
isMonitoringRef: MutableRefObject<boolean>
intervalRef: MutableRefObject<ReturnType<typeof setInterval> | null>
timeoutRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
}): () => void {
const {
pollInterval,
maxDuration,
checkProgress,
stopMonitoring,
setIsSyncing,
setSyncProgress,
isMonitoringRef,
intervalRef,
timeoutRef,
} = params
return useCallback((): void => {
if (isMonitoringRef.current) {
return
}
isMonitoringRef.current = true
setIsSyncing(true)
setSyncProgress({ currentStep: 0, totalSteps: 7, completed: false })
intervalRef.current = setInterval(() => void checkProgress(), pollInterval)
timeoutRef.current = setTimeout(() => stopMonitoring(), maxDuration)
}, [checkProgress, intervalRef, isMonitoringRef, maxDuration, pollInterval, setIsSyncing, setSyncProgress, stopMonitoring, timeoutRef])
}
function useUnmountCleanup(params: { stopMonitoring: () => void }): void {
const { stopMonitoring } = params
useEffect(() => () => stopMonitoring(), [stopMonitoring])
}