Files
aigen/server/utils/jobs.ts
T
TowstyandCursor c4e6af9f77 Fix generation progress getting stuck at 1% after Comfy finishes.
Poll Comfy history and the job snapshot so the browser still gets the video when the live stream is buffered or the websocket misses completion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 18:23:23 -05:00

118 lines
2.7 KiB
TypeScript

export type JobStatus = 'queued' | 'uploading' | 'running' | 'complete' | 'error' | 'cancelled'
export interface JobEvent {
type: string
message?: string
progress?: number
step?: number
maxStep?: number
node?: string | null
filename?: string
subfolder?: string
mediaType?: string
clipId?: string
hideThumbnail?: boolean
error?: string
elapsedMs?: number
}
export interface Job {
id: string
promptId?: string
clientId: string
status: JobStatus
message: string
progress: number
step: number
maxStep: number
startedAt: number
video?: { filename: string; subfolder: string; type: string }
clipId?: string
hideThumbnail?: boolean
library?: {
folderId: string
hideThumbnail: boolean
prompt: string
aspect: string
width: number
height: number
steps: number
turbo: boolean
seed: number
thumb?: Buffer
}
error?: string
socketReady?: boolean
events: JobEvent[]
listeners: Set<(event: JobEvent) => void>
}
const jobs = new Map<string, Job>()
const MAX_JOBS = 40
export function createJob(): Job {
const job: Job = {
id: crypto.randomUUID(),
clientId: crypto.randomUUID(),
status: 'queued',
message: 'Queued',
progress: 0,
step: 0,
maxStep: 0,
startedAt: Date.now(),
events: [],
listeners: new Set()
}
jobs.set(job.id, job)
while (jobs.size > MAX_JOBS) {
const oldest = jobs.keys().next().value
if (oldest) jobs.delete(oldest)
}
return job
}
export function getJob(id: string) {
return jobs.get(id)
}
export function jobSnapshot(job: Job) {
return {
type: 'snapshot' as const,
status: job.status,
message: job.message,
progress: job.progress,
step: job.step,
maxStep: job.maxStep,
promptId: job.promptId,
elapsedMs: Date.now() - job.startedAt,
filename: job.video?.filename,
subfolder: job.video?.subfolder,
mediaType: job.video?.type,
clipId: job.clipId,
hideThumbnail: job.hideThumbnail,
error: job.error
}
}
export function emitJob(job: Job, event: JobEvent) {
if (event.message) job.message = event.message
if (typeof event.progress === 'number') job.progress = event.progress
if (typeof event.step === 'number') job.step = event.step
if (typeof event.maxStep === 'number') job.maxStep = event.maxStep
event.elapsedMs = Date.now() - job.startedAt
job.events.push(event)
if (job.events.length > 200) job.events.shift()
for (const listener of job.listeners) {
try {
listener(event)
} catch {
// ignore broken SSE listeners
}
}
}
export function subscribeJob(job: Job, listener: (event: JobEvent) => void) {
job.listeners.add(listener)
return () => job.listeners.delete(listener)
}