|
@@ -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 {
|