|
@@ -3,129 +3,90 @@ import Edge, { EdgeSchema } from "./edge/Edge.js";
|
|
|
import Node, { NodeSchema } from "./node/Node.js";
|
|
import Node, { NodeSchema } from "./node/Node.js";
|
|
|
import GraphError from "../errors/graph/GraphError.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 _cache: GraphCache<TNode> = {};
|
|
|
|
|
|
|
+ private readonly _adjacencyList: Map<TNode, Array<TNode>> = new Map();
|
|
|
|
|
|
|
|
- private readonly _nodeMap: Map<TNode["id"], TNode>;
|
|
|
|
|
- private readonly _edgeMap: Map<TEdge["id"], TEdge>;
|
|
|
|
|
|
|
+ private readonly _nodeMap: Map<string, TNode>;
|
|
|
|
|
+ private readonly _edgeMap: Map<string, TEdge>;
|
|
|
|
|
|
|
|
public constructor(nodes: TNode[], edges: TEdge[]) {
|
|
public constructor(nodes: TNode[], edges: TEdge[]) {
|
|
|
this._nodeMap = new Map();
|
|
this._nodeMap = new Map();
|
|
|
- this._edgeMap = new Map();
|
|
|
|
|
-
|
|
|
|
|
- for(const node of nodes)
|
|
|
|
|
- this._nodeMap.set(node.id, node);
|
|
|
|
|
-
|
|
|
|
|
- for(const edge of edges)
|
|
|
|
|
- this._edgeMap.set(edge.id, edge);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private get adjacencyList(): Map<TNode["id"], Array<TNode["id"]>> {
|
|
|
|
|
- const cache = this._cache.adjacencyList;
|
|
|
|
|
|
|
+ nodes.forEach((node) => this.addNode(node));
|
|
|
|
|
|
|
|
- if(cache)
|
|
|
|
|
- return cache;
|
|
|
|
|
|
|
+ this._edgeMap = new Map();
|
|
|
|
|
+ edges.forEach((edge) => this.addEdge(edge));
|
|
|
|
|
|
|
|
- const adjacencyList = new Map<TNode["id"], Array<TNode["id"]>>();
|
|
|
|
|
|
|
+ for(const edge of edges) {
|
|
|
|
|
+ const from = edge.getFrom(), to = edge.getTo();
|
|
|
|
|
|
|
|
- for(const [, edge] of this._edgeMap) {
|
|
|
|
|
- const { from, to } = edge;
|
|
|
|
|
- const adjacencyNodes = adjacencyList.get(from);
|
|
|
|
|
|
|
+ const adjacencyNodes = this._adjacencyList.get(from);
|
|
|
if(adjacencyNodes)
|
|
if(adjacencyNodes)
|
|
|
adjacencyNodes.push(to);
|
|
adjacencyNodes.push(to);
|
|
|
else
|
|
else
|
|
|
- adjacencyList.set(from, [to]);
|
|
|
|
|
|
|
+ this._adjacencyList.set(from, [to]);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return this._cache.adjacencyList = adjacencyList;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public get isAcyclic() : boolean {
|
|
|
|
|
- const cache = this._cache.isAcyclic;
|
|
|
|
|
-
|
|
|
|
|
- if(cache !== undefined)
|
|
|
|
|
- return cache;
|
|
|
|
|
-
|
|
|
|
|
- const queue: TNode["id"][] = [];
|
|
|
|
|
- const nodeToInDegree = new Map<TNode["id"], number>();
|
|
|
|
|
|
|
+ public isAcyclic() : boolean {
|
|
|
|
|
+ const queue: TNode[] = [];
|
|
|
|
|
+ const nodeToInDegree = new Map<TNode, number>();
|
|
|
const nodesCount = this._nodeMap.size;
|
|
const nodesCount = this._nodeMap.size;
|
|
|
let visitedNodesCount = 0;
|
|
let visitedNodesCount = 0;
|
|
|
|
|
|
|
|
- for(const { id } of this.getNodes())
|
|
|
|
|
- nodeToInDegree.set(id, this.getNodeInputs(id).length);
|
|
|
|
|
|
|
+ for(const node of this.getNodes())
|
|
|
|
|
+ nodeToInDegree.set(node, this.getNodeInputs(node).length);
|
|
|
|
|
|
|
|
for(const [nodeId, inDegree] of nodeToInDegree)
|
|
for(const [nodeId, inDegree] of nodeToInDegree)
|
|
|
if(inDegree === 0)
|
|
if(inDegree === 0)
|
|
|
queue.push(nodeId);
|
|
queue.push(nodeId);
|
|
|
|
|
|
|
|
while(queue.length > 0) {
|
|
while(queue.length > 0) {
|
|
|
- const nodeId = queue.pop()!;
|
|
|
|
|
|
|
+ const node = queue.pop()!;
|
|
|
visitedNodesCount++;
|
|
visitedNodesCount++;
|
|
|
|
|
|
|
|
- for(const { id: neighbourId } of this.getNodeOutputs(nodeId)) {
|
|
|
|
|
- const inDegree = nodeToInDegree.get(neighbourId);
|
|
|
|
|
|
|
+ for(const output of this.getNodeOutputs(node)) {
|
|
|
|
|
+ const inDegree = nodeToInDegree.get(output);
|
|
|
const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
|
|
const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
|
|
|
|
|
|
|
|
- nodeToInDegree.set(neighbourId, newInDegree);
|
|
|
|
|
|
|
+ nodeToInDegree.set(output, newInDegree);
|
|
|
|
|
|
|
|
if(newInDegree === 0)
|
|
if(newInDegree === 0)
|
|
|
- queue.push(neighbourId);
|
|
|
|
|
|
|
+ queue.push(output);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return this._cache.isAcyclic = visitedNodesCount !== nodesCount;
|
|
|
|
|
|
|
+ return visitedNodesCount !== nodesCount;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public getNodes() : TNode[] {
|
|
public getNodes() : TNode[] {
|
|
|
- return Array.from(this._nodeMap.values());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public getNode(nodeId: TNode["id"]): TNode | null {
|
|
|
|
|
- return this._nodeMap.get(nodeId) || null;
|
|
|
|
|
|
|
+ return [...this._nodeMap.values()];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public addNode(node: TNode) : this {
|
|
public addNode(node: TNode) : this {
|
|
|
- const { id } = node;
|
|
|
|
|
|
|
+ const id = node.getId();
|
|
|
|
|
|
|
|
if(this._nodeMap.has(id))
|
|
if(this._nodeMap.has(id))
|
|
|
throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
|
|
throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
|
|
|
|
|
|
|
|
this._nodeMap.set(id, node);
|
|
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();
|
|
|
|
|
|
|
+ node.subscribe(() => this.onNodeChange(node));
|
|
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public removeNode(nodeId: TNode["id"]) : this {
|
|
|
|
|
|
|
+ public removeNode(nodeId: string) : this {
|
|
|
if(!this._nodeMap.has(nodeId))
|
|
if(!this._nodeMap.has(nodeId))
|
|
|
throw new GraphError(`Can't delete a node from graph: node with id ${nodeId} doesn't exists`);
|
|
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)
|
|
|
|
|
|
|
+ for(const [edgeId, edge] of this._edgeMap) {
|
|
|
|
|
+ const fromId = edge.getFrom().getId(), toId = edge.getTo().getId();
|
|
|
|
|
+
|
|
|
|
|
+ if(fromId == nodeId || toId == nodeId)
|
|
|
this._edgeMap.delete(edgeId);
|
|
this._edgeMap.delete(edgeId);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
this._nodeMap.delete(nodeId);
|
|
this._nodeMap.delete(nodeId);
|
|
|
- this.invalidateCache();
|
|
|
|
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
}
|
|
}
|
|
@@ -134,99 +95,78 @@ export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
return Array.from(this._edgeMap.values());
|
|
return Array.from(this._edgeMap.values());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public getEdge(edgeId: TEdge["id"]): TEdge | null {
|
|
|
|
|
|
|
+ public getEdge(edgeId: string): TEdge | null {
|
|
|
return this._edgeMap.get(edgeId) || null;
|
|
return this._edgeMap.get(edgeId) || null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public addEdge(edge: TEdge) : this {
|
|
public addEdge(edge: TEdge) : this {
|
|
|
- const { id, from, to } = edge;
|
|
|
|
|
|
|
+ const id = edge.getId(), from = edge.getFrom(), to = edge.getTo();
|
|
|
|
|
|
|
|
if(this._edgeMap.has(id))
|
|
if(this._edgeMap.has(id))
|
|
|
throw new GraphError(`Can't add a new edge to graph: edge with id ${id} already exists`);
|
|
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`);
|
|
|
|
|
|
|
+ if(!this._nodeMap.has(from.getId()) || !this._nodeMap.has(to.getId()))
|
|
|
|
|
+ throw new GraphError(`Can't add a new edge to graph: edge references to non-existing nodes`);
|
|
|
|
|
|
|
|
this._edgeMap.set(id, edge);
|
|
this._edgeMap.set(id, edge);
|
|
|
- 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();
|
|
|
|
|
|
|
+ edge.subscribe(() => this.onEdgeChange(edge));
|
|
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public removeEdge(edgeId: TEdge["id"]): this {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ public removeEdge(edgeId: string): this {
|
|
|
if(!this._edgeMap.has(edgeId))
|
|
if(!this._edgeMap.has(edgeId))
|
|
|
return this;
|
|
return this;
|
|
|
|
|
|
|
|
this._edgeMap.delete(edgeId);
|
|
this._edgeMap.delete(edgeId);
|
|
|
- this.invalidateCache();
|
|
|
|
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
|
|
|
|
+ public getNodeInputs(node: TNode) : TNode[] {
|
|
|
const inputs: TNode[] = [];
|
|
const inputs: TNode[] = [];
|
|
|
|
|
|
|
|
- for(const [id, outputs] of this.adjacencyList) {
|
|
|
|
|
- if(!outputs.includes(nodeId))
|
|
|
|
|
- continue;
|
|
|
|
|
-
|
|
|
|
|
- const input = this._nodeMap.get(id);
|
|
|
|
|
- if(!input)
|
|
|
|
|
|
|
+ for(const [input, outputs] of this._adjacencyList) {
|
|
|
|
|
+ if(!outputs.includes(node))
|
|
|
continue;
|
|
continue;
|
|
|
-
|
|
|
|
|
inputs.push(input);
|
|
inputs.push(input);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return inputs;
|
|
return inputs;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
|
|
- const outputIds = this.adjacencyList.get(nodeId);
|
|
|
|
|
- if(!outputIds)
|
|
|
|
|
- return [];
|
|
|
|
|
-
|
|
|
|
|
- const outputs: TNode[] = [];
|
|
|
|
|
-
|
|
|
|
|
- outputIds.forEach((id) => {
|
|
|
|
|
- const node = this._nodeMap.get(id);
|
|
|
|
|
- if(!node)
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ public getNodeOutputs(node: TNode) : TNode[] {
|
|
|
|
|
+ return this._adjacencyList.get(node) || [];
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- outputs.push(node);
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ public isSourceNode(node: TNode) : boolean {
|
|
|
|
|
+ return this.getNodeInputs(node).length === 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return outputs;
|
|
|
|
|
|
|
+ public isSinkNode(node: TNode) : boolean {
|
|
|
|
|
+ return this.getNodeOutputs(node).length === 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public isSourceNode(nodeId: TNode["id"]) : boolean {
|
|
|
|
|
- return this.getNodeInputs(nodeId).length === 0;
|
|
|
|
|
|
|
+ protected onNodeChange(node: TNode) : void {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public isSinkNode(nodeId: TNode["id"]) : boolean {
|
|
|
|
|
- return this.getNodeOutputs(nodeId).length === 0;
|
|
|
|
|
|
|
+ protected onEdgeChange(edge: TEdge) : void {
|
|
|
|
|
+ this.buildAdjacencyList();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private invalidateCache(): void {
|
|
|
|
|
- this._cache = {};
|
|
|
|
|
|
|
+ private buildAdjacencyList() : void {
|
|
|
|
|
+ this._adjacencyList.clear();
|
|
|
|
|
+
|
|
|
|
|
+ for(const [, edge] of this._edgeMap) {
|
|
|
|
|
+ const from = edge.getFrom(), to = edge.getTo();
|
|
|
|
|
+
|
|
|
|
|
+ const adjacencyNodes = this._adjacencyList.get(from);
|
|
|
|
|
+ if(adjacencyNodes)
|
|
|
|
|
+ adjacencyNodes.push(to);
|
|
|
|
|
+ else
|
|
|
|
|
+ this._adjacencyList.set(from, [to]);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|