Przeglądaj źródła

May (not sure) have fixed all conflict types.

Joeri Exelmans 2 lat temu
rodzic
commit
af1cb0bdef

+ 23 - 10
src/frontend/editable_graph.tsx

@@ -24,7 +24,7 @@ import {
 interface NodeObjType {
   nodeCreation: NodeCreation;
   outgoing: Map<string, [EdgeCreation|EdgeUpdate, NodeObjType]>;
-  incoming: Array<[EdgeCreation|EdgeUpdate, NodeObjType]>;
+  incoming: Array<[EdgeCreation|EdgeUpdate|NodeDeletion, NodeObjType]>;
 }
 
 interface EditableGraphProps {
@@ -40,7 +40,7 @@ interface EditableGraphState {
 
 export class EditableGraph extends React.Component<EditableGraphProps, EditableGraphState> {
   graphRef: React.RefObject<Graph<NodeObjType,EdgeCreation|EdgeUpdate>> = React.createRef<Graph<NodeObjType,EdgeCreation|EdgeUpdate>>();
-  mouseDownNode: d3Types.d3Node<NodeObjType> | null;
+  mouseDownNode: d3Types.d3Node<NodeObjType> | null = null; // only while mouse button is down, does this record the d3Node that was under the cursor when the mouse went down.
 
   nextId: number = 0;
 
@@ -50,7 +50,6 @@ export class EditableGraph extends React.Component<EditableGraphProps, EditableG
 
   constructor(props) {
     super(props);
-    this.mouseDownNode = null;
     this.state = {};
     this.currentVersion = initialVersion;
   }
@@ -123,16 +122,30 @@ export class EditableGraph extends React.Component<EditableGraphProps, EditableG
       else if (event.button === 1) { // middle mouse button
         if (node) {
           // Delete node (and its incoming + outgoing edges):
-          // console.log("incoming:", node.obj.incoming);
+          console.log("incoming:", node.obj.incoming);
+          console.log("outgoing:", node.obj.outgoing);
           const incomingEdgeUnsettings = node.obj.incoming.map(([incomingEdge,sourceObj]) => {
-            const edgeUnset = new EdgeUpdate(incomingEdge, null);
-            sourceObj.outgoing.set(incomingEdge.getCreation().label, [edgeUnset,node.obj]);
-            return edgeUnset;
+            if (incomingEdge instanceof NodeDeletion) {
+              return incomingEdge;
+            }
+            else {
+              const edgeUnset = new EdgeUpdate(incomingEdge, null);
+              sourceObj.outgoing.set(incomingEdge.getCreation().label, [edgeUnset,node.obj]);
+              return edgeUnset;
+            }
+          });
+          const targetIncomings: Array<[EdgeCreation|EdgeUpdate|NodeDeletion, NodeObjType]>[] = [];
+          const deletedOutgoingEdges = [...node.obj.outgoing.values()].map(([outgoingEdge,targetObj]) => {
+            targetObj.incoming.splice(targetObj.incoming.findIndex(([d,_])=>d===outgoingEdge), 1);
+            targetIncomings.push(targetObj.incoming);
+            return outgoingEdge;
           });
-          // console.log("outgoing:", node.obj.outgoing);
-          const delta = new NodeDeletion(node.obj.nodeCreation, [...incomingEdgeUnsettings, ...[...node.obj.outgoing.values()].map(([edge,_])=>edge)]);
+          const nodeDeletion = new NodeDeletion(node.obj.nodeCreation, [...incomingEdgeUnsettings, ...deletedOutgoingEdges]);
+          for (const targetIncoming of targetIncomings) {
+            targetIncoming.push([nodeDeletion, node.obj]);
+          }
           this.graphRef.current.deleteNode(node.id);
-          const tx = this.compositeLvl.createComposite([delta, ...incomingEdgeUnsettings]);
+          const tx = this.compositeLvl.createComposite([nodeDeletion, ...incomingEdgeUnsettings.filter(d=>!(d instanceof NodeDeletion))]);
           this.currentVersion = this.versionRegistry.createVersion(this.currentVersion, tx);
           this.props.newDeltaHandler(tx);
           this.props.newVersionHandler(this.currentVersion);

+ 5 - 3
src/onion/primitive_delta.test.ts

@@ -192,7 +192,7 @@ describe("Delta", () => {
 
 
   // Not yet supported
-  it("Delete source and target of edge", () => {
+  it("Delete source and target of edge (no conflict)", () => {
     const getId = mockUuid();
 
     const sourceCreation = new NodeCreation(getId());
@@ -200,9 +200,11 @@ describe("Delta", () => {
     const edgeCreation = new EdgeCreation(sourceCreation, "label", targetCreation);
     const sourceDeletion = new NodeDeletion(sourceCreation, [edgeCreation]);
 
-    assert(sourceDeletion.getConflicts().length === 0, "expected no conflicts far");
+    assert(sourceDeletion.getConflicts().length === 0, "expected no conflicts");
 
-    // const targetDeletion = new NodeDeletion(targetCreation, [sourceDeletion]); // unsupported :(
+    const targetDeletion = new NodeDeletion(targetCreation, [sourceDeletion]); // unsupported :(
+
+    assert(targetDeletion.getConflicts().length === 0, "expected no conflicts");
   })
 
 });

+ 42 - 26
src/onion/primitive_delta.ts

@@ -1,6 +1,7 @@
 import {inspect} from "util"; // NodeJS library
 import {createHash} from "crypto";
 import {Buffer} from "buffer";
+import * as _ from "lodash";
 
 import {UUID} from "./types";
 import {bufferXOR} from "./buffer_xor";
@@ -67,7 +68,7 @@ export class NodeDeletion implements Delta {
   readonly deletedOutgoingEdges: Array<EdgeCreation | EdgeUpdate>;
 
   // Dependency: For every time the deleted node was target of an edge, the deletion depends on the EdgeUpdate that sets this edge to have a different target.
-  readonly afterIncomingEdges: Array<EdgeUpdate>;
+  readonly afterIncomingEdges: Array<EdgeUpdate | NodeDeletion>;
 
   // Conflicts: Concurrent deletion of the same node.
   deleteConflicts: Array<NodeDeletion> = [];
@@ -81,37 +82,46 @@ export class NodeDeletion implements Delta {
   // Conflicts: Concurrent creation/update of an edge with as target the deleted node.
   edgeTargetConflicts: Array<EdgeCreation | EdgeUpdate> = [];
 
-  // afterEdges: If the deleted node had any outgoing edges, then for every such edge, the most recent EdgeCreation/EdgeUpdate must be included as a dependency. If the deleted node had any incoming edges, then a follow-up EdgeUpdate that sets the target of that edge to another node (or null) must be included as a dependency.
-  constructor(creation: NodeCreation, afterEdges: Array<EdgeCreation | EdgeUpdate>) {
+  // afterOps: If the deleted node had any outgoing edges, then for every such edge, the most recent EdgeCreation/EdgeUpdate must be included as a dependency. If the deleted node had any incoming edges, then a follow-up EdgeUpdate that sets the target of that edge to another node (or null) must be included as a dependency.
+  constructor(creation: NodeCreation, afterOps: Array<EdgeCreation | EdgeUpdate | NodeDeletion>) {
     this.creation = creation;
     this.deletedOutgoingEdges = [];
     this.afterIncomingEdges = [];
 
+    if (_.uniq(afterOps).length !== afterOps.length) {
+      throw new Error("Assertion failed: afterOps contains duplicates.");
+    }
+
     // hash will be calculated based on all EdgeCreations/EdgeUpdates that we depend on, by XOR (insensitive to array order).
     const hash = createHash('sha256').update(this.creation.hash);
     let union = Buffer.alloc(32); // all zeroes
-    for (const afterEdge of afterEdges) {
-      union = bufferXOR(union, afterEdge.getHash());
+    for (const after of afterOps) {
+      union = bufferXOR(union, after.getHash());
     }
     this.hash = hash.update(union).digest();
 
-    // partition 'afterEdges' into outgoing and incoming edges, while checking some assertions:
-    for (const afterEdge of afterEdges) {
-      if (afterEdge.getCreation().source === this.creation) {
-        this.deletedOutgoingEdges.push(afterEdge);
+    // partition 'afterOps' into outgoing and incoming edges, while checking some assertions:
+    for (const after of afterOps) {
+      if (after instanceof NodeDeletion) {
+        this.afterIncomingEdges.push(after);
       }
       else {
-        if (!(afterEdge instanceof EdgeUpdate)) {
-          throw new Error("Assertion failed: Expected EdgeUpdate here.");
-        }
-        if (afterEdge.target.target === creation) {
-          throw new Error("Assertion failed: NodeDeletion must not have after-dependency on EdgeUpdate that sets target to deleted node.");
+        if (after.getCreation().source === this.creation) {
+          this.deletedOutgoingEdges.push(after);
         }
+        else {
+          if (!(after instanceof EdgeUpdate)) {
+            throw new Error("Assertion failed: Expected EdgeUpdate here.");
+          }
+          if (after.target.target === creation) {
+            throw new Error("Assertion failed: NodeDeletion must not have after-dependency on EdgeUpdate that sets target to deleted node.");
+          }
 
-        if (! [...afterEdge.iterUpdates()].some(e => e.target.target === this.creation)) {
-          throw new Error("Assertion failed: None of the EdgeUpdates of afterEdge set the target to the deleted node.");
+          if (! [...after.iterUpdates()].some(e => e.target.target === this.creation)) {
+            throw new Error("Assertion failed: None of the EdgeUpdates of after set the target to the deleted node.");
+          }
+          this.afterIncomingEdges.push(after);
         }
-        this.afterIncomingEdges.push(afterEdge);
       }
     }
 
@@ -144,17 +154,23 @@ export class NodeDeletion implements Delta {
       }
     }
 
+    function overwritesEdge(op: EdgeCreation | EdgeUpdate | NodeDeletion, edge: EdgeUpdate | EdgeCreation) {
+      if (op === edge) {
+        return true;
+      }
+      if (op instanceof EdgeUpdate) {
+        return overwritesEdge(op.overwrites, edge);
+      }
+      if (op instanceof NodeDeletion) {
+        return op.deletedOutgoingEdges.some(deletedEdge => overwritesEdge(deletedEdge, edge));
+      }
+      return false;
+    }
+
     // Concurrently updated incoming edges of this node
     for (const incomingEdge of this.creation.incomingEdges) {
       // every incoming edge of deleted node must have been overwritten by an EdgeUpdate that is an explicit dependency:
-      if (!this.afterIncomingEdges.some(edge => {
-        let current: EdgeUpdate | EdgeCreation = edge;
-        while (true) {
-          if (current === incomingEdge) return true;
-          if (current instanceof EdgeUpdate) current = current.overwrites;
-          else return false;
-        }
-      })) {
+      if (!this.afterIncomingEdges.some(edge => overwritesEdge(edge, incomingEdge))) {
         // Symmetric
         this.edgeTargetConflicts.push(incomingEdge);
         incomingEdge.target.deleteTargetConflicts.push(this);
@@ -207,7 +223,7 @@ export class NodeDeletion implements Delta {
 
   // pretty print to console under NodeJS
   [inspect.custom](depth: number, options: object) {
-    return "NodeDeletion{" + inspect(this.creation.id, options) + ",delEdges=" + this.deletedOutgoingEdges.map(e => inspect(e, options)).join(",") + ",afterEdges=" + this.afterIncomingEdges.map(e => inspect(e, options)).join(",") + "}";
+    return "NodeDeletion{" + inspect(this.creation.id, options) + ",delEdges=" + this.deletedOutgoingEdges.map(e => inspect(e, options)).join(",") + ",after=" + this.afterIncomingEdges.map(e => inspect(e, options)).join(",") + "}";
   }
 
   toString(): string {