| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- const NO_MARKER = 0;
- const IN_PROGRESS_MARKER = 1;
- const DONE_MARKER = 2;
- const CANDIDATE_MARKER = 3;
- /**
- * @template T
- * @typedef {Set<Node<T>>} Nodes
- */
- /**
- * @template T
- */
- class Node {
- /**
- * @param {T} item the value of the node
- */
- constructor(item) {
- this.item = item;
- /** @type {Nodes<T>} */
- this.dependencies = new Set();
- /** @type {SCC<T>} */
- this.scc = new SCC();
- // Each node starts as a single-node SCC
- this.scc.nodes.add(this);
- /** @type {number} */
- this.incoming = 0;
- }
- }
- /**
- * SCC (strongly connected component)
- * @template T
- */
- class SCC {
- constructor() {
- /** @type {Nodes<T>} */
- this.nodes = new Set();
- this.marker = NO_MARKER;
- }
- }
- /**
- * @template T
- * @typedef {object} StackEntry
- * @property {Node<T>} node
- * @property {Node<T>[]} openEdges
- */
- /**
- * @template T
- * @param {Iterable<T>} items list of items
- * @param {(item: T) => Iterable<T>} getDependencies function to get dependencies of an item (items that are not in list are ignored)
- * @returns {Iterable<T>} graph roots of the items
- */
- module.exports = (items, getDependencies) => {
- /** @type {Map<T, Node<T>>} */
- const itemToNode = new Map();
- for (const item of items) {
- const node = new Node(item);
- itemToNode.set(item, node);
- }
- // Early exit when there is only one node
- if (itemToNode.size <= 1) return items;
- // Build graph edges
- for (const node of itemToNode.values()) {
- for (const dep of getDependencies(node.item)) {
- const depNode = itemToNode.get(dep);
- if (depNode !== undefined) {
- node.dependencies.add(depNode);
- }
- }
- }
- // All candidate root SCCs, they will be removed once an incoming edge is found
- /** @type {Set<SCC<T>>} */
- const rootSCCs = new Set();
- for (const selectedNode of itemToNode.values()) {
- // DFS walk only once per unseen SCC
- if (selectedNode.scc.marker === NO_MARKER) {
- selectedNode.scc.marker = IN_PROGRESS_MARKER;
- // Keep a stack to avoid recursive walk
- /** @type {StackEntry<T>[]} */
- const stack = [
- {
- node: selectedNode,
- openEdges: [...selectedNode.dependencies]
- }
- ];
- while (stack.length > 0) {
- const topOfStack = stack[stack.length - 1];
- // Process one unvisited outgoing edge if available
- if (topOfStack.openEdges.length > 0) {
- const dependency =
- /** @type {Node<T>} */
- (topOfStack.openEdges.pop());
- const depSCC = dependency.scc;
- switch (depSCC.marker) {
- case NO_MARKER:
- // First time we see this SCC: enter it
- stack.push({
- node: dependency,
- openEdges: [...dependency.dependencies]
- });
- depSCC.marker = IN_PROGRESS_MARKER;
- break;
- case IN_PROGRESS_MARKER: {
- // Back-edge to an SCC that is still on the stack
- // Example:
- // A -> B -> C -> D
- // ^ |
- // |_________|
- // If we are at `D` and traverse `D` -> `B`, then `B/C/D` must be in one SCC
- /** @type {Set<SCC<T>>} */
- const sccsToMerge = new Set();
- for (
- let i = stack.length - 1;
- stack[i].node.scc !== depSCC;
- i--
- ) {
- sccsToMerge.add(stack[i].node.scc);
- }
- for (const sccToMerge of sccsToMerge) {
- for (const nodeInMergedSCC of sccToMerge.nodes) {
- nodeInMergedSCC.scc = depSCC;
- depSCC.nodes.add(nodeInMergedSCC);
- }
- }
- break;
- }
- case CANDIDATE_MARKER:
- // This finished SCC was previously considered as root SCC
- // We just found a new incoming edge, so it is no longer a candidate
- rootSCCs.delete(/** @type {SCC<T>} */ (depSCC));
- depSCC.marker = DONE_MARKER;
- break;
- case DONE_MARKER:
- // Already finalized and not a candidate
- break;
- }
- } else {
- // All dependencies of the current node have been processed
- // So we leave the node
- stack.pop();
- // Mark an SCC as DONE only when the popped node is the last
- // node from that SCC remaining on the current stack.
- // A -> B -> C -> D
- // ^ |
- // |_________|
- // If `B` is popped and the new stack top is `A`, they are in
- // different SCCs, so B's SCC can be finalized.
- if (
- stack.length &&
- topOfStack.node.scc !== stack[stack.length - 1].node.scc
- ) {
- topOfStack.node.scc.marker = DONE_MARKER;
- }
- }
- }
- const scc = selectedNode.scc;
- // This SCC is complete and currently has no known incoming edge
- scc.marker = CANDIDATE_MARKER;
- rootSCCs.add(scc);
- }
- }
- /** @type {Set<T>} */
- const rootNodes = new Set();
- // For each root SCC, we select node with the most incoming edges
- // from within the same SCC
- for (const scc of rootSCCs) {
- let max = 0;
- /** @type {Nodes<T>} */
- const nodes = new Set(scc.nodes);
- for (const node of scc.nodes) {
- for (const dep of node.dependencies) {
- if (scc.nodes.has(dep)) {
- dep.incoming++;
- if (dep.incoming < max) continue;
- if (dep.incoming > max) {
- nodes.clear();
- max = dep.incoming;
- }
- nodes.add(dep);
- }
- }
- }
- for (const node of nodes) {
- rootNodes.add(node.item);
- }
- }
- // When root nodes were found, return them
- if (rootNodes.size > 0) return rootNodes;
- throw new Error("Implementation of findGraphRoots is broken");
- };
|