The Coline design system: accessible React components for Coline Apps, themed live by the host workspace, and for Coline Sites in light and dark. Tailwind v4 stylesheet included. MIT.
| 5 | import { useColine } from "../provider"; |
| 6 |
| 7 | const FINISHED = new Set(["succeeded", "failed", "cancelled", "dead"]); |
| 8 |
| 9 | /** |
| 10 | * Watch a background job run until it finishes. Updates arrive through |
| 11 | * the install's realtime hints (`coline.jobs.changed`) when the app has |
| 12 | * `realtime.subscribe`, with polling as the fallback either way. |
| 13 | */ |
| 14 | export function useJob(jobId: string | null, options: { pollMs?: number } = {}) { |
| 15 | const coline = useColine(); |
| 16 | const [latest, setRun] = React.useState<ColineJobRun | null>(null); |
| 17 | const [error, setError] = React.useState<string | null>(null); |
| 18 | const pollMs = options.pollMs ?? 1500; |
| 19 | // A stale run from a previous jobId never shows for the new one. |
| 20 | const run = jobId && latest?.id === jobId ? latest : null; |
| 21 |
| 22 | React.useEffect(() => { |
| 23 | if (!jobId) return; |
| 24 | let cancelled = false; |
| 25 | let timer: ReturnType<typeof setTimeout> | null = null; |
| 26 | let unsubscribe: (() => void) | null = null; |
| 27 |
| 28 | const load = async () => { |
| 29 | try { |
| 30 | const next = await coline.jobs.get(jobId); |
| 31 | if (cancelled) return; |
| 32 | setRun(next); |
| 33 | setError(null); |
| 34 | if (!next || FINISHED.has(next.status)) return; |
| 35 | } catch (cause) { |
| 36 | if (cancelled) return; |
| 37 | setError(cause instanceof Error ? cause.message : String(cause)); |
| 38 | } |
| 39 | if (timer) clearTimeout(timer); |
| 40 | timer = setTimeout(() => void load(), pollMs); |
| 41 | }; |
| 42 |
| 43 | void load(); |
| 44 | void coline.realtime |
| 45 | .subscribe("coline.jobs.changed", (message) => { |
| 46 | const data = message.data as { id?: string } | null; |
| 47 | if (data?.id === jobId) void load(); |
| 48 | }) |
| 49 | .then((subscription) => { |
| 50 | if (cancelled) subscription.unsubscribe(); |
| 51 | else unsubscribe = subscription.unsubscribe; |
| 52 | }) |
| 53 | .catch(() => { |
| 54 | // Without realtime permission, polling alone keeps the run current. |
| 55 | }); |
| 56 |
| 57 | return () => { |
| 58 | cancelled = true; |
| 59 | if (timer) clearTimeout(timer); |
| 60 | unsubscribe?.(); |
| 61 | }; |
| 62 | }, [coline, jobId, pollMs]); |
| 63 |
| 64 | return { |
| 65 | run, |
| 66 | error, |
| 67 | isRunning: run ? !FINISHED.has(run.status) : false, |
| 68 | progress: run?.progress?.value ?? (run?.status === "succeeded" ? 1 : 0), |
| 69 | }; |
| 70 | } |
| 71 |