@sha1n/dagraph
Version:
Directed acyclic graph utility in TypeScript
142 lines (141 loc) • 4.27 kB
JavaScript
;
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 dependency of node.dependencies) {
const depData = this.nodesById.get(dependency).data;
reverseGraph.addNode(depData);
reverseGraph.addEdge(node.data, depData);
}
}
return reverseGraph;
}
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;
exports.default = createDAG;