| import { useEffect, useRef, useState } from "react"; | |
| import { api } from "../api"; | |
| import type { Job } from "../types"; | |
| // Poll a background job until it finishes, tolerant of transient poll failures (a single failed | |
| // request must not freeze progress β see the load-bar fix). Give up only after a run of failures. | |
| export function useJobPolling(jobId: string | null, onDone?: (job: Job) => void): Job | null { | |
| const [job, setJob] = useState<Job | null>(null); | |
| const onDoneRef = useRef(onDone); | |
| onDoneRef.current = onDone; | |
| useEffect(() => { | |
| if (!jobId) { | |
| setJob(null); | |
| return; | |
| } | |
| let stopped = false; | |
| let fails = 0; | |
| const t = setInterval(async () => { | |
| if (stopped) return; | |
| try { | |
| const next = await api.getJob(jobId); | |
| fails = 0; | |
| setJob(next); | |
| if (next.status === "done" || next.status === "error") { | |
| stopped = true; | |
| clearInterval(t); | |
| if (next.status === "done") onDoneRef.current?.(next); | |
| } | |
| } catch { | |
| fails += 1; | |
| if (fails >= 20) { | |
| // ~10s of consecutive failures β treat the job as unreachable. | |
| stopped = true; | |
| clearInterval(t); | |
| setJob((j) => | |
| j | |
| ? { ...j, status: "error", error: "Lost connection to the job." } | |
| : { | |
| id: jobId, type: "", status: "error", progress: {}, | |
| result: null, error: "Lost connection to the job.", created_at: "", | |
| } | |
| ); | |
| } | |
| // otherwise keep polling: the job is still running server-side. | |
| } | |
| }, 500); | |
| return () => { | |
| stopped = true; | |
| clearInterval(t); | |
| }; | |
| }, [jobId]); | |
| return job; | |
| } | |