import { useEffect, useRef, useState, useCallback } from 'react'; import api from '../api'; export type BackgroundJobStatus = 'PENDING' | 'RUNNING' | 'SUCCESS' | 'ERROR' | 'CANCELLED'; export interface BackgroundJob { id: string; jobType: string; mandateId?: string | null; featureInstanceId?: string | null; triggeredBy?: string | null; status: BackgroundJobStatus; progress: number; progressMessage?: string | null; payload?: Record; result?: Record | null; errorMessage?: string | null; createdAt?: string; startedAt?: string | null; finishedAt?: string | null; } const TERMINAL_STATUSES: BackgroundJobStatus[] = ['SUCCESS', 'ERROR', 'CANCELLED']; export interface UseBackgroundJobOptions { pollMs?: number; enabled?: boolean; onSuccess?: (job: BackgroundJob) => void; onError?: (job: BackgroundJob) => void; } export interface UseBackgroundJobResult { job: BackgroundJob | null; isFinal: boolean; isError: boolean; isLoading: boolean; refetch: () => Promise; } /** * Polls /api/jobs/{jobId} until the job reaches a terminal status. * * Use after submitting a long-running task to the generic background job * service. Handles polling, cleanup on unmount, and exposes the job record * directly so callers can read `job.progress`, `job.result`, etc. */ export function useBackgroundJob( jobId: string | null | undefined, opts: UseBackgroundJobOptions = {}, ): UseBackgroundJobResult { const { pollMs = 2000, enabled = true, onSuccess, onError } = opts; const [job, setJob] = useState(null); const [isLoading, setIsLoading] = useState(false); const mountedRef = useRef(true); const onSuccessRef = useRef(onSuccess); const onErrorRef = useRef(onError); useEffect(() => { onSuccessRef.current = onSuccess; }, [onSuccess]); useEffect(() => { onErrorRef.current = onError; }, [onError]); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const fetchOnce = useCallback(async (): Promise => { if (!jobId) return null; setIsLoading(true); try { const res = await api.get(`/api/jobs/${jobId}`); const next = res.data as BackgroundJob; if (mountedRef.current) setJob(next); return next; } catch (err: any) { if (mountedRef.current) { setJob(prev => prev ?? { id: jobId, jobType: '', status: 'ERROR', progress: 0, errorMessage: err?.response?.data?.detail || err?.message || 'Job nicht abrufbar', }); } return null; } finally { if (mountedRef.current) setIsLoading(false); } }, [jobId]); useEffect(() => { if (!enabled || !jobId) return; let cancelled = false; let timer: ReturnType | null = null; let firedTerminal = false; const tick = async () => { if (cancelled) return; const next = await fetchOnce(); if (cancelled) return; const status = next?.status; if (status && TERMINAL_STATUSES.includes(status)) { if (!firedTerminal) { firedTerminal = true; if (status === 'SUCCESS') onSuccessRef.current?.(next!); else onErrorRef.current?.(next!); } return; } timer = setTimeout(tick, pollMs); }; tick(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [jobId, enabled, pollMs, fetchOnce]); const isFinal = !!job && TERMINAL_STATUSES.includes(job.status); const isError = job?.status === 'ERROR' || job?.status === 'CANCELLED'; return { job, isFinal, isError, isLoading, refetch: async () => { await fetchOnce(); } }; }