|
|
@@ -1,4 +1,4 @@
|
|
|
-// NodeJS library
|
|
|
+// NodeJS libraries
|
|
|
import {inspect} from "util";
|
|
|
|
|
|
type PrimitiveType = string | number | boolean;
|
|
|
@@ -30,7 +30,7 @@ function nodeIdsEqual(a: NodeId, b: NodeId) {
|
|
|
// Every edge corresponds to one entry in the source's 'outgoing', and one entry in the target's 'incoming'.
|
|
|
class Node {
|
|
|
outgoing: Map<string, NodeId>;
|
|
|
- incoming: Array<{label: string, fromId: NodeId}>;
|
|
|
+ incoming: Array<{label: string, srcId: NodeId}>;
|
|
|
|
|
|
constructor() {
|
|
|
this.outgoing = new Map();
|
|
|
@@ -51,27 +51,32 @@ class NodeMap {
|
|
|
this.values = new Map();
|
|
|
}
|
|
|
|
|
|
- get(id: NodeId): Node {
|
|
|
+ getOptional(id: NodeId): Node | undefined {
|
|
|
if (id instanceof UUID) {
|
|
|
// ordinary node: can only get it if it actually exists,
|
|
|
// i.e., it has already been created and has not yet been deleted.
|
|
|
- if (! this.nodes.has(id.uuid)) {
|
|
|
- // TODO: turn this into 'Maybe'
|
|
|
- throw Error("No such node:" + id);
|
|
|
- }
|
|
|
- return this.nodes.get(id.uuid);
|
|
|
+ return this.nodes.get(id.uuid); // may return undefined
|
|
|
}
|
|
|
|
|
|
// value node: implicitly create it if it doesn't exist yet,
|
|
|
// pretending that it's "always already there"
|
|
|
- if (this.values.has(id)) {
|
|
|
- return this.values.get(id);
|
|
|
- } else {
|
|
|
- // auto-construct non-existing value node
|
|
|
- const node = new Node();
|
|
|
- this.values.set(id, node);
|
|
|
- return node;
|
|
|
+ const v = this.values.get(id);
|
|
|
+ if (v !== undefined) {
|
|
|
+ return v;
|
|
|
}
|
|
|
+ // auto-construct non-existing value node
|
|
|
+ const node = new Node();
|
|
|
+ this.values.set(id, node);
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+
|
|
|
+ // same as get, but raises error when not found
|
|
|
+ get(id: NodeId): Node {
|
|
|
+ const node = this.getOptional(id);
|
|
|
+ if (node === undefined) {
|
|
|
+ throw Error("node not found");
|
|
|
+ }
|
|
|
+ return node;
|
|
|
}
|
|
|
|
|
|
// create a new ordinary node
|
|
|
@@ -82,6 +87,7 @@ class NodeMap {
|
|
|
}
|
|
|
|
|
|
// delete an ordinary node
|
|
|
+ // Idempotent.
|
|
|
delete(id: UUID) {
|
|
|
this.nodes.delete(id.uuid);
|
|
|
}
|
|
|
@@ -104,67 +110,87 @@ export class GraphState {
|
|
|
}
|
|
|
|
|
|
// Delete node and delete all of its outgoing + incoming edges.
|
|
|
+ // Does nothing when given uuid does not exist.
|
|
|
+ // Idempotent.
|
|
|
deleteNode(uuid: UUID) {
|
|
|
- const node = this.nodes.get(uuid);
|
|
|
- // delete outgoing edges
|
|
|
- for (const [label, toId] of node.outgoing.entries()) {
|
|
|
- const toNode = this.nodes.get(toId);
|
|
|
- // remove edge from toNode.incoming
|
|
|
- for (const [i, {label, fromId}] of toNode.incoming.entries()) {
|
|
|
- toNode.incoming.splice(i, 1);
|
|
|
- break;
|
|
|
+ const node = this.nodes.getOptional(uuid);
|
|
|
+ if (node !== undefined) {
|
|
|
+ // delete outgoing edges
|
|
|
+ for (const [label, targetId] of node.outgoing.entries()) {
|
|
|
+ const targetNode = this.nodes.get(targetId);
|
|
|
+ // remove edge from targetNode.incoming
|
|
|
+ const i = this.lookupIncoming(targetNode, label, uuid);
|
|
|
+ targetNode.incoming.splice(i, 1);
|
|
|
}
|
|
|
+ // delete incoming edges
|
|
|
+ for (const {label, srcId} of node.incoming) {
|
|
|
+ const srcNode = this.nodes.get(srcId);
|
|
|
+ // remove edge from srcNode.outgoing
|
|
|
+ srcNode.outgoing.delete(label);
|
|
|
+ }
|
|
|
+ // delete node itself
|
|
|
+ this.nodes.delete(uuid);
|
|
|
}
|
|
|
- // delete incoming edges
|
|
|
- for (const {label, fromId} of node.incoming) {
|
|
|
- const fromNode = this.nodes.get(fromId);
|
|
|
- // remove edge from fromNode.outgoing
|
|
|
- fromNode.outgoing.delete(label);
|
|
|
- }
|
|
|
- // delete node itself
|
|
|
- this.nodes.delete(uuid);
|
|
|
}
|
|
|
|
|
|
- createEdge(fromId: NodeId, toId: NodeId, label: string) {
|
|
|
- const fromNode = this.nodes.get(fromId);
|
|
|
- fromNode.outgoing.set(label, toId);
|
|
|
+ // Set a node's outgoing edge to point to a node
|
|
|
+ // Idempotent.
|
|
|
+ setEdge(srcId: NodeId, label: string, targetId: NodeId) {
|
|
|
+ // gotta remove the existing edge first, if it exists
|
|
|
+ this.unsetEdge(srcId, label);
|
|
|
|
|
|
- const toNode = this.nodes.get(toId);
|
|
|
- toNode.incoming.push({label, fromId});
|
|
|
+ const srcNode = this.nodes.get(srcId);
|
|
|
+ srcNode.outgoing.set(label, targetId);
|
|
|
+ const targetNode = this.nodes.get(targetId);
|
|
|
+ targetNode.incoming.push({label, srcId});
|
|
|
}
|
|
|
|
|
|
- deleteEdge(nodeId: NodeId, label: string) {
|
|
|
- const node = this.nodes.get(nodeId);
|
|
|
- const toId = node.outgoing.get(label);
|
|
|
- const toNode = this.nodes.get(toId);
|
|
|
- for (const [i, {label, fromId}] of toNode.incoming.entries()) {
|
|
|
- if (label === label && nodeIdsEqual(nodeId, fromId)) {
|
|
|
- toNode.incoming.splice(i, 1);
|
|
|
- break;
|
|
|
+ // Unset, i.e., delete an edge
|
|
|
+ // Idempotent.
|
|
|
+ unsetEdge(srcId: NodeId, label: string) {
|
|
|
+ const srcNode = this.nodes.get(srcId);
|
|
|
+ const existingTargetId = srcNode.outgoing.get(label);
|
|
|
+ if (existingTargetId !== undefined) {
|
|
|
+ // remove the respective entry in the existingTargetNode's 'incoming' array:
|
|
|
+ const existingTargetNode = this.nodes.get(existingTargetId);
|
|
|
+ const i = this.lookupIncoming(existingTargetNode, label, srcId);
|
|
|
+ existingTargetNode.incoming.splice(i, 1); // remove from array
|
|
|
+ }
|
|
|
+ srcNode.outgoing.delete(label);
|
|
|
+ }
|
|
|
+
|
|
|
+ // In a node's array of incoming edges, search for {label, srcId}, and return the position in the array.
|
|
|
+ private lookupIncoming(node: Node, label: string, srcId: NodeId): number {
|
|
|
+ for (const [i, {label: l, srcId: s}] of node.incoming.entries()) {
|
|
|
+ if (l === label && nodeIdsEqual(s, srcId)) {
|
|
|
+ return i;
|
|
|
}
|
|
|
}
|
|
|
- node.outgoing.delete(label);
|
|
|
+ throw new Error("Not found!");
|
|
|
}
|
|
|
|
|
|
- readEdge(fromId: NodeId, label: string): NodeId {
|
|
|
- // TODO: write test for this function
|
|
|
- const fromNode = this.nodes.get(fromId);
|
|
|
- return fromNode.outgoing.get(label);
|
|
|
+ // Read a node's outgoing edge with given label.
|
|
|
+ // If no such edge exists, returns undefined.
|
|
|
+ readEdge(srcId: NodeId, label: string): NodeId | undefined {
|
|
|
+ const srcNode = this.nodes.getOptional(srcId);
|
|
|
+ if (srcNode !== undefined) {
|
|
|
+ return srcNode.outgoing.get(label);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Get all the edges in the graph. Slow. For debugging purposes.
|
|
|
getEdges(): Array<[NodeId, NodeId, string]> {
|
|
|
- const result = [];
|
|
|
+ const result: Array<[NodeId, NodeId, string]> = [];
|
|
|
// get all outgoing edges of ordinary nodes
|
|
|
- for (const [fromId, fromNode] of this.nodes.nodes.entries()) {
|
|
|
- for (const [label, toId] of fromNode.outgoing) {
|
|
|
- result.push([new UUID(fromId), toId, label]);
|
|
|
+ for (const [srcId, srcNode] of this.nodes.nodes.entries()) {
|
|
|
+ for (const [label, targetId] of srcNode.outgoing) {
|
|
|
+ result.push([new UUID(srcId), targetId, label]);
|
|
|
}
|
|
|
}
|
|
|
// get all outgoing edges of value nodes
|
|
|
- for (const [fromId, fromNode] of this.nodes.values.entries()) {
|
|
|
- for (const [label, toId] of fromNode.outgoing) {
|
|
|
- result.push([fromId, toId, label]);
|
|
|
+ for (const [srcId, srcNode] of this.nodes.values.entries()) {
|
|
|
+ for (const [label, targetId] of srcNode.outgoing) {
|
|
|
+ result.push([srcId, targetId, label]);
|
|
|
}
|
|
|
}
|
|
|
return result;
|