"use client"; import * as React from "react"; import type { ColineJobRun } from "@colineapp/sdk/v2"; import { useColine } from "../provider"; const FINISHED = new Set(["succeeded", "failed", "cancelled", "dead"]); /** * Watch a background job run until it finishes. Updates arrive through * the install's realtime hints (`coline.jobs.changed`) when the app has * `realtime.subscribe`, with polling as the fallback either way. */ export function useJob(jobId: string | null, options: { pollMs?: number } = {}) { const coline = useColine(); const [latest, setRun] = React.useState(null); const [error, setError] = React.useState(null); const pollMs = options.pollMs ?? 1500; // A stale run from a previous jobId never shows for the new one. const run = jobId && latest?.id === jobId ? latest : null; React.useEffect(() => { if (!jobId) return; let cancelled = false; let timer: ReturnType | null = null; let unsubscribe: (() => void) | null = null; const load = async () => { try { const next = await coline.jobs.get(jobId); if (cancelled) return; setRun(next); setError(null); if (!next || FINISHED.has(next.status)) return; } catch (cause) { if (cancelled) return; setError(cause instanceof Error ? cause.message : String(cause)); } if (timer) clearTimeout(timer); timer = setTimeout(() => void load(), pollMs); }; void load(); void coline.realtime .subscribe("coline.jobs.changed", (message) => { const data = message.data as { id?: string } | null; if (data?.id === jobId) void load(); }) .then((subscription) => { if (cancelled) subscription.unsubscribe(); else unsubscribe = subscription.unsubscribe; }) .catch(() => { // Without realtime permission, polling alone keeps the run current. }); return () => { cancelled = true; if (timer) clearTimeout(timer); unsubscribe?.(); }; }, [coline, jobId, pollMs]); return { run, error, isRunning: run ? !FINISHED.has(run.status) : false, progress: run?.progress?.value ?? (run?.status === "succeeded" ? 1 : 0), }; }