Browse Source

Record read-dependencies in live modeling demo

Joeri Exelmans 2 years ago
parent
commit
d4e7025d57

+ 11 - 11
src/frontend/demos/demo_le.tsx

@@ -56,8 +56,8 @@ export function getDemoLE() {
 
         // Inserts list item after prevNode and before nextNode.
         function insertBetween(prevNode: INodeState, nextNode: INodeState, val: PrimitiveValue) {
-            if (prevNode.getOutgoingEdges().get("next") !== nextNode 
-             || nextNode.getOutgoingEdges().get("prev") !== prevNode) {
+            if (prevNode.outgoing.get("next") !== nextNode 
+             || nextNode.outgoing.get("prev") !== prevNode) {
                 throw new Error("Assertion failed");
             }
 
@@ -80,18 +80,18 @@ export function getDemoLE() {
         }
 
         function insertBefore(node: INodeState, val: PrimitiveValue) {
-            const prevNode = node.getOutgoingEdges().get("prev") as INodeState;
+            const prevNode = node.outgoing.get("prev") as INodeState;
             return insertBetween(prevNode, node, val);
         }
 
         function insertAfter(node: INodeState, val: PrimitiveValue) {
-            const nextNode = node.getOutgoingEdges().get("next") as INodeState;
+            const nextNode = node.outgoing.get("next") as INodeState;
             return insertBetween(node, nextNode, val);
         }
 
         function deleteItem(node: INodeState) {
-            const prevNode = node.getOutgoingEdges().get("prev") as INodeState;
-            const nextNode = node.getOutgoingEdges().get("next") as INodeState;
+            const prevNode = node.outgoing.get("prev") as INodeState;
+            const nextNode = node.outgoing.get("next") as INodeState;
 
             onion.graphState.pushState(); // we will go back to this checkpoint
 
@@ -101,26 +101,26 @@ export function getDemoLE() {
 
             const deltas = onion.graphState.popState();
 
-            reducer.createAndGotoNewVersion(deltas as PrimitiveDelta[], "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
+            reducer.createAndGotoNewVersion(deltas as PrimitiveDelta[], "delete"+JSON.stringify((node.outgoing.get("value") as IValueState).value));
         }
 
         // Alternative implementation of deletion - just add a property 'deleted' to the deleted node.
         function deleteItemAlt(node: INodeState) {
             const delta = node.getDeltaForSetEdge(deltaRegistry, "deleted", true);
 
-            reducer.createAndGotoNewVersion([delta], "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
+            reducer.createAndGotoNewVersion([delta], "delete"+JSON.stringify((node.outgoing.get("value") as IValueState).value));
         }
 
         const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
 
         // Recursively renders list elements as a vertical stack
         function renderList(head: INodeState, stopAt: INodeState) {
-            const nextItem = head.getOutgoingEdges().get("next") as INodeState;
+            const nextItem = head.outgoing.get("next") as INodeState;
             const nextDOM = nextItem === stopAt ? <></> : renderList(nextItem, stopAt);
-            if (head.getOutgoingEdges().get("deleted")?.asTarget() === true) {
+            if (head.outgoing.get("deleted")?.asTarget() === true) {
                 return nextDOM; // skip deleted items
             }
-            const value = head.getOutgoingEdges().get("value");
+            const value = head.outgoing.get("value");
             const itemText = value === undefined ? "Start of list" : "List item: " + JSON.stringify((value as IValueState).value);
             return <>
                 <Paper shadow="sm" radius="md" p="md" withBorder>

+ 7 - 8
src/frontend/demos/demo_live.tsx

@@ -10,7 +10,7 @@ import {InfoHoverCard, InfoHoverCardOverlay} from "../info_hover_card";
 import {OnionContext} from "../onion_context";
 
 import {mockUuid} from "../../util/test_helpers";
-import {NodeDeletion, TargetValue, TargetNode} from "onion/delta";
+import {NodeDeletion, TargetValue, TargetNode, ExistingEdge} from "onion/delta";
 import {DeltaRegistry} from "onion/delta_registry";
 import {VersionRegistry} from "onion/version";
 import {Delta} from "onion/delta";
@@ -28,7 +28,7 @@ export const demo_Live_description = <>
   </Text>
 </>;
 
-const getStateName = s => (s.getOutgoingEdges().get("name") as IValueState).value as string;
+const getStateName = (s: INodeState) => (s.outgoing.get("name") as IValueState).value as string;
 
 export function getDemoLive() {
   const deltaRegistry = new DeltaRegistry();
@@ -276,17 +276,17 @@ export function getDemoLive() {
     }
     function* iterType(graphState: GraphState, type: string) {
       for (const nodeState of iterNonDeletedNodes(graphState)) {
-        const nodeType = (nodeState.getOutgoingEdges().get("type") as IValueState)?.value as string;
+        const nodeType = (nodeState.outgoing.get("type") as IValueState)?.value as string;
         if (nodeType === type) {
           yield nodeState;
         }
       }
     }
     function getProperty<T>(nodeState: INodeState, property: string): (T|undefined) {
-      return (nodeState.getOutgoingEdges().get(property) as IValueState)?.value as T;
+      return (nodeState.outgoing.get(property) as IValueState)?.value as T;
     }
     function getLink(nodeState: INodeState, linkName: string): (INodeState|undefined) {
-      return (nodeState.getOutgoingEdges().get(linkName) as INodeState);
+      return (nodeState.outgoing.get(linkName) as INodeState);
     }
     const findState = (graphState: GraphState, stateName: string) => {
       for (const nodeState of iterType(graphState, "State")) {
@@ -383,9 +383,8 @@ export function getDemoLive() {
           graphState.exec(nodeCreation);
           graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("type"), "RuntimeModel"));
           graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("design"), runtimeStuff.modelNode!.creation));
-          // overwrite 'initial' pointer with its current value:
-          // graphState.exec(runtimeStuff.modelNode!.getDeltaForSetEdge(deltaRegistry, "initial", runtimeStuff.initial!.creation));
-        graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("current"), runtimeStuff.initial!.creation));
+          // set the 'current' pointer - read dependency on 'initial'.
+          graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("current"), runtimeStuff.initial!.creation, [(runtimeStuff.modelNode!.outgoingDeltas.get("initial")!.read())]));
           return {compositeLabel: "initialize"};
         });
       }

+ 2 - 2
src/frontend/rountangleEditor/RountangleEditor.tsx

@@ -18,7 +18,7 @@ export interface Rountangle {
 export function isRountangle(d3node: D3OnionNodeData) {
     if (!(d3node.obj.type === "node")) return false;
     const nodeState = d3node.obj as unknown as INodeState;
-    const outgoing = nodeState.getOutgoingEdges();
+    const outgoing = nodeState.outgoing;
     if (!(outgoing.get('type')?.asTarget() === 'Rountangle')) return false;
     if (!(typeof outgoing.get('x')?.asTarget()       === "number")) return false;
     if (!(typeof outgoing.get('y')?.asTarget()       === "number")) return false;
@@ -35,7 +35,7 @@ export function graphStateToRountangle(d3node: D3OnionNodeData): [UUID, Rountang
     }
 
     const nodeState = d3node.obj as INodeState;
-    const outgoing = nodeState.getOutgoingEdges();
+    const outgoing = nodeState.outgoing;
     return [nodeState.creation.id, {
         posX: outgoing.get("x")!.asTarget() as number,
         posY: outgoing.get("y")!.asTarget() as number,

+ 25 - 23
src/frontend/versioned_model/correspondence.tsx

@@ -111,28 +111,30 @@ export function newCorrespondence({deltaRegistry, generateUUID, versionRegistry}
         const filteredCorrParentVersions = filterCorrParents(corrParentVersions);
         // And if then, there are still multiple CORR versions to choose from, we just pick one:
         // (it would be better to ask the user which one, but whatever)
-        const [corrParentVersion] = filteredCorrParentVersions;
-        const asParentVersion = corrParentVersion.getEmbedding("as").version;
-        if (asParentVersion === undefined) {
-          throw new Error("Assertion failed: CS's parent is part of a CORR version, but that CORR version does not embed an AS version.");
+        for (const corrParentVersion of filteredCorrParentVersions) {
+          const asParentVersion = corrParentVersion.getEmbedding("as").version;
+          if (asParentVersion === undefined) {
+            throw new Error("Assertion failed: CS's parent is part of a CORR version, but that CORR version does not embed an AS version.");
+          }
+  
+          const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
+  
+          const parser = new RountangleParser(deltaRegistry, generateUUID);
+          const {corrDeltas, asDeltas, csOverrides, asOverrides} = parser.parse(csDeltas, csGS, corrGS, asGS);
+  
+          const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "as:"+description, asParentVersion) : asParentVersion;
+  
+          reducer.appendVersions([csVersion, asVersion]);
+  
+          const corrVersion = reducer.createAndGotoNewVersion(corrDeltas, "corr:"+description, corrParentVersion,
+            () => new Map([
+              ["cs", {version: csVersion, overridings: csOverrides}],
+              ["as", {version: asVersion, overridings: asOverrides}],
+            ]));
+  
+          corrVersions.add(corrVersion);
+  
         }
-
-        const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
-
-        const parser = new RountangleParser(deltaRegistry, generateUUID);
-        const {corrDeltas, asDeltas, csOverrides, asOverrides} = parser.parse(csDeltas, csGS, corrGS, asGS);
-
-        const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "as:"+description, asParentVersion) : asParentVersion;
-
-        reducer.appendVersions([csVersion, asVersion]);
-
-        const corrVersion = reducer.createAndGotoNewVersion(corrDeltas, "corr:"+description, corrParentVersion,
-          () => new Map([
-            ["cs", {version: csVersion, overridings: csOverrides}],
-            ["as", {version: asVersion, overridings: asOverrides}],
-          ]));
-
-        corrVersions.add(corrVersion);
       }
     };
     const renderExistingVersion = async (asVersion: Version, setManualRendererState) => {
@@ -200,11 +202,11 @@ export function newCorrespondence({deltaRegistry, generateUUID, versionRegistry}
             const asToCs = new Map();
 
             for (const corrNode of corrGS.nodes.values()) {
-              const csNode = corrNode.getOutgoingEdges().get("cs");
+              const csNode = corrNode.outgoing.get("cs");
               if (csNode?.type !== "node") {
                 continue; // corrNode is not a correspondence node
               }
-              const asNode = corrNode.getOutgoingEdges().get("as");
+              const asNode = corrNode.outgoing.get("as");
               if (asNode?.type !== "node") {
                 continue; // corrNode is not a correspondence node
               }

+ 2 - 2
src/frontend/versioned_model/manual_renderer.tsx

@@ -86,13 +86,13 @@ export function ManualRenderer(props: ManualRendererProps) {
     for (const [insideCs, outsideCs] of insidenesses) {
       const childAs = asGraphState.nodes.get(props.csToAs.get(insideCs)!);
       const parentAs = asGraphState.nodes.get(props.csToAs.get(outsideCs)!);
-      if (childAs.getOutgoingEdges().get("hasParent") !== parentAs) {
+      if (childAs.outgoing.get("hasParent") !== parentAs) {
         inconsistencies.push("CS rountangle " + JSON.stringify(insideCs) + " must not be inside " + JSON.stringify(outsideCs));
       }
     }
     // For every parent-link in AS, there must be an "insideness" in CS:
     for (const childAs of asGraphState.nodes.values()) {
-      const parentAs = childAs.getOutgoingEdges().get("hasParent");
+      const parentAs = childAs.outgoing.get("hasParent");
       if (parentAs !== undefined) {
         const insideCsId = props.asToCs.get(childAs.creation.id)!;
         const outsideCsId = props.asToCs.get(parentAs.creation.id)!

+ 0 - 18
src/onion/buffer_xor.ts

@@ -1,18 +0,0 @@
-import {Buffer} from "buffer";
-
-// Often in the code, when we want to calculate a content-based ID, where the content is an unordered set, we compute the XOR of the content-based IDs of the set elements. This is because XOR is insensitive to order.
-
-// Precondition that is NOT CHECKED: buffers must be of equal length
-// Returns new buffer that is bitwise XOR of inputs.
-export function bufferXOR(a: Buffer, b: Buffer): Buffer {
-  const result = Buffer.allocUnsafe(a.length);
-  for (let i=0; i<a.length; i+=4) {
-    // Little endian is fastest, because native to Intel CPUs
-    result.writeInt32LE(a.readInt32LE(i) ^ b.readInt32LE(i), i);
-  }
-  return result;
-}
-
-export function buffersXOR(...args: Buffer[]): Buffer {
-  return args.reduce((a,b) => bufferXOR(a,b), Buffer.alloc(32));
-}

+ 69 - 1
src/onion/delta.test.ts

@@ -229,5 +229,73 @@ describe("Primitive Delta", () => {
 
     // console.log(nodeDeletion.conflictsWith);
     assert(nodeDeletion.conflictsWith.length === 0, "expected no conflicts");
+  });
+
+  it("Read/write conflict", () => {
+    const registry = new DeltaRegistry();
+    const getId = mockUuid();
+
+    const envCreation = registry.newNodeCreation(getId());
+    const xInitial = registry.newEdgeUpdate(envCreation.createOutgoingEdge("x"), 1);
+    const yInitial = registry.newEdgeUpdate(envCreation.createOutgoingEdge("y"), 2);
+    
+    // so now, x == 1, y == 2
+
+    // then, x := x + 1
+    const xUpdate = registry.newEdgeUpdate(xInitial.overwrite(), 2, [xInitial.overwrite()]);
+
+    // and concurrently, y := x + y
+    const yUpdate = registry.newEdgeUpdate(yInitial.overwrite(), 3, [xInitial.overwrite(), yInitial.overwrite()]);
+
+    assert(xUpdate.conflictsWith.length === 1 && yUpdate.conflictsWith.length === 1, "expected one conflict");
+    assert(xUpdate.conflictsWith.some(([d]) => d === yUpdate), "expected one conflict");
+  });
+  
+  it("No Read/Read conflict", () => {
+    const registry = new DeltaRegistry();
+    const getId = mockUuid();
+  
+    const envCreation = registry.newNodeCreation(getId());
+    const xInitial = registry.newEdgeUpdate(envCreation.createOutgoingEdge("x"), 1);
+    
+    // so now, x == 1
+  
+    // then, y := x + 1
+    const yInitial = registry.newEdgeUpdate(envCreation.createOutgoingEdge("y"), 2, [xInitial.overwrite()]);
+  
+    // and concurrently, z := x + 2
+    const zInitial = registry.newEdgeUpdate(envCreation.createOutgoingEdge("z"), 3, [xInitial.overwrite()]);
+  
+    assert(yInitial.conflictsWith.length === 0 && zInitial.conflictsWith.length === 0, "expected no conflicts");
+  });
+
+  it("R*/U conflict", () => {
+    const registry = new DeltaRegistry();
+    const getId = mockUuid();
+
+    const collection = registry.newNodeCreation(getId());
+    const newEdge = collection.createOutgoingEdge("x");
+    assert(newEdge === collection.createOutgoingEdge("x"), "Should return same object");
+
+    const addItemX = registry.newEdgeUpdate(newEdge, "x");
+    assert(newEdge === collection.createOutgoingEdge("x"), "Should return same object");
+
+    assert(addItemX.conflictsWith.length === 0, "No conflicts so far");
+
+    // Now we'll create our conflict:
+    const readAll = registry.newReadAllOutgoing(collection, [collection.createOutgoingEdge("x")]);
+    const addItemY = registry.newEdgeUpdate(collection.createOutgoingEdge("y"), "y");
+    
+    console.log(collection);
+    console.log(readAll.conflictsWith)
+    console.log(addItemY.conflictsWith)
+
+    assert(readAll.conflictsWith.length === 1
+       && addItemY.conflictsWith.length === 1, "Expected one conflict here");
+    assert(readAll.conflictsWith.some(([d]) => d === addItemY)
+       && addItemY.conflictsWith.some(([d]) => d === readAll), "Expected 'addItemY' and 'readAll' to be conflicting");
+
+    assert(addItemX.conflictsWith.length === 0, "Still no conflicts here");
   })
-});
+});
+

+ 109 - 28
src/onion/delta.ts

@@ -1,7 +1,9 @@
 import {Buffer} from "buffer";
 import {PrimitiveValue, UUID} from "./types";
+import { bufferXOR, buffersXOR } from "../util/buffer_xor";
+import { createHash } from "crypto";
 
-type ConflictType = "U/U" | "U/R" | "U/D" | "D/D";
+type ConflictType = "U/U" | "R/U" | "R*/U" | "U/D" | "D/D";
 
 export abstract class Delta {
   readonly hash: Buffer;
@@ -26,7 +28,7 @@ export abstract class Delta {
 
   serialize(): any {
     return {
-      hash: this.hash.toString('base64'),
+      hash: this.hash.toString('hex'),
       type: this.constructor.name,
     }
   }
@@ -54,6 +56,7 @@ export class NodeCreation extends PrimitiveDelta {
   // Inverse dependencies
   readonly outgoingEdges: Map<string, NewEdge> = new Map();
   readonly incomingEdges: EdgeUpdate[] = []; // all deltas EVER that set this node as target of an edge.
+  readonly readAllOutgoing: ReadAllOutgoing[] = [];
   readonly deletions: NodeDeletion[] = [];
 
   constructor(hash: Buffer, id: UUID) {
@@ -65,11 +68,12 @@ export class NodeCreation extends PrimitiveDelta {
     return [];
   }
 
-  createOutgoingEdge(label: string): NewEdge {
-    return this.outgoingEdges.get(label) || (() => {
-      const ovr = new NewEdge(this, label);
-      this.outgoingEdges.set(label, ovr);
-      return ovr;
+  createOutgoingEdge(label: string, after: ReadAllOutgoing[] = []): NewEdge {
+    const edgeId = label + ':' + buffersXOR(...after.map(a => a.hash)).toString('hex');
+    return this.outgoingEdges.get(edgeId) || (() => {
+      const newEdge = new NewEdge(this, label, after, edgeId);
+      this.outgoingEdges.set(edgeId, newEdge);
+      return newEdge;
     })();
   }
 
@@ -78,6 +82,11 @@ export class NodeCreation extends PrimitiveDelta {
     for (const d of this.deletions) {
       registerConflict(d, u, "U/D");
     }
+    for (const r of this.readAllOutgoing) {
+      if (!(u.overwrites as NewEdge).after.includes(r)) {
+        registerConflict(r, u, "R*/U");
+      }
+    }
     // outgoing edge already stored in Edge.
   }
 
@@ -90,7 +99,7 @@ export class NodeCreation extends PrimitiveDelta {
   }
 
   registerDeletion(d: NodeDeletion) {
-    // A new deletion will conflict with all earliest incoming/outgoing edge operations that the deletion does not explicitly depend on.
+    // A new deletion will conflict with all earliest incoming/outgoing edge operations that the deletion does not transitively explicitly depend on.
     for (const o of this.outgoingEdges.values()) {
       for (const u of o.iterOverwriters(u => !d.afterSrc.some(a => a.hasTransitiveDependency(u)))) {
         registerConflict(d, u, "U/D");
@@ -99,7 +108,7 @@ export class NodeCreation extends PrimitiveDelta {
     for (const i of this.incomingEdges.filter(i => !d.afterTgt.some(a => a.hasTransitiveDependency(i)))) {
       registerConflict(i, d, "U/D");
     }
-    // NodeDeletion/delete conflict
+    // Delete/Delete conflict
     for (const other of this.deletions) {
       if (other !== d) {
         registerConflict(d, other, "D/D");
@@ -107,12 +116,49 @@ export class NodeCreation extends PrimitiveDelta {
     }
     this.deletions.push(d);
   }
+  
+  registerReadAllOutgoing(r: ReadAllOutgoing) {
+    // ReadAllOutgoing will conflict with all outgoing edge creations that are not a dependency of ReadAllOutgoing
+    // (in essence just a R/W conflict)
+    for (const o of this.outgoingEdges.values()) {
+      if (!r.after.includes(o)) {
+        // TODO: We could turn NewEdge into an actual delta type. Then we could just conflict with that delta (possibly more efficient, maybe more elegant)
+        for (const u of o.overwrittenBy) {
+          registerConflict(r, u, "R*/U");
+        }
+      }
+    }
+    this.readAllOutgoing.push(r);
+  }
 
   serialize(): any {
     return {...super.serialize(), id: this.id};
   }
 }
 
+// This delta represents getting all outgoing edges of a node.
+// For instance, when getting all the elements of a set (e.g., when checking some constraint), or iterating over a dictionary.
+export class ReadAllOutgoing extends PrimitiveDelta {
+  // Dependencies:
+  readonly node: NodeCreation;
+  readonly after: readonly NewEdge[];
+
+  constructor(hash: Buffer, node: NodeCreation, after: NewEdge[]) {
+    super(hash, `R*(${node.id})`);
+    this.node = node;
+    this.after = after;
+    // Register inverse dependencies:
+    node.registerReadAllOutgoing(this);
+  }
+
+  getDependencies(): [Delta, string][] {
+    return [
+      [this.node, "N"],
+      ...([] as [Delta,string][]).concat(...this.after.map(a => a.overwrittenBy.map(u => [u, "A"] as [Delta, string]))),
+    ];
+  }
+}
+
 export class NodeDeletion extends PrimitiveDelta {
   // Dependencies:
   node: NodeCreation;
@@ -147,9 +193,9 @@ export class NodeDeletion extends PrimitiveDelta {
   serialize(): any {
     return {
       ...super.serialize(),
-      node: this.node.hash.toString('base64'),
-      afterSrc: this.afterSrc.map(u => u.hash.toString('base64')),
-      afterTgt: this.afterTgt.map(u => u.hash.toString('base64')),
+      node: this.node.hash.toString('hex'),
+      afterSrc: this.afterSrc.map(u => u.hash.toString('hex')),
+      afterTgt: this.afterTgt.map(u => u.hash.toString('hex')),
     };
   }
 }
@@ -160,8 +206,8 @@ export abstract class Edge {
   readonly label: string;
 
   // Inverse dependencies
-  readonly concurrentlyWrittenBy: EdgeUpdate[] = [];
-  readonly concurrentlyReadBy: EdgeUpdate[] = [];
+  readonly overwrittenBy: EdgeUpdate[] = [];
+  readonly readBy: EdgeUpdate[] = [];
 
   constructor(source: NodeCreation, label: string) {
     this.source = source;
@@ -174,7 +220,7 @@ export abstract class Edge {
   // Iterate over all overwriters, from early to late, depth-first.
   // When in some branch the 'condition' is satisfied, the satisfying element is yielded, and the descendants ignored.
   *iterOverwriters(filter: (u: EdgeUpdate) => boolean): Iterable<EdgeUpdate> {
-    for (const ovr of this.concurrentlyWrittenBy) {
+    for (const ovr of this.overwrittenBy) {
       if (filter(ovr)) {
         yield ovr;
       }
@@ -185,37 +231,42 @@ export abstract class Edge {
   }
 
   registerWrite(write: EdgeUpdate) {
-    for (const other of this.concurrentlyWrittenBy) {
+    for (const other of this.overwrittenBy) {
       // A write conflicts with all other writes:
         registerConflict(write, other, "U/U");
     }
-    for (const read of this.concurrentlyReadBy) {
+    for (const read of this.readBy) {
       if (read !== write) {
         // A write conflicts with all reads:
-        registerConflict(read, write, "U/R");
+        registerConflict(read, write, "R/U");
       }
     }
-    this.concurrentlyWrittenBy.push(write);
+    this.overwrittenBy.push(write);
 
     // Also check conflicts with deletions of source:
     this.source.registerOutgoingEdge(write);
   }
 
   registerRead(read: EdgeUpdate) {
-    for (const write of this.concurrentlyWrittenBy) {
+    for (const write of this.overwrittenBy) {
       if (read !== write) {
         // A read conflicts with all writes:
-        registerConflict(read, write, "U/R");
+        registerConflict(read, write, "R/U");
       }
     }
-    this.concurrentlyReadBy.push(read);
+    this.readBy.push(read);
   }
 }
 
 // An Edge that does not yet have a target
 export class NewEdge extends Edge {
-  constructor(source: NodeCreation, label: string) {
+  readonly after: readonly ReadAllOutgoing[];
+  readonly edgeId: string;
+
+  constructor(source: NodeCreation, label: string, after: readonly ReadAllOutgoing[], edgeId: string) {
     super(source, label);
+    this.after = after;
+    this.edgeId = edgeId;
   }
 
   getDependencies(): [Delta,string][] {
@@ -225,7 +276,7 @@ export class NewEdge extends Edge {
   serialize() {
     return {
       type: "NewEdge",
-      source: this.source.hash.toString('base64'),
+      source: this.source.hash.toString('hex'),
       label: this.label,
     };
   }
@@ -247,7 +298,7 @@ export class ExistingEdge extends Edge {
   serialize() {
     return {
       type: "ExistingEdge",
-      overwrites: this.delta.hash.toString('base64'),
+      overwrites: this.delta.hash.toString('hex'),
     };
   }
 }
@@ -273,7 +324,7 @@ export class TargetNode implements Target {
     return [[this.value, "TGT"]];
   }
   serialize() {
-    return {type: "TargetNode", node: this.value.hash.toString('base64')};
+    return {type: "TargetNode", node: this.value.hash.toString('hex')};
   }
 }
 
@@ -321,8 +372,14 @@ export class EdgeUpdate extends PrimitiveDelta {
     return this.overwritable;
   }
 
+  // Makes code slightly easier to read
+  read() {
+    return this.overwritable;
+  }
+
   getDependencies(): [Delta,string][] {
     return this.overwrites.getDependencies()
+      .concat(...this.reads.map(r => r.getDependencies()))
       .concat(this.target.getDependencies());
   }
 
@@ -336,6 +393,30 @@ export class EdgeUpdate extends PrimitiveDelta {
   }
 }
 
+// export class EdgeRead extends PrimitiveDelta {
+//   // Every read has a unique ID (otherwise, different concurrent reads would be represented by the same delta)
+//   readonly id: UUID;
+
+//   // Dependencies
+//   readonly reads: ExistingEdge;
+
+//   constructor(hash: Buffer, id: UUID, reads: ExistingEdge) {
+//     super(hash, `R(${reads.label})`);
+//     // Record our own dependencies:
+//     this.reads = reads;
+
+//     reads.registerRead(this);
+//   }
+
+//   serialize(): any {
+//     return {
+//       ...super.serialize(),
+//       id: this.id,
+//       reads: this.reads.map(r => r.serialize()),
+//     };
+//   }
+// }
+
 export class Transaction extends Delta {
   readonly deltas: Delta[];
   readonly dependencies: Transaction[];
@@ -376,9 +457,9 @@ export class Transaction extends Delta {
   serialize(): any {
     return {
       ...super.serialize(),
-      deltas: this.deltas.map(d => d.hash.toString('base64')),
+      deltas: this.deltas.map(d => d.hash.toString('hex')),
       description: this.description,
-      dependencies: this.dependencies.map(d => d.hash.toString('base64')),
+      dependencies: this.dependencies.map(d => d.hash.toString('hex')),
     };
   }
 

+ 15 - 6
src/onion/delta_registry.ts

@@ -1,8 +1,8 @@
-import {Delta, Transaction, findTxDependencies, NodeCreation, NodeDeletion, EdgeUpdate, Edge, ExistingEdge, TargetValue, TargetNode} from "./delta";
+import {Delta, Transaction, findTxDependencies, NodeCreation, NodeDeletion, EdgeUpdate, Edge, ExistingEdge, TargetValue, TargetNode, NewEdge, ReadAllOutgoing} from "./delta";
 import {PrimitiveValue, UUID} from "./types";
 import {createHash} from "crypto";
 import {Buffer} from "buffer";
-import {buffersXOR} from "./buffer_xor";
+import {buffersXOR} from "../util/buffer_xor";
 
 function string2Hash(data: string): Buffer {
   return createHash('sha256')
@@ -28,10 +28,10 @@ export class DeltaRegistry {
 
   // Given the expected hash 
   private createIdempotent<T extends Delta>(hash: Buffer, callback: () => T): T {
-    const base64 = hash.toString('base64');
-    return this.deltas.get(base64) as T || (() => {
+    const hex = hash.toString('hex');
+    return this.deltas.get(hex) as T || (() => {
       const delta = callback();
-      this.deltas.set(base64, delta);
+      this.deltas.set(hex, delta);
       return delta;
     })();
   }
@@ -44,6 +44,15 @@ export class DeltaRegistry {
     return this.createIdempotent(hash, () => new NodeCreation(hash, id));
   }
 
+  newReadAllOutgoing(node: NodeCreation, after: NewEdge[]): ReadAllOutgoing {
+    const hash = createHash('sha256')
+      .update('node=')
+      .update(getHash(node))
+      .update(buffersXOR(...after.map(a => createHash('sha256').update(a.edgeId).digest())))
+      .digest();
+    return this.createIdempotent(hash, () => new ReadAllOutgoing(hash, node, after));
+  }
+
   newNodeDeletion(creation: NodeCreation, afterSrc: EdgeUpdate[], afterTgt: EdgeUpdate[]): NodeDeletion {
     const hash = createHash('sha256')
       .update('deletes=')
@@ -67,7 +76,7 @@ export class DeltaRegistry {
       .update('target=')
       .update(getHash(wrappedTarget))
       .digest();
-    return this.createIdempotent(hash, () => new EdgeUpdate(hash, overwrites, wrappedTarget, reads));
+      return this.createIdempotent(hash, () => new EdgeUpdate(hash, overwrites, wrappedTarget, reads));
   }
 
   newTransaction(deltas: Array<Delta>, description: string, dependencies: Array<Transaction> = findTxDependencies(deltas)): Transaction {

+ 5 - 5
src/onion/graph_state.test.ts

@@ -66,9 +66,9 @@ describe("GraphState", () => {
 
     graphState.exec(comp1, myListener);
 
-    assert(graphState.nodes.get(nodeId)!.outgoing.get("x") !== undefined, "Expected outgoing edge 'x' for node " + nodeId);
+    assert(graphState.nodes.get(nodeId)!.outgoingDeltas.get("x") !== undefined, "Expected outgoing edge 'x' for node " + nodeId);
 
-    assert((graphState.nodes.get(nodeId)!.getOutgoingEdges().get("x") as IValueState).value === 42, "Expected value of outgoing edge 'x' to be 42");
+    assert((graphState.nodes.get(nodeId)!.outgoing.get("x") as IValueState).value === 42, "Expected value of outgoing edge 'x' to be 42");
 
     const deltasForDelete = nodeState!.getDeltasForDelete(registry);
     console.log({deltasForDelete})
@@ -76,13 +76,13 @@ describe("GraphState", () => {
 
     graphState.exec(comp2, myListener);
 
-    console.log(nodeState!.getOutgoingEdges().get("x"))
-    // assert(nodeState!.getOutgoingEdges().get("x") === null, "Exepected no outgoing edge 'x'");
+    console.log(nodeState!.outgoing.get("x"))
+    // assert(nodeState!.outgoing.get("x") === null, "Exepected no outgoing edge 'x'");
 
 
     graphState.unexec(comp2, myListener);
 
-    // assert((graphState.nodes.get(nodeId)!.getOutgoingEdges().get("x") as IValueState).value === 42, "Expected value of outgoing edge 'x' to be 42");
+    // assert((graphState.nodes.get(nodeId)!.outgoing.get("x") as IValueState).value === 42, "Expected value of outgoing edge 'x' to be 42");
 
 
     // assert(graphState.nodes.size === 0, "Expected no more nodes in graphState");

+ 16 - 88
src/onion/graph_state.ts

@@ -4,7 +4,7 @@ import {
   NodeCreation,
   NodeDeletion,
   EdgeUpdate,
-  Edge, NewEdge, ExistingEdge,
+  NewEdge, ExistingEdge,
   Target, TargetValue, TargetNode,
 } from "./delta";
 
@@ -142,8 +142,9 @@ export interface IValueState extends ICommon {
 export interface INodeState extends ICommon {
   readonly type: "node";
 
-  // mapping of label to NodeState
-  getOutgoingEdges(): Map<string, IValueState | INodeState>;
+  readonly outgoingDeltas: ReadonlyMap<string, EdgeUpdate>;
+
+  readonly outgoing: ReadonlyMap<string, IValueState | INodeState>;
 
   readonly creation: NodeCreation;
 
@@ -158,10 +159,10 @@ class NodeState extends Common implements INodeState {
   readonly creation: NodeCreation;
 
   // For every outgoing edge, the Delta that set this edge to its current value
-  readonly outgoing: Map<string, EdgeUpdate> = new Map();
+  readonly outgoingDeltas: Map<string, EdgeUpdate> = new Map();
 
   // All currently outgoing edges. Edges that were set to null will not be part of this mapping.
-  readonly outgoingStates: Map<string, IValueState | INodeState> = new Map();
+  readonly outgoing: Map<string, IValueState | INodeState> = new Map();
 
   isDeleted: boolean = false; // has the node been deleted?
 
@@ -186,13 +187,10 @@ class NodeState extends Common implements INodeState {
     listener.createLinkToNode(sourceId, label, this.creation.id);
   }
 
-  getOutgoingEdges(): Map<string, IValueState | INodeState> {
-    return this.outgoingStates;
-  }
 
   // Has no side effects - instead returns the deltas that capture the creation or update of the given outgoing edge
   getDeltaForSetEdge(registry: DeltaRegistry, label: string, target: NodeCreation | PrimitiveValue): EdgeUpdate {
-    const previousEdgeUpdate = this.outgoing.get(label);
+    const previousEdgeUpdate = this.outgoingDeltas.get(label);
     if (previousEdgeUpdate === undefined) {
       return registry.newEdgeUpdate(this.creation.createOutgoingEdge(label), target);
     }
@@ -204,7 +202,7 @@ class NodeState extends Common implements INodeState {
   // Has no side effects - instead returns the deltas that capture the deletion of this node (and its incoming+outgoing edges)
   getDeltasForDelete(registry: DeltaRegistry): (EdgeUpdate|NodeDeletion)[] {
     const [afterTgt, newDeltas] = this.getIncomingEdgeDependenciesForDelete(registry);
-    const afterSrc = [...this.outgoing.values()].map(u => {
+    const afterSrc = [...this.outgoingDeltas.values()].map(u => {
       return registry.newEdgeUpdate(u.overwrite(), null);
     })
     const nodeDeletion = registry.newNodeDeletion(this.creation, afterSrc, afterTgt);
@@ -391,19 +389,6 @@ export class GraphState {
     if (nodeState === undefined) {
       throw new Error("Assertion failed: deleted node does not exist")
     }
-    // For every outgoing edge of deleted node, replace in the target node the incoming edge operation by the deletion:
-    // for (const outgoingEdgeOperation of nodeState.outgoing.values()) {
-    //   const target = outgoingEdgeOperation.target;
-    //   const targetState = this._getEdgeTargetState(target);
-    //   if (targetState !== undefined) {
-    //     targetState.replaceIncoming(outgoingEdgeOperation, delta, listener);
-    //     const outgoingEdgeCreation = outgoingEdgeOperation.getCreation();
-    //     const sourceId = outgoingEdgeCreation.source.id;
-    //     const label = outgoingEdgeCreation.label;
-    //     targetState.incomingStates.splice(targetState.incomingStates.findIndex(([l,s]) => l===label && s===nodeState), 1);
-    //     listener.deleteLink(sourceId, label);
-    //   }
-    // }
     nodeState.isDeleted = true;
     listener.deleteNode(id);
   }
@@ -416,64 +401,7 @@ export class GraphState {
     }
     nodeState.isDeleted = false;
     listener.createNode(nodeState);
-    // For every outgoing edge of deleted node, restore in the target node the incoming edge operation by whatever was there before
-    // for (const outgoingEdgeOperation of nodeState.outgoing.values()) {
-    //   const target = outgoingEdgeOperation.target;
-    //   const targetState = this._getEdgeTargetState(target);
-    //   if (targetState !== undefined) {
-    //     targetState.replaceIncoming(delta, outgoingEdgeOperation, listener);
-    //     const outgoingEdgeCreation = outgoingEdgeOperation.getCreation();
-    //     const sourceId = outgoingEdgeCreation.source.id;
-    //     const label = outgoingEdgeCreation.label;
-    //     targetState.incomingStates.push([label, nodeState]);
-    //     targetState.createLinkTo(sourceId, label, listener);
-    //   }
-    // }
-  }
-
-  // private execEdgeCreation(delta: EdgeCreation, listener: GraphStateListener) {
-  //   // console.log("execEdgeCreation", delta)
-  //   const sourceId = delta.source.id;
-  //   const target = delta.target.getTarget();
-  //   if (target === null) {
-  //     throw new Error("Assertion failed: EdgeCreation never sets edge target to null.");
-  //   }
-  //   const targetState = this._getEdgeTargetState(target);
-  //   const label = delta.label;
-  //   const sourceState = this.nodes.get(sourceId);
-  //   if (sourceState === undefined) {
-  //     throw new Error("Assertion failed: Source node is non-existing.")
-  //   }
-  //   if (targetState === undefined) {
-  //     throw new Error("Assertion failed: Target node is non-existing.");
-  //   }
-  //   sourceState.outgoing.set(label, delta);
-  //   sourceState.outgoingStates.set(label, targetState);
-  //   targetState.addIncoming(delta, listener);
-  //   targetState.incomingStates.push([label, sourceState]);
-  //   targetState.createLinkTo(sourceId, label, listener);
-  // }
-  // private unexecEdgeCreation(delta: EdgeCreation, listener: GraphStateListener) {
-  //   const sourceId = delta.source.id;
-  //   const target = delta.target.getTarget();
-  //   if (target === null) {
-  //     throw new Error("Assertion failed: EdgeCreation never sets edge target to null.");
-  //   }
-  //   const label = delta.label;
-  //   const sourceState = this.nodes.get(sourceId);
-  //   const targetState = this._getEdgeTargetState(target);
-  //   if (sourceState === undefined) {
-  //     throw new Error("Assertion failed: Source node is non-existing.")
-  //   }
-  //   if (targetState === undefined) {
-  //     throw new Error("Assertion failed: Target node is non-existing.");
-  //   }
-  //   sourceState.outgoing.delete(label);
-  //   sourceState.outgoingStates.delete(label);
-  //   targetState.removeIncoming(delta, listener);
-  //   targetState.incomingStates.splice(targetState.incomingStates.findIndex(([l,s]) => l===label && s===sourceState), 1);
-  //   listener.deleteLink(sourceId, label);
-  // }
+  }
 
   private execEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
     const edge = delta.overwrites;
@@ -507,14 +435,14 @@ export class GraphState {
     const newTargetState = this._getEdgeTargetState(newTarget);
     if (newTargetState === undefined) {
       console.log("TODO: Check: Should this only be allowed when new target is null?");
-      sourceState.outgoingStates.delete(label);
+      sourceState.outgoing.delete(label);
     }
     else {
       newTargetState.addIncoming(label, delta, sourceState, listener);
       newTargetState.createLinkTo(sourceId, label, listener);
-      sourceState.outgoingStates.set(label, newTargetState);
+      sourceState.outgoing.set(label, newTargetState);
     }
-    sourceState.outgoing.set(label, delta);
+    sourceState.outgoingDeltas.set(label, delta);
   }
   private unexecEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
     const edge = delta.overwrites;
@@ -540,8 +468,8 @@ export class GraphState {
     // Add edge to old target
     if (edge instanceof NewEdge) {
       // Nothing was overwritten
+      sourceState.outgoingDeltas.delete(label);
       sourceState.outgoing.delete(label);
-      sourceState.outgoingStates.delete(label);
     }
     else if (edge instanceof ExistingEdge) {
       const overwrittenUpdate = edge.delta;
@@ -549,14 +477,14 @@ export class GraphState {
       const oldTargetState = this._getEdgeTargetState(oldTarget);
       if (oldTargetState === undefined) {
         console.log("TODO: Check: Should this only be allowed when old target is null?");
-        sourceState.outgoingStates.delete(label);
+        sourceState.outgoing.delete(label);
       }
       else {
         oldTargetState.incomingAgain(label, overwrittenUpdate, sourceState, delta, listener);
         oldTargetState.createLinkTo(sourceId, label, listener);
-        sourceState.outgoingStates.set(label, oldTargetState);
+        sourceState.outgoing.set(label, oldTargetState);
       }
-      sourceState.outgoing.set(label, overwrittenUpdate);
+      sourceState.outgoingDeltas.set(label, overwrittenUpdate);
     }
   }
 }

+ 17 - 17
src/onion/version.ts

@@ -2,7 +2,7 @@ import {inspect} from "util"; // NodeJS library
 import { Buffer } from "buffer"; // NodeJS library
 import {findDFS} from "../util/dfs";
 import {permutations} from "../util/permutations";
-import {bufferXOR} from "./buffer_xor";
+import {bufferXOR} from "../util/buffer_xor";
 import {PrimitiveDelta, Transaction} from "./delta";
 
 // import * as _ from "lodash";
@@ -164,7 +164,7 @@ export class Version {
     const versions = [];
     this.serializeInternal(new Set(alreadyHave), new Set<Delta>(), deltas, versions);
     return {
-      externalDependencies: [...alreadyHave].map(v => v.hash.toString('base64')),
+      externalDependencies: [...alreadyHave].map(v => v.hash.toString('hex')),
       deltas,
       versions,
     };
@@ -180,9 +180,9 @@ export class Version {
       version.serializeInternal(alreadyHaveVersions, alreadyHaveDeltas, deltas, versions);
       const ovr = {};
       for (const [key,val] of overridings.entries()) {
-        ovr[key.hash.toString('base64')] = val.hash.toString('base64');
+        ovr[key.hash.toString('hex')] = val.hash.toString('hex');
       }
-      embeddings.push({guestId, v: version.hash.toString('base64'), ovr});
+      embeddings.push({guestId, v: version.hash.toString('hex'), ovr});
     }
 
     if (this.parents.length > 0) {
@@ -202,9 +202,9 @@ export class Version {
       visitDelta(delta);
 
       versions.push({
-        id: this.hash.toString('base64'),
-        parent: parentVersion.hash.toString('base64'),
-        delta: delta.hash.toString('base64'),
+        id: this.hash.toString('hex'),
+        parent: parentVersion.hash.toString('hex'),
+        delta: delta.hash.toString('hex'),
         embeddings,
       })
     }
@@ -228,24 +228,24 @@ export class VersionRegistry {
 
   // Maps version ID (as string, because a Buffer cannot be a map key) to Version
   readonly versionMap: Map<string, Version> = new Map([
-    [initialHash.toString('base64'), this.initialVersion], // the initial version, always already there
+    [initialHash.toString('hex'), this.initialVersion], // the initial version, always already there
   ]);
 
   lookupOptional(hash: Buffer): Version | undefined {
-    return this.versionMap.get(hash.toString('base64'));
+    return this.versionMap.get(hash.toString('hex'));
   }
 
   lookup(hash: Buffer): Version {
-    const base64 = hash.toString('base64');
-    const version = this.versionMap.get(base64);
+    const hex = hash.toString('hex');
+    const version = this.versionMap.get(hex);
     if (version === undefined) {
-      throw new Error("no such version: " + base64);
+      throw new Error("no such version: " + hex);
     }
     return version;
   }
 
   private putVersion(hash: Buffer, version: Version) {
-    this.versionMap.set(hash.toString('base64'), version);
+    this.versionMap.set(hash.toString('hex'), version);
   }
 
   // Idempotent
@@ -500,7 +500,7 @@ export class VersionRegistry {
       function printIndent(...args) {
         printDebug("  ".repeat(depth), ...args);
       }
-      printIndent("deltasToTry=", ...candidates.map(([d])=>d));
+      // printIndent("deltasToTry=", ...candidates.map(([d])=>d));
 
       let couldNotRecurse = true;
 
@@ -536,7 +536,7 @@ export class VersionRegistry {
             }
             embeddings.set(guestId, {version: nextGuestVersion, overridings});
           }
-          printIndent("Creating version (", delta, ...[...startVersion].map(d => d), ") with embeddings", embeddings);
+          // printIndent("Creating version (", delta, ...[...startVersion].map(d => d), ") with embeddings", embeddings);
           const nextVersion = this.createVersion(startVersion, delta, newVersion => {
             // add self-embeddings:
             for (const guestId of selfEmbeddingKeys) {
@@ -544,7 +544,7 @@ export class VersionRegistry {
             }
             return embeddings;
           });
-          printIndent("Created version (", ...[...nextVersion].map(d => d), ") with embeddings", embeddings);
+          // printIndent("Created version (", ...[...nextVersion].map(d => d), ") with embeddings", embeddings);
           return nextVersion;
         }
 
@@ -571,7 +571,7 @@ export class VersionRegistry {
       if (couldNotRecurse) {
         // possibly have a new maximal version
         if (!result.some(v => startVersion.isSubSetOf(v))) {
-          printIndent("new result");
+          // printIndent("new result");
           result.push(startVersion);
         }
       }

+ 3 - 3
src/onion/version_parser.ts

@@ -15,7 +15,7 @@ export class VersionParser {
 
   load({externalDependencies, deltas, versions}, onLoadDelta: (Delta) => void, onLoadVersion: (Version) => void) {
     for (const e of externalDependencies) {
-      if (this.versionRegistry.lookupOptional(Buffer.from(e, 'base64')) === undefined) {
+      if (this.versionRegistry.lookupOptional(Buffer.from(e, 'hex')) === undefined) {
         throw new Error("Cannot load versions: missing dependency: " + e);
       }
     }
@@ -25,7 +25,7 @@ export class VersionParser {
     }
 
     for (const {id, delta, parent, embeddings} of versions) {
-      const parentVersion = this.versionRegistry.lookup(Buffer.from(parent, 'base64'));
+      const parentVersion = this.versionRegistry.lookup(Buffer.from(parent, 'hex'));
       const parentDelta = this.deltaParser.deltaRegistry.deltas.get(delta)!;
       const theEmbeddings = new Map();
       const selfEmbeddingKeys = new Set<string>();
@@ -35,7 +35,7 @@ export class VersionParser {
           selfEmbeddingKeys.add(guestId);
         }
         else {
-          const guestVersion = this.versionRegistry.lookupOptional(Buffer.from(v, 'base64'));
+          const guestVersion = this.versionRegistry.lookupOptional(Buffer.from(v, 'hex'));
           for (const [key,val] of Object.entries(ovr)) {
             const guestDelta = this.deltaParser.deltaRegistry.deltas.get(key as string);
             const hostDelta = this.deltaParser.deltaRegistry.deltas.get(val as string);

+ 9 - 9
src/parser/rountangle_parser.ts

@@ -35,10 +35,10 @@ export function getGeometry(sourceState: GraphState, nodeId: PrimitiveValue): Ge
   if (node !== undefined) {
     try {
       return {
-        x: (node.getOutgoingEdges().get("x")!.asTarget()) as number,
-        y: (node.getOutgoingEdges().get("y")!.asTarget()) as number,
-        w: (node.getOutgoingEdges().get("width")!.asTarget()) as number,
-        h: (node.getOutgoingEdges().get("height")!.asTarget()) as number,
+        x: (node.outgoing.get("x")!.asTarget()) as number,
+        y: (node.outgoing.get("y")!.asTarget()) as number,
+        w: (node.outgoing.get("width")!.asTarget()) as number,
+        h: (node.outgoing.get("height")!.asTarget()) as number,
       };
     }
     catch(e) {}
@@ -131,7 +131,7 @@ export class RountangleParser  {
           // Follow 'cs'/'as' edges in correspondence model to find the corresponding node:
           const sourceNodeState = corrState.nodes.get(sourceId)!;
           const [_, corrNodeState] = sourceNodeState.getIncomingEdges().find(([label, nodeState]) => label === corr2SourceLabel)!;
-          const targetNodeState = corrNodeState.getOutgoingEdges().get(corr2TargetLabel) as INodeState;
+          const targetNodeState = corrNodeState.outgoing.get(corr2TargetLabel) as INodeState;
 
           // Deletion of correspondence node, and its outgoing ('cs', 'as') edges:
           const corrDeletion = corrNodeState.getDeltasForDelete(this.deltaRegistry);
@@ -187,7 +187,7 @@ export class RountangleParser  {
               throw new Error("No incoming 'cs' edge.");
             }
             const [_, corrNodeState] = pair;
-            const asState = corrNodeState.getOutgoingEdges().get(reverse?"cs":"as");
+            const asState = corrNodeState.outgoing.get(reverse?"cs":"as");
             if (asState === undefined) {
               throw new Error("Found correspondence node, but it has no outgoing 'as' edge.")
             }
@@ -227,7 +227,7 @@ export class RountangleParser  {
                   }
                 }
                 if (smallestParent !== null) {
-                  const existingLink = asNodeState.getOutgoingEdges().get("hasParent");
+                  const existingLink = asNodeState.outgoing.get("hasParent");
                   if (existingLink !== smallestParent) {
                     // console.log("updated geometry is on inside...");
                     const asParentLink = asNodeState.getDeltaForSetEdge(this.deltaRegistry, "hasParent", smallestParent.asTarget());
@@ -239,7 +239,7 @@ export class RountangleParser  {
 
               // 1. Check if existing parent links still hold
               // 1.a. outgoing parent links
-              const otherAsNodeState = updatedAsNode.getOutgoingEdges().get("hasParent");
+              const otherAsNodeState = updatedAsNode.outgoing.get("hasParent");
               if (otherAsNodeState !== undefined && otherAsNodeState.type === "node") {
                 const otherCsNodeState = findCorrespondingAsNode(otherAsNodeState, true);
                 const otherNodeId = otherCsNodeState.creation.id;
@@ -291,7 +291,7 @@ export class RountangleParser  {
                   // CORRECT: For geometries that are inside of our updated geometry, we should only "steal" their outgoing parent link if our updated geometry is smaller than their current parent.
                   if (outside) {
                     const otherAsNode = findCorrespondingAsNode(corrState.nodes.get(otherNodeId) as INodeState);
-                    const otherCurrentParent = otherAsNode.getOutgoingEdges().get("hasParent");
+                    const otherCurrentParent = otherAsNode.outgoing.get("hasParent");
                     const otherCurrentParentSurface = (() => {
                       // find surface area of existing parent of other geometry...
                       if (otherCurrentParent === undefined || otherCurrentParent.type !== "node") {