229 lines
5.9 KiB
TypeScript
229 lines
5.9 KiB
TypeScript
|
export default class DepGraph<T> {
|
||
|
// node: incoming and outgoing edges
|
||
|
_graph = new Map<T, { incoming: Set<T>; outgoing: Set<T> }>()
|
||
|
|
||
|
constructor() {
|
||
|
this._graph = new Map()
|
||
|
}
|
||
|
|
||
|
export(): Object {
|
||
|
return {
|
||
|
nodes: this.nodes,
|
||
|
edges: this.edges,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
toString(): string {
|
||
|
return JSON.stringify(this.export(), null, 2)
|
||
|
}
|
||
|
|
||
|
// BASIC GRAPH OPERATIONS
|
||
|
|
||
|
get nodes(): T[] {
|
||
|
return Array.from(this._graph.keys())
|
||
|
}
|
||
|
|
||
|
get edges(): [T, T][] {
|
||
|
let edges: [T, T][] = []
|
||
|
this.forEachEdge((edge) => edges.push(edge))
|
||
|
return edges
|
||
|
}
|
||
|
|
||
|
hasNode(node: T): boolean {
|
||
|
return this._graph.has(node)
|
||
|
}
|
||
|
|
||
|
addNode(node: T): void {
|
||
|
if (!this._graph.has(node)) {
|
||
|
this._graph.set(node, { incoming: new Set(), outgoing: new Set() })
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove node and all edges connected to it
|
||
|
removeNode(node: T): void {
|
||
|
if (this._graph.has(node)) {
|
||
|
// first remove all edges so other nodes don't have references to this node
|
||
|
for (const target of this._graph.get(node)!.outgoing) {
|
||
|
this.removeEdge(node, target)
|
||
|
}
|
||
|
for (const source of this._graph.get(node)!.incoming) {
|
||
|
this.removeEdge(source, node)
|
||
|
}
|
||
|
this._graph.delete(node)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
forEachNode(callback: (node: T) => void): void {
|
||
|
for (const node of this._graph.keys()) {
|
||
|
callback(node)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
hasEdge(from: T, to: T): boolean {
|
||
|
return Boolean(this._graph.get(from)?.outgoing.has(to))
|
||
|
}
|
||
|
|
||
|
addEdge(from: T, to: T): void {
|
||
|
this.addNode(from)
|
||
|
this.addNode(to)
|
||
|
|
||
|
this._graph.get(from)!.outgoing.add(to)
|
||
|
this._graph.get(to)!.incoming.add(from)
|
||
|
}
|
||
|
|
||
|
removeEdge(from: T, to: T): void {
|
||
|
if (this._graph.has(from) && this._graph.has(to)) {
|
||
|
this._graph.get(from)!.outgoing.delete(to)
|
||
|
this._graph.get(to)!.incoming.delete(from)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// returns -1 if node does not exist
|
||
|
outDegree(node: T): number {
|
||
|
return this.hasNode(node) ? this._graph.get(node)!.outgoing.size : -1
|
||
|
}
|
||
|
|
||
|
// returns -1 if node does not exist
|
||
|
inDegree(node: T): number {
|
||
|
return this.hasNode(node) ? this._graph.get(node)!.incoming.size : -1
|
||
|
}
|
||
|
|
||
|
forEachOutNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||
|
this._graph.get(node)?.outgoing.forEach(callback)
|
||
|
}
|
||
|
|
||
|
forEachInNeighbor(node: T, callback: (neighbor: T) => void): void {
|
||
|
this._graph.get(node)?.incoming.forEach(callback)
|
||
|
}
|
||
|
|
||
|
forEachEdge(callback: (edge: [T, T]) => void): void {
|
||
|
for (const [source, { outgoing }] of this._graph.entries()) {
|
||
|
for (const target of outgoing) {
|
||
|
callback([source, target])
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DEPENDENCY ALGORITHMS
|
||
|
|
||
|
// Add all nodes and edges from other graph to this graph
|
||
|
mergeGraph(other: DepGraph<T>): void {
|
||
|
other.forEachEdge(([source, target]) => {
|
||
|
this.addNode(source)
|
||
|
this.addNode(target)
|
||
|
this.addEdge(source, target)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// For the node provided:
|
||
|
// If node does not exist, add it
|
||
|
// If an incoming edge was added in other, it is added in this graph
|
||
|
// If an incoming edge was deleted in other, it is deleted in this graph
|
||
|
updateIncomingEdgesForNode(other: DepGraph<T>, node: T): void {
|
||
|
this.addNode(node)
|
||
|
|
||
|
// Add edge if it is present in other
|
||
|
other.forEachInNeighbor(node, (neighbor) => {
|
||
|
this.addEdge(neighbor, node)
|
||
|
})
|
||
|
|
||
|
// For node provided, remove incoming edge if it is absent in other
|
||
|
this.forEachEdge(([source, target]) => {
|
||
|
if (target === node && !other.hasEdge(source, target)) {
|
||
|
this.removeEdge(source, target)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Remove all nodes that do not have any incoming or outgoing edges
|
||
|
// A node may be orphaned if the only node pointing to it was removed
|
||
|
removeOrphanNodes(): Set<T> {
|
||
|
let orphanNodes = new Set<T>()
|
||
|
|
||
|
this.forEachNode((node) => {
|
||
|
if (this.inDegree(node) === 0 && this.outDegree(node) === 0) {
|
||
|
orphanNodes.add(node)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
orphanNodes.forEach((node) => {
|
||
|
this.removeNode(node)
|
||
|
})
|
||
|
|
||
|
return orphanNodes
|
||
|
}
|
||
|
|
||
|
// Get all leaf nodes (i.e. destination paths) reachable from the node provided
|
||
|
// Eg. if the graph is A -> B -> C
|
||
|
// D ---^
|
||
|
// and the node is B, this function returns [C]
|
||
|
getLeafNodes(node: T): Set<T> {
|
||
|
let stack: T[] = [node]
|
||
|
let visited = new Set<T>()
|
||
|
let leafNodes = new Set<T>()
|
||
|
|
||
|
// DFS
|
||
|
while (stack.length > 0) {
|
||
|
let node = stack.pop()!
|
||
|
|
||
|
// If the node is already visited, skip it
|
||
|
if (visited.has(node)) {
|
||
|
continue
|
||
|
}
|
||
|
visited.add(node)
|
||
|
|
||
|
// Check if the node is a leaf node (i.e. destination path)
|
||
|
if (this.outDegree(node) === 0) {
|
||
|
leafNodes.add(node)
|
||
|
}
|
||
|
|
||
|
// Add all unvisited neighbors to the stack
|
||
|
this.forEachOutNeighbor(node, (neighbor) => {
|
||
|
if (!visited.has(neighbor)) {
|
||
|
stack.push(neighbor)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return leafNodes
|
||
|
}
|
||
|
|
||
|
// Get all ancestors of the leaf nodes reachable from the node provided
|
||
|
// Eg. if the graph is A -> B -> C
|
||
|
// D ---^
|
||
|
// and the node is B, this function returns [A, B, D]
|
||
|
getLeafNodeAncestors(node: T): Set<T> {
|
||
|
const leafNodes = this.getLeafNodes(node)
|
||
|
let visited = new Set<T>()
|
||
|
let upstreamNodes = new Set<T>()
|
||
|
|
||
|
// Backwards DFS for each leaf node
|
||
|
leafNodes.forEach((leafNode) => {
|
||
|
let stack: T[] = [leafNode]
|
||
|
|
||
|
while (stack.length > 0) {
|
||
|
let node = stack.pop()!
|
||
|
|
||
|
if (visited.has(node)) {
|
||
|
continue
|
||
|
}
|
||
|
visited.add(node)
|
||
|
// Add node if it's not a leaf node (i.e. destination path)
|
||
|
// Assumes destination file cannot depend on another destination file
|
||
|
if (this.outDegree(node) !== 0) {
|
||
|
upstreamNodes.add(node)
|
||
|
}
|
||
|
|
||
|
// Add all unvisited parents to the stack
|
||
|
this.forEachInNeighbor(node, (parentNode) => {
|
||
|
if (!visited.has(parentNode)) {
|
||
|
stack.push(parentNode)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return upstreamNodes
|
||
|
}
|
||
|
}
|