/** * Shared utility functions that can be used across frontend and backend. */ export interface SortableItem { priority: 'low' | 'medium' | 'high'; createdAt: Date | string; } /** * Priority order mapping for sorting (higher number = higher priority) */ const PRIORITY_ORDER = { high: 3, medium: 2, low: 1, } as const; /** * Sorts items by priority (high to low) then by queue time (oldest first). * This ensures high-priority items come first, but within the same priority, * items are processed in FIFO order. */ export function sortByPriorityAndQueueTime(items: T[]): T[] { return [...items].sort((a, b) => { // First sort by priority (high to low) const priorityDiff = PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority]; if (priorityDiff !== 0) { return priorityDiff; } // Within same priority, sort by creation time (oldest first - FIFO) const aTime = typeof a.createdAt === 'string' ? new Date(a.createdAt) : a.createdAt; const bTime = typeof b.createdAt === 'string' ? new Date(b.createdAt) : b.createdAt; return aTime.getTime() - bTime.getTime(); }); } // --------------------------------------------------------------------------- // Topological Phase Sorting // --------------------------------------------------------------------------- export interface PhaseForSort { id: string; createdAt: Date | string; } export interface DependencyEdge { phaseId: string; dependsOnPhaseId: string; } /** * Topologically sort phases by their dependency edges (Kahn's algorithm). * Phases with no dependencies come first. Deterministic tiebreaker: createdAt ascending. * Phases involved in cycles are appended at the end, sorted by createdAt. */ export function topologicalSortPhases( phases: T[], edges: DependencyEdge[], ): T[] { if (phases.length === 0) return []; const phaseMap = new Map(phases.map((p) => [p.id, p])); const inDegree = new Map(); const adjacency = new Map(); // dependsOnPhaseId -> phaseIds that depend on it for (const p of phases) { inDegree.set(p.id, 0); adjacency.set(p.id, []); } for (const edge of edges) { if (!phaseMap.has(edge.phaseId) || !phaseMap.has(edge.dependsOnPhaseId)) continue; inDegree.set(edge.phaseId, (inDegree.get(edge.phaseId) ?? 0) + 1); adjacency.get(edge.dependsOnPhaseId)!.push(edge.phaseId); } // Seed queue with zero in-degree phases, sorted by createdAt const queue = phases .filter((p) => (inDegree.get(p.id) ?? 0) === 0) .sort((a, b) => toTime(a.createdAt) - toTime(b.createdAt)); const result: T[] = []; while (queue.length > 0) { // Pick the earliest-created from the queue queue.sort((a, b) => toTime(a.createdAt) - toTime(b.createdAt)); const current = queue.shift()!; result.push(current); for (const neighborId of adjacency.get(current.id) ?? []) { const newDeg = (inDegree.get(neighborId) ?? 1) - 1; inDegree.set(neighborId, newDeg); if (newDeg === 0) { queue.push(phaseMap.get(neighborId)!); } } } // Append any phases caught in cycles, sorted by createdAt if (result.length < phases.length) { const inResult = new Set(result.map((p) => p.id)); const remaining = phases .filter((p) => !inResult.has(p.id)) .sort((a, b) => toTime(a.createdAt) - toTime(b.createdAt)); result.push(...remaining); } return result; } function toTime(d: Date | string): number { return typeof d === 'string' ? new Date(d).getTime() : d.getTime(); } // --------------------------------------------------------------------------- // Pipeline Column Grouping // --------------------------------------------------------------------------- export interface PipelineColumn { depth: number; phases: T[]; } /** * Groups phases into pipeline columns by dependency depth. * Depth 0 = no dependencies, depth N = longest path from a root is N. * Phases within a column are sorted by createdAt ascending. * Phases in cycles are placed in the last column + 1. */ export function groupPhasesByDependencyLevel( phases: T[], edges: DependencyEdge[], ): PipelineColumn[] { if (phases.length === 0) return []; const phaseIds = new Set(phases.map((p) => p.id)); // Build adjacency: for each phase, what does it depend on? const dependsOn = new Map(); for (const p of phases) dependsOn.set(p.id, []); for (const edge of edges) { if (!phaseIds.has(edge.phaseId) || !phaseIds.has(edge.dependsOnPhaseId)) continue; dependsOn.get(edge.phaseId)!.push(edge.dependsOnPhaseId); } // Compute depth via recursive longest path with cycle detection const depthCache = new Map(); const visiting = new Set(); function computeDepth(id: string): number { if (depthCache.has(id)) return depthCache.get(id)!; if (visiting.has(id)) return -1; // cycle detected visiting.add(id); const deps = dependsOn.get(id) ?? []; let maxDepth = 0; for (const depId of deps) { const d = computeDepth(depId); if (d === -1) { visiting.delete(id); return -1; } maxDepth = Math.max(maxDepth, d + 1); } visiting.delete(id); depthCache.set(id, maxDepth); return maxDepth; } // Compute depths let maxValidDepth = 0; for (const p of phases) { const d = computeDepth(p.id); if (d >= 0) maxValidDepth = Math.max(maxValidDepth, d); } // Group by depth const columnMap = new Map(); for (const p of phases) { const d = depthCache.get(p.id); const depth = d != null && d >= 0 ? d : maxValidDepth + 1; // cycles go last if (!columnMap.has(depth)) columnMap.set(depth, []); columnMap.get(depth)!.push(p); } // Sort phases within each column by createdAt const columns: PipelineColumn[] = []; const sortedDepths = [...columnMap.keys()].sort((a, b) => a - b); for (const depth of sortedDepths) { const phasesInCol = columnMap.get(depth)!; phasesInCol.sort((a, b) => toTime(a.createdAt) - toTime(b.createdAt)); columns.push({ depth, phases: phasesInCol }); } return columns; }