|
|
@@ -1,61 +1,98 @@
|
|
|
-import { pipe, object, array, forward, partialCheck } from "valibot";
|
|
|
+import { array, forward, object, partialCheck, pipe } from "valibot";
|
|
|
import Edge, { EdgeSchema } from "./Edge.js";
|
|
|
import Node, { NodeSchema } from "./Node.js";
|
|
|
|
|
|
export default class Graph<TNode extends Node, TEdge extends Edge<TNode>> {
|
|
|
- private readonly _nodes: TNode[];
|
|
|
- private readonly _edges: TEdge[];
|
|
|
private readonly _adjacencyList: Map<TNode["id"], Array<TNode["id"]>>;
|
|
|
+ private readonly _nodeMap: Map<TNode["id"], TNode>;
|
|
|
+ private readonly _edgeMap: Map<TEdge["id"], TEdge>;
|
|
|
|
|
|
public constructor(nodes: TNode[], edges: TEdge[]) {
|
|
|
- this._nodes = nodes;
|
|
|
- this._edges = edges;
|
|
|
this._adjacencyList = new Map();
|
|
|
+ this._nodeMap = new Map();
|
|
|
+ this._edgeMap = new Map();
|
|
|
|
|
|
- for(const node of nodes)
|
|
|
+ for(const node of nodes) {
|
|
|
+ this._nodeMap.set(node.id, node);
|
|
|
this._adjacencyList.set(node.id, []);
|
|
|
-
|
|
|
- for(const { from, to } of edges) {
|
|
|
- const adjacencyNodes = this._adjacencyList.get(from);
|
|
|
- if(adjacencyNodes)
|
|
|
- adjacencyNodes.push(to)
|
|
|
- else
|
|
|
- this._adjacencyList.set(from, [to])
|
|
|
}
|
|
|
+
|
|
|
+ for(const edge of edges)
|
|
|
+ this._edgeMap.set(edge.id, edge);
|
|
|
+
|
|
|
+ this.updateAdjacencyList();
|
|
|
}
|
|
|
|
|
|
public getNodes() : TNode[] {
|
|
|
- return this._nodes;
|
|
|
+ return Array.from(this._nodeMap.values());
|
|
|
}
|
|
|
|
|
|
public getNode(nodeId: TNode["id"]): TNode | null {
|
|
|
- return this._nodes.find((node) => node.id == nodeId) || null;
|
|
|
+ return this._nodeMap.get(nodeId) || null;
|
|
|
}
|
|
|
|
|
|
public getEdges() : TEdge[] {
|
|
|
- return this._edges;
|
|
|
+ return Array.from(this._edgeMap.values());
|
|
|
}
|
|
|
|
|
|
public getEdge(edgeId: TEdge["id"]): TEdge | null {
|
|
|
- return this._edges.find((edge) => edge.id == edgeId) || null;
|
|
|
+ return this._edgeMap.get(edgeId) || null;
|
|
|
}
|
|
|
+
|
|
|
+ public updateEdgeById(id: TEdge["id"], edge: TEdge) : this {
|
|
|
+ this._edgeMap.set(id, edge);
|
|
|
+ this.updateAdjacencyList();
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public getNodeInputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
+ const inputs: TNode[] = []
|
|
|
+
|
|
|
+ for(const [id, outputs] of this._adjacencyList) {
|
|
|
+ if(!outputs.includes(nodeId))
|
|
|
+ continue;
|
|
|
|
|
|
- public getNodeNeighbours(nodeId: TNode["id"]): TNode[] {
|
|
|
- const neighbours: TNode[] = [];
|
|
|
- const neighbourIds = this._adjacencyList.get(nodeId);
|
|
|
+ const input = this._nodeMap.get(id);
|
|
|
+ if(!input)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ inputs.push(input);
|
|
|
+ }
|
|
|
|
|
|
- if(!neighbourIds)
|
|
|
+ return inputs;
|
|
|
+ }
|
|
|
+
|
|
|
+ public getNodeOutputs(nodeId: TNode["id"]) : TNode[] {
|
|
|
+ const outputIds = this._adjacencyList.get(nodeId);
|
|
|
+ if(!outputIds)
|
|
|
return [];
|
|
|
|
|
|
- neighbourIds.forEach((id) => {
|
|
|
- const node = this._nodes.find((n) => n.id == id);
|
|
|
+ const outputs: TNode[] = [];
|
|
|
+
|
|
|
+ outputIds.forEach((id) => {
|
|
|
+ const node = this._nodeMap.get(id);
|
|
|
if(!node)
|
|
|
return;
|
|
|
|
|
|
- neighbours.push(node);
|
|
|
+ outputs.push(node);
|
|
|
})
|
|
|
|
|
|
- return neighbours;
|
|
|
+ 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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -64,6 +101,42 @@ export const GraphSchema = pipe(
|
|
|
nodes: array(NodeSchema),
|
|
|
edges: array(EdgeSchema)
|
|
|
}),
|
|
|
+ forward(
|
|
|
+ partialCheck(
|
|
|
+ [['nodes']],
|
|
|
+ (input) => {
|
|
|
+ const {nodes} = input;
|
|
|
+ for(let i = 0; i < nodes.length; i++) {
|
|
|
+ const currentNodeId = nodes[i]!.id;
|
|
|
+ const idIndex = nodes.findIndex((n) => n.id === currentNodeId);
|
|
|
+ if(idIndex !== i)
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ `Every node in "nodes" array must have an unique identifier!`
|
|
|
+ ),
|
|
|
+ ["nodes"]
|
|
|
+ ),
|
|
|
+ forward(
|
|
|
+ partialCheck(
|
|
|
+ [['edges']],
|
|
|
+ (input) => {
|
|
|
+ const { edges } = input;
|
|
|
+ for(let i = 0; i < edges.length; i++) {
|
|
|
+ const currentNodeId = edges[i]!.id;
|
|
|
+ const idIndex = edges.findIndex((n) => n.id === currentNodeId);
|
|
|
+ if(idIndex !== i)
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ `Every edge in "edges" array must have an unique identifier!`
|
|
|
+ ),
|
|
|
+ ['edges']
|
|
|
+ ),
|
|
|
forward(
|
|
|
partialCheck(
|
|
|
[['nodes'], ['edges']],
|
|
|
@@ -81,4 +154,4 @@ export const GraphSchema = pipe(
|
|
|
),
|
|
|
['edges']
|
|
|
)
|
|
|
-)
|
|
|
+)
|