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