UNPKG

@sha1n/dagraph

Version:

Directed acyclic graph utility in TypeScript

198 lines (197 loc) 6.66 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createDAG = void 0; class Node { constructor(data, dependencies = new Set()) { this.data = data; this.dependencies = dependencies; } get id() { return this.data.id; } } class DAGraph { constructor() { this.nodesById = new Map(); } /** * Adds the specified identifiable node to the graph. */ addNode(data) { this.ensureNode(data); return this; } /** * @returns the data node identified by the specified id if found, else returns undefined. */ getNode(id) { var _a; return (_a = this.nodesById.get(id)) === null || _a === void 0 ? void 0 : _a.data; } /** * Adds an edge pointing from 'from' to 'to'. */ addEdge(from, to) { const fromNode = this.ensureNode(from); const toNode = this.ensureNode(to); toNode.dependencies.add(fromNode.id); if (!this.isAcyclic()) { throw new Error(`[${from.id}] -> [${to.id}] form a cycle`); } return this; } /** * Returns a generator that returns all the nodes in topological order. * Implements a depth-first-search algorithm. */ *topologicalSort() { const nodesById = this.nodesById; const visited = new Set(); const dependenciesOf = function* (node) { for (const child of node.dependencies || []) { if (!visited.has(child)) { yield* dependenciesOf(nodesById.get(child)); yield nodesById.get(child).data; visited.add(child); } } }; for (const node of nodesById.values()) { if (!visited.has(node.id)) { yield* dependenciesOf(node); yield node.data; visited.add(node.id); } } } /** * A generator that returns the traverse roots of this graph. */ *roots() { for (const node of this.nodesById.values()) { if (node.dependencies.size === 0) { yield node.data; } } } /** * A generator that returns all the nodes in the this graph. */ *nodes() { for (const node of this.nodesById.values()) { yield node.data; } } /** * Returns a graph with the same edges pointing in the opposite direction. * * @returns a DAGraph */ reverse() { const reverseGraph = new DAGraph(); for (const node of this.nodesById.values()) { reverseGraph.addNode(node.data); } for (const node of this.nodesById.values()) { for (const dependencyId of node.dependencies) { const dependencyNode = reverseGraph.nodesById.get(dependencyId); dependencyNode.dependencies.add(node.id); } } return reverseGraph; } /** * Traverses the graph in depth-first order and calls the visitor function for each node. * Siblings (nodes sharing the same parent) are visited in the order they were added`. * * Note: This traversal behaves like a tree expansion. If a node is reachable via multiple paths * (e.g., a "diamond" structure), it will be visited multiple times—once for each path reaching it. * * * * @param visitor the visitor function to call for each node. * @param context the context object to pass to the visitor. */ traverse(visitor, context) { const outgoing = new Map(); for (const node of this.nodesById.values()) { for (const depId of node.dependencies) { let children = outgoing.get(depId); if (!children) { children = []; outgoing.set(depId, children); } children.push(node.id); } } const visitNode = (nodeId, parent, depth, index, total) => { const node = this.nodesById.get(nodeId); if (!node) { return; } visitor(node.data, { parent, depth, index, total }, context); const children = outgoing.get(nodeId) || []; children.forEach((childId, i) => { visitNode(childId, node.data, depth + 1, i, children.length); }); }; const roots = [...this.roots()]; roots.forEach((root, i) => { visitNode(root.id, null, 0, i, roots.length); }); } ensureNode(data) { let node = this.nodesById.get(data.id); if (node) { return node; } node = new Node(data); this.nodesById.set(data.id, node); return node; } isAcyclic() { const degrees = new Map(); this.nodesById.forEach(node => degrees.set(node.id, 0)); this.nodesById.forEach(node => node.dependencies.forEach(child => { degrees.set(child, degrees.get(child) + 1); })); const queue = new Array(); this.nodesById.forEach(node => { if (degrees.get(node.id) === 0) { queue.push(node.id); } }); let visitedNodeCount = 0; while (queue.length > 0) { const [nodeId] = queue.splice(0, 1); visitedNodeCount += 1; this.nodesById.get(nodeId).dependencies.forEach(child => { degrees.set(child, degrees.get(child) - 1); if (degrees.get(child) === 0) { queue.push(child); } }); } return visitedNodeCount === this.nodesById.size; } } function createDAG() { return new DAGraph(); } exports.createDAG = createDAG; __exportStar(require("./lib/formatVisitors"), exports); exports.default = createDAG;