fork of hey-api/openapi-ts because I need some additional things
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};