fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 207 lines 6.7 kB view raw
1import { MinHeap } from '../utils/minHeap'; 2import type { GetPointerPriorityFn, WalkFn } from './types/walk'; 3 4/** 5 * Walk the nodes of the graph in declaration (insertion) order. 6 * This is a cheap alternative to `walkTopological` when dependency ordering 7 * is not required and the caller only wants nodes in the order they were 8 * added to the graph. 9 */ 10const walkDeclarations: WalkFn = (graph, callback, options) => { 11 const pointers = Array.from(graph.nodes.keys()); 12 13 if (options?.preferGroups && options.preferGroups.length > 0) { 14 // emit nodes that match each preferred group in order, preserving insertion order 15 const emitted = new Set<string>(); 16 if (options.matchPointerToGroup) { 17 for (const kind of options.preferGroups) { 18 for (const pointer of pointers) { 19 const result = options.matchPointerToGroup(pointer); 20 if (!result.matched) continue; 21 if (result.kind === kind) { 22 emitted.add(pointer); 23 callback(pointer, graph.nodes.get(pointer)!); 24 } 25 } 26 } 27 } 28 29 // emit anything not covered by the preferGroups (in declaration order) 30 for (const pointer of pointers) { 31 if (emitted.has(pointer)) continue; 32 callback(pointer, graph.nodes.get(pointer)!); 33 } 34 return; 35 } 36 37 // fallback: simple declaration order 38 for (const pointer of pointers) { 39 callback(pointer, graph.nodes.get(pointer)!); 40 } 41}; 42 43/** 44 * Walks the nodes of the graph in topological order (dependencies before dependents). 45 * Calls the callback for each node pointer in order. 46 * Nodes in cycles are grouped together and emitted in arbitrary order within the group. 47 * 48 * @param graph - The dependency graph 49 * @param callback - Function to call for each node pointer 50 */ 51const walkTopological: WalkFn = (graph, callback, options) => { 52 // stable Kahn's algorithm that respects declaration order as a tiebreaker. 53 const pointers = Array.from(graph.nodes.keys()); 54 // base insertion order 55 const baseIndex = new Map<string, number>(); 56 pointers.forEach((pointer, index) => baseIndex.set(pointer, index)); 57 58 // composite decl index: group priority then base insertion order 59 const declIndex = new Map<string, number>(); 60 for (const pointer of pointers) { 61 const priority = options?.getPointerPriority?.(pointer) ?? 10; 62 const composite = priority * 1_000_000 + (baseIndex.get(pointer) ?? 0); 63 declIndex.set(pointer, composite); 64 } 65 66 // build dependency sets for each pointer 67 const depsOf = new Map<string, Set<string>>(); 68 for (const pointer of pointers) { 69 const raw = graph.subtreeDependencies?.get(pointer) ?? new Set(); 70 const filtered = new Set<string>(); 71 for (const rawPointer of raw) { 72 if (rawPointer === pointer) continue; // ignore self-dependencies for ordering 73 if (graph.nodes.has(rawPointer)) { 74 filtered.add(rawPointer); 75 } 76 } 77 depsOf.set(pointer, filtered); 78 } 79 80 // build inDegree and dependents adjacency 81 const inDegree = new Map<string, number>(); 82 const dependents = new Map<string, Set<string>>(); 83 for (const pointer of pointers) { 84 inDegree.set(pointer, 0); 85 } 86 for (const [pointer, deps] of depsOf) { 87 inDegree.set(pointer, deps.size); 88 for (const d of deps) { 89 if (!dependents.has(d)) { 90 dependents.set(d, new Set()); 91 } 92 dependents.get(d)!.add(pointer); 93 } 94 } 95 96 // sort pointers by declaration order 97 const sortByDecl = (arr: Array<string>) => 98 arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!); 99 100 // initialize queue with zero-inDegree nodes in declaration order 101 // use min-heap prioritized by declaration index to avoid repeated full sorts 102 const heap = new MinHeap(declIndex); 103 for (const pointer of pointers) { 104 if ((inDegree.get(pointer) ?? 0) === 0) { 105 heap.push(pointer); 106 } 107 } 108 109 const emitted = new Set<string>(); 110 const order: Array<string> = []; 111 112 while (!heap.isEmpty()) { 113 const cur = heap.pop()!; 114 if (emitted.has(cur)) continue; 115 emitted.add(cur); 116 order.push(cur); 117 118 const deps = dependents.get(cur); 119 if (!deps) continue; 120 121 for (const dep of deps) { 122 const v = (inDegree.get(dep) ?? 0) - 1; 123 inDegree.set(dep, v); 124 if (v === 0) { 125 heap.push(dep); 126 } 127 } 128 } 129 130 // emit remaining nodes (cycles) in declaration order 131 const remaining = pointers.filter((pointer) => !emitted.has(pointer)); 132 sortByDecl(remaining); 133 for (const pointer of remaining) { 134 emitted.add(pointer); 135 order.push(pointer); 136 } 137 138 // prefer specified groups when safe 139 let finalOrder = order; 140 if (options?.preferGroups && options.preferGroups.length > 0) { 141 // build group priority map (lower = earlier) 142 const groupPriority = new Map<string, number>(); 143 for (let i = 0; i < options.preferGroups.length; i++) { 144 const k = options.preferGroups[i]; 145 if (k) { 146 groupPriority.set(k, i); 147 } 148 } 149 150 const getGroup: GetPointerPriorityFn = (pointer) => { 151 if (options.matchPointerToGroup) { 152 const result = options.matchPointerToGroup(pointer); 153 if (result.matched) { 154 return groupPriority.has(result.kind) 155 ? groupPriority.get(result.kind)! 156 : options.preferGroups!.length; 157 } 158 } 159 return options.preferGroups!.length; 160 }; 161 162 // proposed order: sort by (groupPriority, originalIndex) 163 const proposed = [...order].sort((a, b) => { 164 const ga = getGroup(a); 165 const gb = getGroup(b); 166 return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b); 167 }); 168 169 // build quick lookup of original index and proposed index 170 const proposedIndex = new Map<string, number>(); 171 for (let i = 0; i < proposed.length; i++) { 172 proposedIndex.set(proposed[i]!, i); 173 } 174 175 // only validate edges where group(dep) > group(node) 176 const violated = (() => { 177 for (const [node, deps] of depsOf) { 178 for (const dep of deps) { 179 const gDep = getGroup(dep); 180 const gNode = getGroup(node); 181 if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping 182 const pDep = proposedIndex.get(dep)!; 183 const pNode = proposedIndex.get(node)!; 184 if (pDep >= pNode) { 185 return true; 186 } 187 } 188 } 189 return false; 190 })(); 191 192 if (!violated) { 193 finalOrder = proposed; 194 } 195 } 196 197 for (const pointer of finalOrder) { 198 callback(pointer, graph.nodes.get(pointer)!); 199 } 200}; 201 202export const walk: WalkFn = (graph, callback, options) => { 203 if (options?.order === 'topological') { 204 return walkTopological(graph, callback, options); 205 } 206 return walkDeclarations(graph, callback, options); 207};