findGraphRoots.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const NO_MARKER = 0;
  7. const IN_PROGRESS_MARKER = 1;
  8. const DONE_MARKER = 2;
  9. const CANDIDATE_MARKER = 3;
  10. /**
  11. * @template T
  12. * @typedef {Set<Node<T>>} Nodes
  13. */
  14. /**
  15. * @template T
  16. */
  17. class Node {
  18. /**
  19. * @param {T} item the value of the node
  20. */
  21. constructor(item) {
  22. this.item = item;
  23. /** @type {Nodes<T>} */
  24. this.dependencies = new Set();
  25. /** @type {SCC<T>} */
  26. this.scc = new SCC();
  27. // Each node starts as a single-node SCC
  28. this.scc.nodes.add(this);
  29. /** @type {number} */
  30. this.incoming = 0;
  31. }
  32. }
  33. /**
  34. * SCC (strongly connected component)
  35. * @template T
  36. */
  37. class SCC {
  38. constructor() {
  39. /** @type {Nodes<T>} */
  40. this.nodes = new Set();
  41. this.marker = NO_MARKER;
  42. }
  43. }
  44. /**
  45. * @template T
  46. * @typedef {object} StackEntry
  47. * @property {Node<T>} node
  48. * @property {Node<T>[]} openEdges
  49. */
  50. /**
  51. * @template T
  52. * @param {Iterable<T>} items list of items
  53. * @param {(item: T) => Iterable<T>} getDependencies function to get dependencies of an item (items that are not in list are ignored)
  54. * @returns {Iterable<T>} graph roots of the items
  55. */
  56. module.exports = (items, getDependencies) => {
  57. /** @type {Map<T, Node<T>>} */
  58. const itemToNode = new Map();
  59. for (const item of items) {
  60. const node = new Node(item);
  61. itemToNode.set(item, node);
  62. }
  63. // Early exit when there is only one node
  64. if (itemToNode.size <= 1) return items;
  65. // Build graph edges
  66. for (const node of itemToNode.values()) {
  67. for (const dep of getDependencies(node.item)) {
  68. const depNode = itemToNode.get(dep);
  69. if (depNode !== undefined) {
  70. node.dependencies.add(depNode);
  71. }
  72. }
  73. }
  74. // All candidate root SCCs, they will be removed once an incoming edge is found
  75. /** @type {Set<SCC<T>>} */
  76. const rootSCCs = new Set();
  77. for (const selectedNode of itemToNode.values()) {
  78. // DFS walk only once per unseen SCC
  79. if (selectedNode.scc.marker === NO_MARKER) {
  80. selectedNode.scc.marker = IN_PROGRESS_MARKER;
  81. // Keep a stack to avoid recursive walk
  82. /** @type {StackEntry<T>[]} */
  83. const stack = [
  84. {
  85. node: selectedNode,
  86. openEdges: [...selectedNode.dependencies]
  87. }
  88. ];
  89. while (stack.length > 0) {
  90. const topOfStack = stack[stack.length - 1];
  91. // Process one unvisited outgoing edge if available
  92. if (topOfStack.openEdges.length > 0) {
  93. const dependency =
  94. /** @type {Node<T>} */
  95. (topOfStack.openEdges.pop());
  96. const depSCC = dependency.scc;
  97. switch (depSCC.marker) {
  98. case NO_MARKER:
  99. // First time we see this SCC: enter it
  100. stack.push({
  101. node: dependency,
  102. openEdges: [...dependency.dependencies]
  103. });
  104. depSCC.marker = IN_PROGRESS_MARKER;
  105. break;
  106. case IN_PROGRESS_MARKER: {
  107. // Back-edge to an SCC that is still on the stack
  108. // Example:
  109. // A -> B -> C -> D
  110. // ^ |
  111. // |_________|
  112. // If we are at `D` and traverse `D` -> `B`, then `B/C/D` must be in one SCC
  113. /** @type {Set<SCC<T>>} */
  114. const sccsToMerge = new Set();
  115. for (
  116. let i = stack.length - 1;
  117. stack[i].node.scc !== depSCC;
  118. i--
  119. ) {
  120. sccsToMerge.add(stack[i].node.scc);
  121. }
  122. for (const sccToMerge of sccsToMerge) {
  123. for (const nodeInMergedSCC of sccToMerge.nodes) {
  124. nodeInMergedSCC.scc = depSCC;
  125. depSCC.nodes.add(nodeInMergedSCC);
  126. }
  127. }
  128. break;
  129. }
  130. case CANDIDATE_MARKER:
  131. // This finished SCC was previously considered as root SCC
  132. // We just found a new incoming edge, so it is no longer a candidate
  133. rootSCCs.delete(/** @type {SCC<T>} */ (depSCC));
  134. depSCC.marker = DONE_MARKER;
  135. break;
  136. case DONE_MARKER:
  137. // Already finalized and not a candidate
  138. break;
  139. }
  140. } else {
  141. // All dependencies of the current node have been processed
  142. // So we leave the node
  143. stack.pop();
  144. // Mark an SCC as DONE only when the popped node is the last
  145. // node from that SCC remaining on the current stack.
  146. // A -> B -> C -> D
  147. // ^ |
  148. // |_________|
  149. // If `B` is popped and the new stack top is `A`, they are in
  150. // different SCCs, so B's SCC can be finalized.
  151. if (
  152. stack.length &&
  153. topOfStack.node.scc !== stack[stack.length - 1].node.scc
  154. ) {
  155. topOfStack.node.scc.marker = DONE_MARKER;
  156. }
  157. }
  158. }
  159. const scc = selectedNode.scc;
  160. // This SCC is complete and currently has no known incoming edge
  161. scc.marker = CANDIDATE_MARKER;
  162. rootSCCs.add(scc);
  163. }
  164. }
  165. /** @type {Set<T>} */
  166. const rootNodes = new Set();
  167. // For each root SCC, we select node with the most incoming edges
  168. // from within the same SCC
  169. for (const scc of rootSCCs) {
  170. let max = 0;
  171. /** @type {Nodes<T>} */
  172. const nodes = new Set(scc.nodes);
  173. for (const node of scc.nodes) {
  174. for (const dep of node.dependencies) {
  175. if (scc.nodes.has(dep)) {
  176. dep.incoming++;
  177. if (dep.incoming < max) continue;
  178. if (dep.incoming > max) {
  179. nodes.clear();
  180. max = dep.incoming;
  181. }
  182. nodes.add(dep);
  183. }
  184. }
  185. }
  186. for (const node of nodes) {
  187. rootNodes.add(node.item);
  188. }
  189. }
  190. // When root nodes were found, return them
  191. if (rootNodes.size > 0) return rootNodes;
  192. throw new Error("Implementation of findGraphRoots is broken");
  193. };