UNPKG

topological-sort-group

Version:

Topological sorting and cycle detection. Optional grouping for parallel processing

234 lines (233 loc) 8.32 kB
import get from './deepGet.js'; import { SortMode } from './types.js'; let Graph = class Graph { // Create graph from DependencyGraph format static from(input, options) { const graph = new Graph(options); // Add all nodes first for(const id in input.nodes){ graph.addNode(id, input.nodes[id]); } // Add dependencies (which become edges in the internal representation) for(const dependent in input.dependencies){ const deps = input.dependencies[dependent]; for(let i = 0; i < deps.length; i++){ graph.addDependency(dependent, deps[i]); } } return graph; } // Export to DependencyGraph format toGraph() { const nodes = {}; const dependencies = {}; // Build nodes map for(const key in this.nodeMap){ nodes[key] = this.nodeMap[key].value; dependencies[key] = []; } // Build dependencies by reversing edges // edges[a] contains b means a -> b (b depends on a) // So we need to add a to dependencies[b] for(const from in this.nodeMap){ const edges = this.nodeMap[from].edges; for(let i = 0; i < edges.length; i++){ const to = edges[i]; if (dependencies[to]) { dependencies[to].push(from); } } } return { nodes, dependencies }; } key(keyOrValue) { if (this.path) return typeof keyOrValue === 'object' ? get(keyOrValue, this.path) : keyOrValue; return keyOrValue; } keys() { const keys = []; for(const key in this.nodeMap)keys.push(key); return keys; } value(key) { if (this.nodeMap[key] === undefined) { throw new Error(`Node with key '${String(key)}' does not exist in graph`); } return this.nodeMap[key].value; } edges(key) { if (this.nodeMap[key] === undefined) { throw new Error(`Node with key '${String(key)}' does not exist in graph`); } return this.nodeMap[key].edges; } addNode(idOrValue, value) { // Determine if called with (value) or (id, value) let id; let nodeValue; if (value === undefined) { // Called as addNode(value) - extract key from path nodeValue = idOrValue; if (nodeValue === null || nodeValue === undefined) { throw new Error('Cannot add null or undefined to graph'); } id = this.key(nodeValue); if (this.path && id === undefined) { throw new Error(`Node is missing required path '${this.path}'`); } } else { // Called as addNode(id, value) id = idOrValue; nodeValue = value; if (id === null || id === undefined) { throw new Error('Cannot add null or undefined id to graph'); } if (nodeValue === null || nodeValue === undefined) { throw new Error('Cannot add null or undefined value to graph'); } } if (this.nodeMap[id] === undefined) { this.nodeMap[id] = { value: nodeValue, edges: [] }; this.size++; } else if (this.nodeMap[id].value !== nodeValue) { // Track duplicate instead of throwing if (this.duplicateMap[id] === undefined) { this.duplicateMap[id] = [ this.nodeMap[id].value ]; } this.duplicateMap[id].push(nodeValue); } } // Add a dependency: dependent depends on dependency // This creates an edge from dependency -> dependent (dependency must come before dependent) addDependency(dependent, dependency) { if (dependent === null || dependent === undefined) { throw new Error('Cannot add null or undefined dependent to graph'); } if (dependency === null || dependency === undefined) { throw new Error('Cannot add null or undefined dependency to graph'); } // Ensure both nodes exist (create with key as value if not using path) if (this.nodeMap[dependency] === undefined) { this.nodeMap[dependency] = { value: dependency, edges: [] }; this.size++; } if (this.nodeMap[dependent] === undefined) { this.nodeMap[dependent] = { value: dependent, edges: [] }; this.size++; } // Add edge: dependency -> dependent (dependency unlocks dependent) this.nodeMap[dependency].edges.push(dependent); } degrees() { const degrees = {}; for(const from in this.nodeMap){ if (degrees[from] === undefined) degrees[from] = 0; this.nodeMap[from].edges.forEach((key)=>{ if (degrees[key] === undefined) degrees[key] = 0; degrees[key]++; }); } return degrees; } cycles() { const visited = {}; const marks = {}; const cycles = []; const visit = (key, ancestors)=>{ if (marks[key]) return cycles.push(ancestors.concat(key)); // found a cycle if (visited[key]) return; // already visited visited[key] = true; // check for cycles from this key marks[key] = true; this.edges(key).forEach((neighborKey)=>{ visit(neighborKey, ancestors.concat(key)); }); delete marks[key]; }; // check all keys let keys = this.keys(); while(keys.length > 0){ visit(keys[0], []); keys = keys.filter((key)=>!visited[key]); // remove processed } return cycles; } sort(mode = SortMode.Group) { // Validate sort mode if (mode !== SortMode.Group && mode !== SortMode.Flat) { throw new Error(`Invalid sort mode: ${mode}. Use SortMode.Group (${SortMode.Group}) or SortMode.Flat (${SortMode.Flat})`); } const degrees = this.degrees(); // Initialize queue with no links const queue = []; for(const key in degrees){ if (degrees[key] === 0) queue.push({ key, level: 0 }); } const nodes = []; const groups = []; // process each level let processed = 0; let level = 0; while(queue.length > 0){ const queued = queue.shift(); // If we're moving to a new level, store the previous level's nodes if (mode === SortMode.Group && queued.level > level) { groups.push(nodes.splice(0, nodes.length)); level = queued.level; } nodes.push(this.value(queued.key)); processed++; // Process neighbors const neighbors = this.edges(queued.key); for(let i = 0; i < neighbors.length; i++){ const key = neighbors[i]; if (--degrees[key] === 0) { queue.push({ key, level: level + 1 }); } } } // Add the last nodes if (mode === SortMode.Group && nodes.length > 0) groups.push(nodes); // Build duplicates array const duplicates = []; for(const key in this.duplicateMap){ duplicates.push({ key: key, values: this.duplicateMap[key] }); } return { nodes: mode === SortMode.Group ? groups : nodes, cycles: processed !== this.size ? this.cycles() : [], duplicates }; } constructor(options){ this.size = 0; this.path = options === null || options === void 0 ? void 0 : options.path; this.nodeMap = {}; this.duplicateMap = {}; } }; Graph.SortMode = SortMode; export { Graph as default };