114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import type { SmartEditCheckpoint } from "./smart-edit/execute";
|
|
import { audioCheckpointSchema } from "./audio-contract";
|
|
import { videoCheckpointSchema } from "./video-task-lifecycle";
|
|
// Resume only server-recorded provider jobs. A lost request without an upstream ID is never resubmitted.
|
|
export type RecoveryTask = {
|
|
status: string;
|
|
createdAt: string;
|
|
updatedAt?: string;
|
|
attempts?: number;
|
|
request: { kind: string; params: Record<string, unknown> };
|
|
result?: Record<string, any> | null;
|
|
};
|
|
export function getEnhanceCheckpoint(
|
|
task: Pick<RecoveryTask, "request" | "result">
|
|
) {
|
|
const upstream = task.result?.upstreamTask;
|
|
if (
|
|
task.request.kind !== "image-operation" ||
|
|
task.request.params.operation !== "enhance" ||
|
|
upstream?.operation !== "enhance" ||
|
|
typeof upstream.taskId !== "string" ||
|
|
!upstream.taskId.trim() ||
|
|
upstream.sourceImageUrl !== task.request.params.imageUrl
|
|
)
|
|
return null;
|
|
return {
|
|
operation: "enhance" as const,
|
|
taskId: upstream.taskId,
|
|
sourceImageUrl: String(upstream.sourceImageUrl),
|
|
};
|
|
}
|
|
export function getVideoCheckpoint(
|
|
task: Pick<RecoveryTask, "request" | "result">
|
|
) {
|
|
if (task.request.kind !== "video") return null;
|
|
const parsed = videoCheckpointSchema.safeParse(task.result?.upstreamTask);
|
|
if (!parsed.success) return null;
|
|
const preferred = task.request.params.preferredModelRef;
|
|
if (
|
|
preferred &&
|
|
preferred !== "auto" &&
|
|
preferred !== parsed.data.modelRef &&
|
|
preferred !== parsed.data.modelId
|
|
)
|
|
return null;
|
|
return parsed.data;
|
|
}
|
|
export function getAudioCheckpoint(
|
|
task: Pick<RecoveryTask, "request" | "result">
|
|
) {
|
|
if (task.request.kind !== "audio") return null;
|
|
const parsed = audioCheckpointSchema.safeParse(task.result?.upstreamAudio);
|
|
return parsed.success && parsed.data.modelRef === task.request.params.modelRef
|
|
? parsed.data
|
|
: null;
|
|
}
|
|
export function getSmartEditCheckpoint(
|
|
task: Pick<RecoveryTask, "request" | "result">
|
|
): SmartEditCheckpoint | null {
|
|
const value = task.result?.smartEdit;
|
|
if (
|
|
task.request.kind !== "smart-edit" ||
|
|
value?.operation !== "smart-edit" ||
|
|
value.action !== task.request.params.operation ||
|
|
!["local", "analyzed", "selected", "remote-render"].includes(value.phase)
|
|
)
|
|
return null;
|
|
if (
|
|
value.phase === "remote-render" &&
|
|
(value.action !== "render" ||
|
|
!/^[a-f0-9]{64}$/.test(value.remote?.id || "") ||
|
|
!/^[a-f0-9]{64}$/.test(value.remote?.endpointHash || ""))
|
|
)
|
|
return null;
|
|
return value as SmartEditCheckpoint;
|
|
}
|
|
export function hasRecoverableCanvasTask(task: RecoveryTask, now = Date.now()) {
|
|
const created = Date.parse(task.createdAt);
|
|
return (
|
|
Boolean(
|
|
getEnhanceCheckpoint(task) ||
|
|
getVideoCheckpoint(task) ||
|
|
getAudioCheckpoint(task) ||
|
|
getSmartEditCheckpoint(task) ||
|
|
getUpscaleCheckpoint(task)
|
|
) &&
|
|
Number.isFinite(created) &&
|
|
now >= created &&
|
|
now - created <= 86400000 &&
|
|
(task.attempts || 0) < 20
|
|
);
|
|
}
|
|
export function canResumeCanvasTask(task: RecoveryTask, now = Date.now()) {
|
|
if (task.status !== "interrupted" || !hasRecoverableCanvasTask(task, now))
|
|
return false;
|
|
const updated = Date.parse(task.updatedAt || "");
|
|
const delay = Math.min(
|
|
300000,
|
|
30000 * 2 ** Math.min(4, Math.max(0, (task.attempts || 1) - 1))
|
|
);
|
|
return Number.isFinite(updated) && now - updated >= delay;
|
|
}
|
|
|
|
export function getUpscaleCheckpoint(
|
|
task: Pick<RecoveryTask, "request" | "result">
|
|
) {
|
|
const value = task.result?.upscale?.remote;
|
|
return task.request.kind === "video-upscale" &&
|
|
/^[a-f0-9]{64}$/.test(value?.id || "") &&
|
|
/^[a-f0-9]{64}$/.test(value?.endpointHash || "")
|
|
? (value as { id: string; endpointHash: string })
|
|
: null;
|
|
}
|