nexus/ui/src/hooks/useContentJob.ts

114 lines
3 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from "react";
import { submitContentJob } from "../api/contentJobs";
type JobStatus = "idle" | "queued" | "running" | "done" | "failed";
interface ContentJobState {
jobId: string | null;
status: JobStatus;
progress: number;
resultAssetId: string | null;
errorMessage: string | null;
}
const INITIAL_STATE: ContentJobState = {
jobId: null,
status: "idle",
progress: 0,
resultAssetId: null,
errorMessage: null,
};
function statusToProgress(status: string): number {
switch (status) {
case "queued":
return 5;
case "running":
return 50;
case "done":
return 100;
case "failed":
return 0;
default:
return 0;
}
}
export function useContentJob(companyId: string | null) {
const [state, setState] = useState<ContentJobState>(INITIAL_STATE);
const eventSourceRef = useRef<EventSource | null>(null);
const closeEventSource = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
}, []);
useEffect(() => {
return () => {
closeEventSource();
};
}, [closeEventSource]);
const submit = useCallback(
async (jobType: string, input: Record<string, unknown>, sourceTaskId?: string | null) => {
if (!companyId) return;
closeEventSource();
setState({ ...INITIAL_STATE, status: "queued", progress: 5 });
const result = await submitContentJob(companyId, jobType, input, sourceTaskId);
const jobId = result.jobId;
setState((prev) => ({ ...prev, jobId, status: "queued", progress: 5 }));
const url = `/api/companies/${companyId}/content-jobs/${jobId}/events`;
const es = new EventSource(url, { withCredentials: true });
eventSourceRef.current = es;
es.addEventListener("status", (e: MessageEvent) => {
const data = JSON.parse(e.data as string) as {
status?: string;
progress?: number;
resultAssetId?: string | null;
errorMessage?: string | null;
};
const status = (data.status ?? "queued") as JobStatus;
const progress =
typeof data.progress === "number"
? data.progress
: statusToProgress(status);
setState((prev) => ({
...prev,
status,
progress,
resultAssetId: data.resultAssetId ?? prev.resultAssetId,
errorMessage: data.errorMessage ?? prev.errorMessage,
}));
if (status === "done" || status === "failed") {
closeEventSource();
}
});
es.addEventListener("error", () => {
closeEventSource();
setState((prev) => ({
...prev,
status: "failed",
errorMessage: prev.errorMessage ?? "Connection error",
}));
});
},
[companyId, closeEventSource],
);
const reset = useCallback(() => {
closeEventSource();
setState(INITIAL_STATE);
}, [closeEventSource]);
return { ...state, submit, reset };
}