70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import type { Asset, GenerationJob } from "@/lib/types";
|
|
|
|
export async function waitForJobOutputAssets(job: GenerationJob, signal: AbortSignal) {
|
|
let currentJob = job;
|
|
for (let attempt = 0; attempt < 25; attempt += 1) {
|
|
if (attempt > 0 || currentJob.outputAssetIds.length === 0) {
|
|
currentJob = await fetchImageJob(job.id, signal);
|
|
if (["failed", "expired", "cancelled"].includes(currentJob.status)) {
|
|
throw new Error(currentJob.error?.message || "图层编辑任务未成功完成");
|
|
}
|
|
}
|
|
if (currentJob.outputAssetIds.length) {
|
|
const outputs = await fetchAssetsByID(currentJob.outputAssetIds, signal);
|
|
if (outputs.length) return outputs;
|
|
}
|
|
await abortableDelay(800, signal);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async function fetchImageJob(jobId: string, signal: AbortSignal) {
|
|
const response = await fetch(`/api/generations/image/${encodeURIComponent(jobId)}`, { cache: "no-store", signal });
|
|
const payload = await readJSON(response);
|
|
if (!response.ok) throw new Error(payloadError(payload, "刷新图层编辑任务失败"));
|
|
const job = payload.job as GenerationJob | undefined;
|
|
if (!job?.id || !Array.isArray(job.outputAssetIds)) throw new Error("图层编辑任务返回数据不完整");
|
|
return job;
|
|
}
|
|
|
|
async function fetchAssetsByID(ids: string[], signal: AbortSignal) {
|
|
const response = await fetch("/api/assets", { cache: "no-store", signal });
|
|
const payload = await readJSON(response);
|
|
if (!response.ok) throw new Error(payloadError(payload, "读取编辑结果素材失败"));
|
|
const assets = Array.isArray(payload.assets) ? payload.assets.filter(isAsset) : [];
|
|
const assetsByID = new Map(assets.map((asset) => [asset.id, asset]));
|
|
return ids.map((id) => assetsByID.get(id)).filter((asset): asset is Asset => Boolean(asset));
|
|
}
|
|
|
|
function isAsset(value: unknown): value is Asset {
|
|
if (!value || typeof value !== "object") return false;
|
|
const record = value as Record<string, unknown>;
|
|
return typeof record.id === "string" && typeof record.name === "string" && typeof record.url === "string";
|
|
}
|
|
|
|
async function readJSON(response: Response): Promise<Record<string, unknown>> {
|
|
return response.json().catch(() => ({})) as Promise<Record<string, unknown>>;
|
|
}
|
|
|
|
function payloadError(payload: Record<string, unknown>, fallback: string) {
|
|
return typeof payload.error === "string" && payload.error.trim() ? payload.error : fallback;
|
|
}
|
|
|
|
function abortableDelay(milliseconds: number, signal: AbortSignal) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
if (signal.aborted) {
|
|
reject(new DOMException("Aborted", "AbortError"));
|
|
return;
|
|
}
|
|
const onAbort = () => {
|
|
window.clearTimeout(timer);
|
|
reject(new DOMException("Aborted", "AbortError"));
|
|
};
|
|
const timer = window.setTimeout(() => {
|
|
signal.removeEventListener("abort", onAbort);
|
|
resolve();
|
|
}, milliseconds);
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
});
|
|
}
|