Graph.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { array, forward, object, partialCheck, pipe } from "valibot";
  2. import Edge, { EdgeSchema } from "./edge/Edge.js";
  3. import Node, { NodeSchema } from "./node/Node.js";
  4. import GraphError from "../errors/graph/GraphError.js";
  5. export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
  6. private readonly _adjacencyList: Map<TNode, Array<TNode>> = new Map();
  7. private readonly _nodeMap: Map<string, TNode>;
  8. private readonly _edgeMap: Map<string, TEdge>;
  9. public constructor(nodes: TNode[], edges: TEdge[]) {
  10. this._nodeMap = new Map();
  11. nodes.forEach((node) => this.addNode(node));
  12. this._edgeMap = new Map();
  13. edges.forEach((edge) => this.addEdge(edge));
  14. for(const edge of edges) {
  15. const from = edge.getFrom(), to = edge.getTo();
  16. const adjacencyNodes = this._adjacencyList.get(from);
  17. if(adjacencyNodes)
  18. adjacencyNodes.push(to);
  19. else
  20. this._adjacencyList.set(from, [to]);
  21. }
  22. }
  23. public isAcyclic() : boolean {
  24. const queue: TNode[] = [];
  25. const nodeToInDegree = new Map<TNode, number>();
  26. const nodesCount = this._nodeMap.size;
  27. let visitedNodesCount = 0;
  28. for(const node of this.getNodes())
  29. nodeToInDegree.set(node, this.getNodeInputs(node).length);
  30. for(const [nodeId, inDegree] of nodeToInDegree)
  31. if(inDegree === 0)
  32. queue.push(nodeId);
  33. while(queue.length > 0) {
  34. const node = queue.pop()!;
  35. visitedNodesCount++;
  36. for(const output of this.getNodeOutputs(node)) {
  37. const inDegree = nodeToInDegree.get(output);
  38. const newInDegree = inDegree !== undefined ? inDegree - 1 : 0;
  39. nodeToInDegree.set(output, newInDegree);
  40. if(newInDegree === 0)
  41. queue.push(output);
  42. }
  43. }
  44. return visitedNodesCount === nodesCount;
  45. }
  46. public getNodes() : TNode[] {
  47. return [...this._nodeMap.values()];
  48. }
  49. public getNode(id: string) : Node | null {
  50. return this._nodeMap.get(id) || null;
  51. }
  52. public addNode(node: TNode) : this {
  53. const id = node.getId();
  54. if(this._nodeMap.has(id))
  55. throw new GraphError(`Can't add a new node to graph: node with id ${id} already exists`);
  56. this._nodeMap.set(id, node);
  57. node.subscribe(() => this.onNodeChange(node));
  58. return this;
  59. }
  60. public removeNode(nodeId: string) : this {
  61. if(!this._nodeMap.has(nodeId))
  62. throw new GraphError(`Can't delete a node from graph: node with id ${nodeId} doesn't exists`);
  63. for(const [edgeId, edge] of this._edgeMap) {
  64. const fromId = edge.getFrom().getId(), toId = edge.getTo().getId();
  65. if(fromId == nodeId || toId == nodeId)
  66. this._edgeMap.delete(edgeId);
  67. }
  68. this._nodeMap.delete(nodeId);
  69. this.buildAdjacencyList();
  70. return this;
  71. }
  72. public getEdges() : TEdge[] {
  73. return Array.from(this._edgeMap.values());
  74. }
  75. public getEdge(edgeId: string): TEdge | null {
  76. return this._edgeMap.get(edgeId) || null;
  77. }
  78. public addEdge(edge: TEdge) : this {
  79. const id = edge.getId(), from = edge.getFrom(), to = edge.getTo();
  80. if(this._edgeMap.has(id))
  81. throw new GraphError(`Can't add a new edge to graph: edge with id ${id} already exists`);
  82. if(!this._nodeMap.has(from.getId()) || !this._nodeMap.has(to.getId()))
  83. throw new GraphError(`Can't add a new edge to graph: edge references to non-existing nodes`);
  84. this._edgeMap.set(id, edge);
  85. edge.subscribe(() => this.onEdgeChange(edge));
  86. this.buildAdjacencyList();
  87. return this;
  88. }
  89. public removeEdge(edgeId: string): this {
  90. if(!this._edgeMap.has(edgeId))
  91. return this;
  92. this._edgeMap.delete(edgeId);
  93. this.buildAdjacencyList();
  94. return this;
  95. }
  96. public getNodeInputs(node: TNode) : TNode[] {
  97. const inputs: TNode[] = [];
  98. for(const [input, outputs] of this._adjacencyList) {
  99. if(!outputs.includes(node))
  100. continue;
  101. inputs.push(input);
  102. }
  103. return inputs;
  104. }
  105. public getNodeOutputs(node: TNode) : TNode[] {
  106. return this._adjacencyList.get(node) || [];
  107. }
  108. public isSourceNode(node: TNode) : boolean {
  109. return this.getNodeInputs(node).length === 0;
  110. }
  111. public isSinkNode(node: TNode) : boolean {
  112. return this.getNodeOutputs(node).length === 0;
  113. }
  114. protected onNodeChange(node: TNode) : void {
  115. }
  116. protected onEdgeChange(edge: TEdge) : void {
  117. this.buildAdjacencyList();
  118. }
  119. private buildAdjacencyList() : void {
  120. this._adjacencyList.clear();
  121. for(const [, edge] of this._edgeMap) {
  122. const from = edge.getFrom(), to = edge.getTo();
  123. const adjacencyNodes = this._adjacencyList.get(from);
  124. if(adjacencyNodes)
  125. adjacencyNodes.push(to);
  126. else
  127. this._adjacencyList.set(from, [to]);
  128. }
  129. }
  130. }
  131. export const GraphSchema = pipe(
  132. object({
  133. nodes: array(NodeSchema),
  134. edges: array(EdgeSchema)
  135. }),
  136. forward(
  137. partialCheck(
  138. [['nodes']],
  139. (input) => {
  140. const {nodes} = input;
  141. for(let i = 0; i < nodes.length; i++) {
  142. const currentNodeId = nodes[i]!.id;
  143. const idIndex = nodes.findIndex((n) => n.id === currentNodeId);
  144. if(idIndex !== i)
  145. return false;
  146. }
  147. return true;
  148. },
  149. `Every node in "nodes" array must have an unique identifier!`
  150. ),
  151. ["nodes"]
  152. ),
  153. forward(
  154. partialCheck(
  155. [['edges']],
  156. (input) => {
  157. const { edges } = input;
  158. for(let i = 0; i < edges.length; i++) {
  159. const currentNodeId = edges[i]!.id;
  160. const idIndex = edges.findIndex((n) => n.id === currentNodeId);
  161. if(idIndex !== i)
  162. return false;
  163. }
  164. return true;
  165. },
  166. `Every edge in "edges" array must have an unique identifier!`
  167. ),
  168. ['edges']
  169. ),
  170. forward(
  171. partialCheck(
  172. [['nodes'], ['edges']],
  173. (input) => {
  174. const { nodes, edges } = input;
  175. for(const edge of edges) {
  176. const { from, to } = edge;
  177. if(!(nodes.some((node) => node.id == from) && nodes.some((node) => node.id == to)))
  178. return false;
  179. }
  180. return true;
  181. },
  182. `Some of edges contains link to a node that doesn't exist in "nodes" array`
  183. ),
  184. ['edges']
  185. )
  186. )