import { useGenerationStore, type GenerationQueueItem } from "../stores/useGenerationStore"; import { waitForTask, type TaskProgressEvent } from "../api/taskSubscription"; import { buildTaskFailureInfo } from "../utils/taskLifecycle"; type PollCallback = (item: GenerationQueueItem) => void; const activePollers = new Map(); const pollCallbacks = new Set(); export function subscribeToTaskUpdates(callback: PollCallback): () => void { pollCallbacks.add(callback); return () => { pollCallbacks.delete(callback); }; } function notifyCallbacks(item: GenerationQueueItem): void { pollCallbacks.forEach((cb) => cb(item)); } function getQueueItemKind(item: GenerationQueueItem): "image" | "video" | "text" { if (item.type === "image") return "image"; if (item.type === "video" || item.type === "ecommerce-video") return "video"; return "text"; } function getQueueItemModel(item: GenerationQueueItem): string | undefined { return typeof item.params?.model === "string" ? item.params.model : undefined; } function updateTaskAndNotify(id: string, patch: Partial): GenerationQueueItem | null { const current = useGenerationStore.getState().queue.find((i) => i.id === id); if (!current) return null; const next = { ...current, ...patch }; useGenerationStore.getState().updateTask(id, patch); notifyCallbacks(next); return next; } function isTerminalStatus(status: GenerationQueueItem["status"]): boolean { return status === "completed" || status === "failed" || status === "cancelled"; } function pollTask(item: GenerationQueueItem): void { const key = `poll-${item.id}`; if (activePollers.has(key) || !item.taskId) return; const kind = getQueueItemKind(item); const abortRef = { current: false }; activePollers.set(key, abortRef); const applyProgress = (event: TaskProgressEvent) => { const current = useGenerationStore.getState().queue.find((i) => i.id === item.id); if (!current || isTerminalStatus(current.status)) { abortRef.current = true; return; } const patch: Partial = { progress: Number(event.progress || 0), resultUrl: event.resultUrl || current.resultUrl, error: event.error || current.error, }; if (event.status === "completed") { patch.status = "completed"; patch.progress = 100; } else if (event.status === "failed" || event.status === "cancelled") { patch.status = "failed"; patch.error = buildTaskFailureInfo(event.error).message; } else { patch.status = "running"; } updateTaskAndNotify(item.id, patch); }; void waitForTask(item.taskId, { kind, model: getQueueItemModel(item), startedAt: item.createdAt || Date.now(), abortRef, onProgress: applyProgress, }) .then((resultUrl) => { if (abortRef.current) return; const current = useGenerationStore.getState().queue.find((i) => i.id === item.id); if (!current || isTerminalStatus(current.status)) return; updateTaskAndNotify(item.id, { status: "completed", progress: 100, resultUrl: resultUrl || current.resultUrl, }); }) .catch((error) => { if (abortRef.current) return; const failure = buildTaskFailureInfo(error instanceof Error ? error.message : String(error)); updateTaskAndNotify(item.id, { status: "failed", error: failure.message, }); }) .finally(() => { cleanupPoll(key, abortRef); }); } function cleanupPoll(key: string, abortRef: { current: boolean }): void { if (activePollers.get(key) !== abortRef) return; activePollers.delete(key); } export function startBackgroundPolling(): void { const tasks = useGenerationStore.getState().getRunningTasks(); tasks.forEach((task) => { if (task.taskId) { pollTask(task); } }); } export function resumeTaskPolling(taskId: string, storeId: string): void { const task = useGenerationStore.getState().queue.find((i) => i.id === storeId); if (task && !isTerminalStatus(task.status)) { pollTask({ ...task, taskId }); } } export function stopAllPolling(): void { activePollers.forEach((abortRef) => { abortRef.current = true; }); activePollers.clear(); } export function recoverAndResumeTasks(): void { const pendingTasks = useGenerationStore.getState().getRunningTasks(); if (!pendingTasks.length) return; pendingTasks.forEach((task) => { if (task.taskId) { useGenerationStore.getState().updateTask(task.id, { status: "pending" }); } else { useGenerationStore.getState().updateTask(task.id, { status: "failed", error: "页面刷新后任务没有服务端 ID,已释放本地占用,请重新提交。", }); } }); setTimeout(() => startBackgroundPolling(), 500); }