import { isTerminalStatus } from './task-state.js'; import type { TaskState, TaskStatus } from './task-state.js'; import { isValidTransition } from './fsm-transitions.js'; /** * In-memory task store with FSM-validated state transitions. * * This is the single source of truth for task state. It enforces the FSM at * the data layer — illegal transitions are rejected before any mutation occurs. * * Terminal tasks can be evicted by age to prevent unbounded memory growth. */ export class TaskStore { private readonly tasks = new Map(); /** Store a new task. Overwrites if the ID already exists. */ create(task: TaskState): void { this.tasks.set(task.id, task); } /** Retrieve a task by its unique ID. */ get(id: string): TaskState | undefined { return this.tasks.get(id); } /** Linear scan for a task matching the given session ID. */ getBySessionId(sessionId: string): TaskState | undefined { for (const task of this.tasks.values()) { if (task.sessionId === sessionId) return task; } return undefined; } /** Return all tasks as an array. */ getAll(): TaskState[] { return [...this.tasks.values()]; } /** * Transition a task to a new status. * * Returns `true` if the transition was valid and applied, `false` if the * task was not found or the transition is illegal per the FSM. */ updateStatus(id: string, next: TaskStatus): boolean { const task = this.tasks.get(id); if (!task) return false; if (!isValidTransition(task.status, next)) return false; const now = new Date().toISOString(); task.status = next; task.updatedAt = now; if (isTerminalStatus(next)) { task.completedAt = now; } return true; } /** * Apply a partial update to a task. If the update includes a `status` * change, the FSM is validated first — an illegal transition causes the * entire update to be rejected (returns `undefined`). */ update(id: string, updates: Partial): TaskState | undefined { const task = this.tasks.get(id); if (!task) return undefined; // Validate status transition if status is being changed if (updates.status !== undefined && updates.status !== task.status) { if (!isValidTransition(task.status, updates.status)) return undefined; } const now = new Date().toISOString(); Object.assign(task, updates, { updatedAt: now }); if (updates.status !== undefined && isTerminalStatus(updates.status)) { task.completedAt = now; } return task; } /** Remove a task by ID. Returns `true` if it existed. */ delete(id: string): boolean { return this.tasks.delete(id); } /** * Remove terminal tasks whose `updatedAt` timestamp is older than * `maxAgeMs` milliseconds from now. * * Returns the number of tasks evicted. */ evict(maxAgeMs: number): number { const cutoff = Date.now() - maxAgeMs; let count = 0; for (const [id, task] of this.tasks) { if (isTerminalStatus(task.status) && new Date(task.updatedAt).getTime() < cutoff) { this.tasks.delete(id); count++; } } return count; } /** Remove all tasks. */ clear(): void { this.tasks.clear(); } /** Return the number of stored tasks. */ size(): number { return this.tasks.size; } }