97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
|
|
/**
|
||
|
|
* Generic single-task keep-alive for tool pages.
|
||
|
|
* Persists task state to localStorage so in-progress tasks survive page switches.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const KEEPALIVE_PREFIX = "omniai:tool-task:";
|
||
|
|
|
||
|
|
interface ToolTaskKeepalive {
|
||
|
|
taskId: string;
|
||
|
|
resultUrl: string;
|
||
|
|
resultPreview: string;
|
||
|
|
status: string;
|
||
|
|
progress: number;
|
||
|
|
sourceName: string;
|
||
|
|
sourceUrl: string;
|
||
|
|
savedAt: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function saveToolTaskState(key: string, state: {
|
||
|
|
taskId: string;
|
||
|
|
resultUrl?: string;
|
||
|
|
resultPreview?: string;
|
||
|
|
status?: string;
|
||
|
|
progress?: number;
|
||
|
|
sourceName?: string;
|
||
|
|
sourceUrl?: string;
|
||
|
|
}): void {
|
||
|
|
if (!state.taskId) return;
|
||
|
|
try {
|
||
|
|
const entry: ToolTaskKeepalive = {
|
||
|
|
taskId: state.taskId,
|
||
|
|
resultUrl: state.resultUrl || "",
|
||
|
|
resultPreview: state.resultPreview || "",
|
||
|
|
status: state.status || "",
|
||
|
|
progress: state.progress || 0,
|
||
|
|
sourceName: state.sourceName || "",
|
||
|
|
sourceUrl: state.sourceUrl || "",
|
||
|
|
savedAt: Date.now(),
|
||
|
|
};
|
||
|
|
window.localStorage.setItem(KEEPALIVE_PREFIX + key, JSON.stringify(entry));
|
||
|
|
} catch { /* quota */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function loadToolTaskState(key: string): ToolTaskKeepalive | null {
|
||
|
|
try {
|
||
|
|
const raw = window.localStorage.getItem(KEEPALIVE_PREFIX + key);
|
||
|
|
if (!raw) return null;
|
||
|
|
const parsed = JSON.parse(raw) as ToolTaskKeepalive;
|
||
|
|
if (Date.now() - (parsed.savedAt || 0) > 2 * 60 * 60 * 1000) {
|
||
|
|
clearToolTaskState(key);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (!parsed.taskId) return null;
|
||
|
|
return parsed;
|
||
|
|
} catch { return null; }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function clearToolTaskState(key: string): void {
|
||
|
|
try { window.localStorage.removeItem(KEEPALIVE_PREFIX + key); } catch { /* ignore */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
const TASK_POLL_INTERVAL = 3000;
|
||
|
|
const TASK_POLL_TIMEOUT = 30 * 60 * 1000;
|
||
|
|
|
||
|
|
export async function pollTaskUntilDone(
|
||
|
|
taskId: string,
|
||
|
|
onProgress?: (progress: number) => void,
|
||
|
|
abortRef?: { current: boolean },
|
||
|
|
): Promise<string | null> {
|
||
|
|
const startTime = Date.now();
|
||
|
|
const { aiGenerationClient } = await import("../../api/aiGenerationClient");
|
||
|
|
|
||
|
|
while (true) {
|
||
|
|
if (abortRef?.current) return null;
|
||
|
|
if (Date.now() - startTime > TASK_POLL_TIMEOUT) return null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const task = await aiGenerationClient.getTaskStatus(taskId);
|
||
|
|
if (!task) return null;
|
||
|
|
|
||
|
|
const progress = Math.min(99, task.progress || 0);
|
||
|
|
onProgress?.(progress);
|
||
|
|
|
||
|
|
if (task.status === "completed") {
|
||
|
|
return task.resultUrl || null;
|
||
|
|
}
|
||
|
|
if (task.status === "failed" || task.status === "cancelled") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// retry on next poll
|
||
|
|
}
|
||
|
|
|
||
|
|
await new Promise((r) => setTimeout(r, TASK_POLL_INTERVAL));
|
||
|
|
}
|
||
|
|
}
|