File size: 2,180 Bytes
34b0226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { useEffect, useRef, useState } from "react";
import { streamSSE } from "../api/stream";
import type { Job } from "../api/types";

const TERMINAL = new Set(["done", "failed"]);

/** Follows a job over SSE until it finishes. `onFinish` fires once with the final state. */
export function useJob(initial: Job | null | undefined, onFinish?: (job: Job) => void): Job | null {
  const [job, setJob] = useState<Job | null>(initial ?? null);
  const finish = useRef(onFinish);
  finish.current = onFinish;
  const id = initial?.id;

  useEffect(() => {
    setJob(initial ?? null);
    if (!initial || TERMINAL.has(initial.status)) return;
    const ctrl = new AbortController();
    let done = false;
    streamSSE(
      `/api/jobs/${initial.id}/events`,
      (_event, data) => {
        const next = data as Job;
        setJob(next);
        if (TERMINAL.has(next.status) && !done) {
          done = true;
          finish.current?.(next);
        }
      },
      {},
      ctrl.signal,
    ).catch(() => {
      /* aborted or dropped: the row keeps the final state */
    });
    return () => ctrl.abort();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id]);

  return job;
}

/** Tracks many jobs by key (e.g. per source), each streamed until done. */
export function useJobMap(onFinish?: (key: string, job: Job) => void) {
  const [jobs, setJobs] = useState<Record<string, Job>>({});
  const ctrls = useRef<Record<string, AbortController>>({});
  const finish = useRef(onFinish);
  finish.current = onFinish;

  useEffect(() => () => Object.values(ctrls.current).forEach((c) => c.abort()), []);

  const watch = (key: string, job: Job) => {
    ctrls.current[key]?.abort();
    setJobs((j) => ({ ...j, [key]: job }));
    if (TERMINAL.has(job.status)) return;
    const ctrl = new AbortController();
    ctrls.current[key] = ctrl;
    streamSSE(
      `/api/jobs/${job.id}/events`,
      (_e, data) => {
        const next = data as Job;
        setJobs((j) => ({ ...j, [key]: next }));
        if (TERMINAL.has(next.status)) finish.current?.(key, next);
      },
      {},
      ctrl.signal,
    ).catch(() => {});
  };

  return { jobs, watch };
}