Initial ecommerce standalone package
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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<string, { current: boolean }>();
|
||||
const pollCallbacks = new Set<PollCallback>();
|
||||
|
||||
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>): 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<GenerationQueueItem> = {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user