|
@@ -1,26 +1,86 @@
|
|
|
import { array, forward, object, partialCheck, pipe } from "valibot";
|
|
import { array, forward, object, partialCheck, pipe } from "valibot";
|
|
|
import Edge, { EdgeSchema } from "./Edge.js";
|
|
import Edge, { EdgeSchema } from "./Edge.js";
|
|
|
import Node, { NodeSchema } from "./Node.js";
|
|
import Node, { NodeSchema } from "./Node.js";
|
|
|
|
|
+import GraphError from "../errors/graph/GraphError.js";
|
|
|
|
|
+
|
|
|
|
|
+type GraphCache<TNode extends Node> = Partial<{
|
|
|
|
|
+ isAcyclic: boolean,
|
|
|
|
|
+ adjacencyList: Map<TNode["id"], Array<TNode["id"]>>
|
|
|
|
|
+}>
|
|
|
|
|
|
|
|
export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
- private readonly _adjacencyList: Map<TNode["id"], Array<TNode["id"]>>;
|
|
|
|
|
|
|
+ private _cache: GraphCache<TNode> = {};
|
|
|
|
|
+
|
|
|
private readonly _nodeMap: Map<TNode["id"], TNode>;
|
|
private readonly _nodeMap: Map<TNode["id"], TNode>;
|
|
|
private readonly _edgeMap: Map<TEdge["id"], TEdge>;
|
|
private readonly _edgeMap: Map<TEdge["id"], TEdge>;
|
|
|
|
|
|
|
|
public constructor(nodes: TNode[], edges: TEdge[]) {
|
|
public constructor(nodes: TNode[], edges: TEdge[]) {
|
|
|
- this._adjacencyList = new Map();
|
|
|
|
|
this._nodeMap = new Map();
|
|
this._nodeMap = new Map();
|
|
|
this._edgeMap = new Map();
|
|
this._edgeMap = new Map();
|
|
|
-
|
|
|
|
|
- for(const node of nodes) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ for(const node of nodes)
|
|
|
this._nodeMap.set(node.id, node);
|
|
this._nodeMap.set(node.id, node);
|
|
|
- this._adjacencyList.set(node.id, []);
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
for(const edge of edges)
|
|
for(const edge of edges)
|
|
|
this._edgeMap.set(edge.id, edge);
|
|
this._edgeMap.set(edge.id, edge);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private get adjacencyList(): Map<TNode["id"], Array<TNode["id"]>> {
|
|
|
|
|
+ const cache = this._cache.adjacencyList;
|
|
|
|
|
+
|
|
|
|
|
+ if(cache)
|
|
|
|
|
+ return cache;
|
|
|
|
|
+
|
|
|
|
|
+ const adjacencyList = new Map<TNode["id"], Array<TNode["id"]>>();
|
|
|
|
|
+
|
|
|
|
|
+ for(const [, edge] of this._edgeMap) {
|
|
|
|
|
+ const { from, to } = edge;
|
|
|
|
|
+ const adjacencyNodes = adjacencyList.get(from);
|
|
|
|
|
+ if(adjacencyNodes)
|
|
|
|
|
+ adjacencyNodes.push(to);
|
|
|
|
|
+ else
|
|
|
|
|
+ adjacencyList.set(from, [to]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return this._cache.adjacencyList = adjacencyList;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public get isAcyclic() : boolean {
|
|
|
|
|
+ const cache = this._cache.isAcyclic;
|
|
|
|
|
+
|
|
|
|
|
+ if(cache !== undefined) {
|
|
|
|
|
+ console.log('from cache')
|
|
|
|
|
+ return cache;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const queue: TNode["id"][] = [];
|
|
|
|
|
+ const nodeToInDegree = new Map<TNode["id"], number>();
|
|
|
|
|
+ const nodesCount = this._nodeMap.size;
|
|
|
|
|
+ let visitedNodesCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for(const { id } of this.getNodes())
|
|
|
|
|
+ nodeToInDegree.set(id, this.getNodeInputs(id).length);
|
|
|
|
|
+
|
|
|
|
|
+ for(const [nodeId, inDegree] of nodeToInDegree)
|
|
|
|
|
+ if(inDegree === 0)
|
|
|
|
|
+ queue.push(nodeId);
|
|
|
|
|
+
|
|
|
|
|
+ while(queue.length > 0) {
|
|
|
|
|
+ const nodeId = queue.pop()!;
|
|
|
|
|
+ visitedNodesCount++;
|
|
|
|
|
|
|
|
- this.updateAdjacencyList();
|
|
|
|
|
|
|
+ for(const { id: neighbourId } of this.getNodeOutputs(nodeId)) {
|
|
|
|
|
+ const inDegree = nodeToInDegree.get(neighbourId);
|
|
|
|
|
+ const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ nodeToInDegree.set(neighbourId, newInDegree);
|
|
|
|
|
+
|
|
|
|
|
+ if(newInDegree === 0)
|
|
|
|
|
+ queue.push(neighbourId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return this._cache.isAcyclic = visitedNodesCount !== nodesCount;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public getNodes() : TNode[] {
|
|
public getNodes() : TNode[] {
|
|
@@ -31,6 +91,47 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
return this._nodeMap.get(nodeId) || null;
|
|
return this._nodeMap.get(nodeId) || null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public addNode(node: TNode) : this {
|
|
|
|
|
+ const { id } = node;
|
|
|
|
|
+
|
|
|
|
|
+ if(this._nodeMap.has(id))
|
|
|
|
|
+ throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
|
|
|
|
|
+
|
|
|
|
|
+ this._nodeMap.set(id, node);
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public updateNode(nodeId: TNode["id"], node: TNode) : this {
|
|
|
|
|
+ const { id } = node;
|
|
|
|
|
+
|
|
|
|
|
+ if(!this._nodeMap.has(nodeId))
|
|
|
|
|
+ throw new GraphError(`Can't update node: node with id ${nodeId} doesn't exist`);
|
|
|
|
|
+
|
|
|
|
|
+ if(nodeId != id)
|
|
|
|
|
+ throw new GraphError(`Cant' update node: node id cannot be changed`);
|
|
|
|
|
+
|
|
|
|
|
+ this._nodeMap.set(nodeId, node);
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public removeNode(nodeId: TNode["id"]) : this {
|
|
|
|
|
+ if(!this._nodeMap.has(nodeId))
|
|
|
|
|
+ throw new GraphError(`Can't delete a node from graph: node with id ${nodeId} doesn't exists`);
|
|
|
|
|
+
|
|
|
|
|
+ for(const [edgeId, edge] of this._edgeMap)
|
|
|
|
|
+ if(edge.from == nodeId || edge.to == nodeId)
|
|
|
|
|
+ this._edgeMap.delete(edgeId);
|
|
|
|
|
+
|
|
|
|
|
+ this._nodeMap.delete(nodeId);
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public getEdges() : TEdge[] {
|
|
public getEdges() : TEdge[] {
|
|
|
return Array.from(this._edgeMap.values());
|
|
return Array.from(this._edgeMap.values());
|
|
|
}
|
|
}
|
|
@@ -38,17 +139,55 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
public getEdge(edgeId: TEdge["id"]): TEdge | null {
|
|
public getEdge(edgeId: TEdge["id"]): TEdge | null {
|
|
|
return this._edgeMap.get(edgeId) || null;
|
|
return this._edgeMap.get(edgeId) || null;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public updateEdgeById(id: TEdge["id"], edge: TEdge) : this {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ public addEdge(edge: TEdge) : this {
|
|
|
|
|
+ const { id, from, to } = edge;
|
|
|
|
|
+
|
|
|
|
|
+ if(this._edgeMap.has(id))
|
|
|
|
|
+ throw new GraphError(`Can't add a new edge to graph: edge with id ${id} already exists`);
|
|
|
|
|
+
|
|
|
|
|
+ if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
|
|
|
|
|
+ throw new GraphError(`Can't add a new edge to graph: edge references non-existing nodes`);
|
|
|
|
|
+
|
|
|
this._edgeMap.set(id, edge);
|
|
this._edgeMap.set(id, edge);
|
|
|
- this.updateAdjacencyList();
|
|
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public updateEdge(id: TEdge["id"], edge: TEdge) : this {
|
|
|
|
|
+ const { id: edgeId, from, to } = edge;
|
|
|
|
|
+
|
|
|
|
|
+ if(!this._edgeMap.has(id))
|
|
|
|
|
+ throw new Error(`Can't update edge: edge with id "${id}" does not exist`);
|
|
|
|
|
+
|
|
|
|
|
+ if(id !== edgeId)
|
|
|
|
|
+ throw new Error(`Can't update edge: edge id cannot be changed`);
|
|
|
|
|
+
|
|
|
|
|
+ if(!this._nodeMap.has(from) || !this._nodeMap.has(to))
|
|
|
|
|
+ throw new Error(`Can't update edge: edge references non-existing nodes`);
|
|
|
|
|
+
|
|
|
|
|
+ this._edgeMap.set(edgeId, edge);
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public removeEdge(edgeId: TEdge["id"]): this {
|
|
|
|
|
+ if(!this._edgeMap.has(edgeId))
|
|
|
|
|
+ return this;
|
|
|
|
|
+
|
|
|
|
|
+ this._edgeMap.delete(edgeId);
|
|
|
|
|
+ this.invalidateCache();
|
|
|
|
|
+
|
|
|
return this;
|
|
return this;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
|
|
public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
- const inputs: TNode[] = []
|
|
|
|
|
|
|
+ const inputs: TNode[] = [];
|
|
|
|
|
|
|
|
- for(const [id, outputs] of this._adjacencyList) {
|
|
|
|
|
|
|
+ for(const [id, outputs] of this.adjacencyList) {
|
|
|
if(!outputs.includes(nodeId))
|
|
if(!outputs.includes(nodeId))
|
|
|
continue;
|
|
continue;
|
|
|
|
|
|
|
@@ -63,7 +202,7 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
|
|
public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
- const outputIds = this._adjacencyList.get(nodeId);
|
|
|
|
|
|
|
+ const outputIds = this.adjacencyList.get(nodeId);
|
|
|
if(!outputIds)
|
|
if(!outputIds)
|
|
|
return [];
|
|
return [];
|
|
|
|
|
|
|
@@ -80,19 +219,8 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
return outputs;
|
|
return outputs;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private updateAdjacencyList() : this {
|
|
|
|
|
- this._adjacencyList.clear();
|
|
|
|
|
-
|
|
|
|
|
- for(const [, edge] of this._edgeMap) {
|
|
|
|
|
- const { from, to } = edge;
|
|
|
|
|
- const adjacencyNodes = this._adjacencyList.get(from);
|
|
|
|
|
- if(adjacencyNodes)
|
|
|
|
|
- adjacencyNodes.push(to);
|
|
|
|
|
- else
|
|
|
|
|
- this._adjacencyList.set(from, [to]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return this;
|
|
|
|
|
|
|
+ private invalidateCache(): void {
|
|
|
|
|
+ this._cache = {};
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|