File size: 1,739 Bytes
de1e3fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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;
}