findGraphRoots.js 5.6 KB

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