/** In-memory, host-resident image generation queue. */ import { randomUUID } from 'node:crypto' import type { GenerateRequest, GenerateResult, GenerationTask, GenerationTaskSummary } from './protocol.ts' export type GenerationTaskListener = (task: GenerationTask) => void export class GenerationTaskQueue { private readonly tasks: GenerationTask[] = [] private readonly controllers = new Map() private readonly listeners = new Set() private running = 0 constructor( private readonly run: (request: GenerateRequest, signal: AbortSignal) => Promise, private readonly concurrency = 1, ) {} list(): GenerationTask[] { return this.tasks.map(task => this.snapshot(task)) } /** * Bounded poll payload: active tasks plus the newest terminal summaries. * Image data and completed results are intentionally omitted. */ summaries(terminalLimit = 100): GenerationTaskSummary[] { const limit = Math.max(0, Math.floor(terminalLimit)) const active = this.tasks.filter(task => task.status === 'queued' || task.status === 'running') const terminal = this.tasks.filter(task => task.status !== 'queued' && task.status !== 'running').slice(0, limit) const selected = new Set([...active, ...terminal].map(task => task.id)) return this.tasks.filter(task => selected.has(task.id)).map(task => this.summary(task)) } get(id: string): GenerationTask | undefined { const task = this.tasks.find(item => item.id === id) return task === undefined ? undefined : this.snapshot(task) } /** Observe queue state changes. Listener failures never disrupt generation. */ subscribe(listener: GenerationTaskListener): () => void { this.listeners.add(listener) return () => { this.listeners.delete(listener) } } submit(request: GenerateRequest): GenerationTask { const task: GenerationTask = { id: randomUUID(), request: { ...request }, status: 'queued', createdAt: Date.now() } this.tasks.unshift(task) this.publish(task) this.drain() return this.snapshot(task) } cancel(id: string): GenerationTask | undefined { const task = this.tasks.find(item => item.id === id) if (task === undefined || task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') return task task.status = 'cancelled' task.finishedAt = Date.now() this.controllers.get(id)?.abort() this.publish(task) this.drain() return this.snapshot(task) } retry(id: string): GenerationTask | undefined { const previous = this.tasks.find(item => item.id === id) return previous === undefined ? undefined : this.submit(previous.request) } /** Start queued tasks while capacity remains. Plain submissions and * comparison batches alike run in parallel up to the host-wide limit, so one * slow upstream can no longer hold back unrelated generations. */ private drain(): void { while (this.running < Math.max(1, this.concurrency)) { const task = this.tasks.find(item => item.status === 'queued') if (task === undefined) return this.running += 1 void this.runTask(task).finally(() => { this.running -= 1 this.drain() }) } } private async runTask(task: GenerationTask): Promise { task.status = 'running' task.startedAt = Date.now() this.publish(task) const controller = new AbortController() this.controllers.set(task.id, controller) try { const result = await this.run(task.request, controller.signal) if (this.tasks.find(item => item.id === task.id)?.status !== 'cancelled') { task.status = 'completed' task.result = result task.finishedAt = Date.now() this.publish(task) } } catch (error) { if (this.tasks.find(item => item.id === task.id)?.status !== 'cancelled') { task.status = 'failed' task.error = error instanceof Error ? error.message : String(error) task.finishedAt = Date.now() this.publish(task) } } finally { this.controllers.delete(task.id) } } private publish(task: GenerationTask): void { const snapshot = this.snapshot(task) for (const listener of this.listeners) { try { listener(snapshot) } catch { // Observers must not be able to interrupt the queue pump. } } } private summary(task: GenerationTask): GenerationTaskSummary { const request = { ...task.request } delete request.image delete request.images return { id: task.id, request, status: task.status, createdAt: task.createdAt, ...task.startedAt === undefined ? {} : { startedAt: task.startedAt }, ...task.finishedAt === undefined ? {} : { finishedAt: task.finishedAt }, ...task.error === undefined ? {} : { error: task.error }, resultAvailable: task.result !== undefined, } } private snapshot(task: GenerationTask): GenerationTask { return { ...task, request: { ...task.request }, ...task.result === undefined ? {} : { result: task.result }, } } }