import { randomUUID } from 'node:crypto'; import type { ServerTask } from './types'; interface TaskManagerOptions { maxConcurrentPerScope: number; keyTTLMs: number; } export class TaskManager { private tasks = new Map(); private apiKeys = new Map(); private sweepInterval: ReturnType | null = null; constructor(private readonly options: TaskManagerOptions) { this.sweepInterval = setInterval(() => this.sweepExpiredKeys(), 60_000); } createTask(projectId: string, sessionId: string, apiKey: string, workspaceId?: string): string { const scope = workspaceId ?? sessionId; const activeTasks = [...this.tasks.values()].filter( (t) => (t.workspaceId ?? t.sessionId) === scope && (t.status === 'running' || t.status === 'paused'), ); if (activeTasks.length >= this.options.maxConcurrentPerScope) { throw new Error(`Concurrent task limit (${this.options.maxConcurrentPerScope}) reached`); } const taskId = randomUUID(); const task: ServerTask = { taskId, projectId, sessionId, workspaceId, status: 'running', startedAt: Date.now(), orchestrator: null, buildDeferred: false, pendingBuildResolve: null, }; this.tasks.set(taskId, task); this.apiKeys.set(taskId, { key: apiKey, createdAt: Date.now() }); return taskId; } getTask(taskId: string): ServerTask | undefined { return this.tasks.get(taskId); } getApiKey(taskId: string): string | undefined { return this.apiKeys.get(taskId)?.key; } completeTask(taskId: string, status: 'completed' | 'failed' | 'cancelled'): void { const task = this.tasks.get(taskId); if (task) { task.status = status; task.orchestrator = null; } this.apiKeys.delete(taskId); } getTasksForSession(sessionId: string): ServerTask[] { return [...this.tasks.values()].filter((t) => t.sessionId === sessionId); } sweepExpiredKeys(): void { const now = Date.now(); for (const [taskId, entry] of this.apiKeys) { if (now - entry.createdAt > this.options.keyTTLMs) { this.apiKeys.delete(taskId); } } for (const [taskId, task] of this.tasks) { if (task.status !== 'running' && task.status !== 'paused' && now - task.startedAt > this.options.keyTTLMs) { this.tasks.delete(taskId); } } } dispose(): void { if (this.sweepInterval) { clearInterval(this.sweepInterval); this.sweepInterval = null; } } }