Kaynağa Gözat

Versions & deltas: refactor a tiny bit, extend tests.

Joeri Exelmans 3 yıl önce
ebeveyn
işleme
72b7a05567
4 değiştirilmiş dosya ile 162 ekleme ve 72 silme
  1. 29 0
      src/onion/delta.ts
  2. 16 0
      src/onion/primitive_delta.ts
  3. 37 29
      src/onion/version.test.ts
  4. 80 43
      src/onion/version.ts

+ 29 - 0
src/onion/delta.ts

@@ -11,3 +11,32 @@ export interface Delta {
   // Returned value must be 8 bytes (256 bits).
   getHash(): Buffer;
 }
+
+export function isConflicting(a: Delta, b: Delta) {
+  return a.getConflicts().includes(b);
+}
+
+export function* iterConflicts(d: Delta, it: Iterable<Delta>) {
+  for (const conflictsWith of d.getConflicts()) {
+    for (const otherDelta of it) {
+      if (conflictsWith === otherDelta) {
+        yield conflictsWith;
+      }
+    }
+  }
+}
+
+export function* iterMissingDependencies(d: Delta, it: Iterable<Delta>) {
+  for (const dep of d.getDependencies()) {
+    let found = false;
+    for (const otherDelta of it) {
+      if (dep === otherDelta) {
+        found = true;
+        break;
+      }
+    }
+    if (!found) {
+      yield dep;
+    }
+  }
+}

+ 16 - 0
src/onion/primitive_delta.ts

@@ -45,6 +45,10 @@ export class NodeCreation implements Delta {
   [inspect.custom](depth: number, options: object) {
     return "NodeCreation{" + inspect(this.id, options) + "}";
   }
+
+  toString(): string {
+    return this[inspect.custom](0, {});
+  }
 }
 
 export class NodeDeletion implements Delta {
@@ -151,6 +155,10 @@ export class NodeDeletion implements Delta {
   [inspect.custom](depth: number, options: object) {
     return "NodeDeletion{" + inspect(this.creation.id, options) + ",edges=" + this.deletedEdges.map(e => inspect(e, options)).join(",") + "}";
   }
+
+  toString(): string {
+    return this[inspect.custom](0, {});
+  }
 }
 
 // Common functionality in EdgeCreation and EdgeUpdate: both set the target of an edge, and this can conflict with the deletion of the target.
@@ -266,6 +274,10 @@ export class EdgeCreation implements Delta {
   [inspect.custom](depth: number, options: object) {
     return "EdgeCreation{src=" + inspect(this.source.id, options) + ",tgt=" + inspect(this.target.target.id, options) + ",label=" + this.label + "}";
   }
+
+  toString(): string {
+    return this[inspect.custom](0, {});
+  }
 }
 
 export class EdgeUpdate implements Delta {
@@ -328,5 +340,9 @@ export class EdgeUpdate implements Delta {
   [inspect.custom](depth: number, options: object) {
     return "EdgeUpdate{ovr=" + inspect(this.overwrites, options) + ",tgt=" + inspect(this.target.target.id, options) + "}";
   }
+
+  toString(): string {
+    return this[inspect.custom](0, {});
+  }
 }
 

+ 37 - 29
src/onion/version.test.ts

@@ -1,6 +1,7 @@
 import * as _ from "lodash";
 
 import {
+  IVersion,
   VersionRegistry,
 } from "./version";
 
@@ -32,13 +33,9 @@ describe("Version", () => {
     const version1 = registry.createVersion(registry.initialVersion, nodeCreation);
     const version2 = registry.createVersion(version1, nodeDeletion);
 
-    // assert(_.isEqual(registry.initialVersion.getDeltas(), []), "expected initialVersion to be empty");
-    // assert(_.isEqual(version1.getDeltas(), [nodeCreation]), "expected version0 to be empty");
-    // assert(_.isEqual(version2.getDeltas(), [nodeCreation, nodeDeletion]), "expected version2 to contain creation and deletion");
-
-    assert(_.isEqual([... registry.initialVersion.iterDeltas()], []), "expected initialVersion to be empty");
-    assert(_.isEqual([... version1.iterDeltas()], [nodeCreation]), "expected version1 to contain creation");
-    assert(_.isEqual([... version2.iterDeltas()], [nodeDeletion, nodeCreation]), "expected version2 to contain creation and deletion");
+    assert(_.isEqual([... registry.initialVersion], []), "expected initialVersion to be empty");
+    assert(_.isEqual([... version1], [nodeCreation]), "expected version1 to contain creation");
+    assert(_.isEqual([... version2], [nodeDeletion, nodeCreation]), "expected version2 to contain creation and deletion");
   });
 
   it("Commutating operations yield equal versions", () => {
@@ -61,20 +58,22 @@ describe("Version", () => {
     const getId = mockUuid();
     const registry = new VersionRegistry();
 
-    const nodeCreationA = new NodeCreation(getId());
-    const nodeCreationB = new NodeCreation(getId());
+    const A = new NodeCreation(getId());
+    const B = new NodeCreation(getId());
+    const C = new NodeCreation(getId());
+    const D = new NodeDeletion(A, []);
 
-    const versionA = registry.createVersion(registry.initialVersion, nodeCreationA);
-    const versionB = registry.createVersion(registry.initialVersion, nodeCreationB);
+    const v1 = registry.quickVersion([D,B,A]);
+    const v2 = registry.quickVersion([C,B]);
 
-    const intersection0 = registry.getIntersection([versionA, versionB]);
-    assert(intersection0 === registry.initialVersion, "expected intersection of A and B to be initial (empty) version");
+    const intersection0 = registry.getIntersection([v1, v2]);
+    assert(intersection0 === registry.quickVersion([B]), "expected intersection of v1 and v2 to be B.");
 
-    const intersection1 = registry.getIntersection([versionA, versionA]);
-    assert(intersection1 === versionA, "expected intersection of A with itself to be A");
+    const intersection1 = registry.getIntersection([v1, v1]);
+    assert(intersection1 === v1, "expected intersection of v1 with itself to be v1");
 
-    const intersection2 = registry.getIntersection([versionA]);
-    assert(intersection2 === versionA, "expected intersection of A with itself to be A");
+    const intersection2 = registry.getIntersection([v1]);
+    assert(intersection2 === v1, "expected intersection of v1 with itself to be v1");
 
     const intersection3 = registry.getIntersection([]);
     assert(intersection3 === registry.initialVersion, "expected intersection of empty set to be initial (empty) version");
@@ -111,7 +110,7 @@ describe("Version", () => {
     const merged = registry.merge([versionA, versionB], nameMap);
     assert(merged.length === 1, "expected 1 merged version");
 
-    const deltas = [... merged[0].iterDeltas()];
+    const deltas = [... merged[0]];
     assert(deltas.length === 2
       && deltas.includes(nodeCreationA)
       && deltas.includes(nodeCreationB),
@@ -145,15 +144,15 @@ describe("Version", () => {
       [D, "D"],
     ]);
 
-    const three = registry.quickVersion([X,Y,Z,A]);
-    const seven = registry.quickVersion([X,Y,Z,C]);
-    const five  = registry.quickVersion([X,Y,Z,B,D]);
+    const three = registry.quickVersion([A,X,Y,Z]);
+    const seven = registry.quickVersion([C,X,Y,Z]);
+    const five  = registry.quickVersion([D,B,X,Y,Z]);
 
     const merged = registry.merge([three, seven, five], nameMap);
     assert(merged.length === 3, "expected three maximal versions");
-    assert(merged.includes(registry.quickVersion([X,Y,Z,A,C])), "expected [X,Y,Z,A,C] to be a maximal version");
-    assert(merged.includes(registry.quickVersion([X,Y,Z,B,C])), "expected [X,Y,Z,B,C] to be a maximal version");
-    assert(merged.includes(registry.quickVersion([X,Y,Z,B,D])), "expected [X,Y,Z,B,D] to be a maximal version");
+    assert(merged.includes(registry.quickVersion([A,C,X,Y,Z])), "expected [X,Y,Z,A,C] to be a maximal version");
+    assert(merged.includes(registry.quickVersion([B,C,X,Y,Z])), "expected [X,Y,Z,B,C] to be a maximal version");
+    assert(merged.includes(registry.quickVersion([D,B,X,Y,Z])), "expected [X,Y,Z,B,D] to be a maximal version");
 
     mergeAgain(registry, merged, nameMap);
   });
@@ -188,21 +187,30 @@ describe("Version", () => {
 
     const HOW_MANY = 3;
 
-    const deltas: Array<Delta> = [];
+    const creations: Array<Delta> = [];
+    const deletions: Array<Delta> = [];
+    const edges: Array<Delta> = [];
+    const versions: Array<IVersion> = [];
+
     const nameMap = new Map();
+
     for (let i=0; i<HOW_MANY; i++) {
       const creation = new NodeCreation(getId());
       const deletion = new NodeDeletion(creation, []);
       const edge = new EdgeCreation(creation, "l", creation); // conflicts with deletion0
-      deltas.push(creation);
-      deltas.push(deletion);
-      deltas.push(edge);
+
+      creations.push(creation);
+      deletions.push(deletion);
+      edges.push(edge);
+
       nameMap.set(creation, "C"+i.toString());
       nameMap.set(deletion, "D"+i.toString());
       nameMap.set(edge, "E"+i.toString());
+
+      versions.push(registry.quickVersion([deletion, creation]));
+      versions.push(registry.quickVersion([edge, creation]));
     }
 
-    const versions = deltas.map(d => registry.quickVersion([d]));
     const merged = registry.merge(versions, nameMap);
     assert(merged.length === Math.pow(2, HOW_MANY), HOW_MANY.toString() + " binary choices should result in " + Math.pow(2,HOW_MANY).toString() + " possible conflict resolutions and therefore merge results.");
 

+ 80 - 43
src/onion/version.ts

@@ -4,39 +4,40 @@ import * as _ from "lodash";
 
 import {
   Delta,
+  isConflicting,
+  iterMissingDependencies,
+  iterConflicts,
 } from "./delta";
 
 
 import {bufferXOR} from "./buffer_xor";
 
-// not exported -> use VersionRegistry to create Versions.
-class Version {
+export interface IVersion {
+  parents: Array<[IVersion, Delta]>;
+  hash: Buffer;
+  size: number;
+  [Symbol.iterator](): Iterator<Delta>;
+  [inspect.custom](depth: number, options: object): string;
+}
+
+// not exported -> use VersionRegistry to create versions
+class Version implements IVersion {
   readonly parents: Array<[Version, Delta]>;
 
   // Unique ID of the version - XOR of all delta hashes - guarantees that Versions with equal (unordered) sets of Deltas have the same ID.
   readonly hash: Buffer;
 
-  constructor(parents: Array<[Version, Delta]>, hash: Buffer) {
+  readonly size: number;
+
+  constructor(parents: Array<[Version, Delta]>, hash: Buffer, size: number) {
     this.parents = parents;
     this.hash = hash;
-
-    // if (parents.length > 0) {
-    //   const [parentVersion, delta] = parents[0];
-    //   this.hash = bufferXOR(parentVersion.hash, delta.getHash());
-
-    //   for (const [parentVersion, delta] of parents.slice(1)) {
-    //     if (! this.hash.equals(bufferXOR(parentVersion.hash, delta.getHash()))) {
-    //       throw new Error("Assertion failed: all paths to this version must yield same hash.");
-    //     }
-    //   }
-    // } else {
-    //   // Empty version has ID with all zeros
-    //   this.hash = Buffer.alloc(32);
-    // }
+    this.size = size;
   }
 
-  // Returns iterator that yields deltas, from recent to early.
-  *iterDeltas(): Iterable<Delta> {
+  // Returns iterator that yields all deltas of this version, from recent to early.
+  // Or put more precisely: a delta's dependencies will be yielded AFTER the delta itself.
+  *[Symbol.iterator](): Iterator<Delta> {
     let current: Version = this;
     while (current.parents.length !== 0) {
       const [parent, delta] = current.parents[0];
@@ -45,8 +46,8 @@ class Version {
     }
   }
 
-  [inspect.custom](depth: number, options: object) {
-    return "Version{" + [...this.iterDeltas()].map(d => inspect(d, options)).join(",") + "}";
+  [inspect.custom](depth: number, options: object): string {
+    return "Version{" + [...this].map(d => inspect(d, options)).join(",") + "}";
   }
 }
 
@@ -58,7 +59,7 @@ export class VersionRegistry {
 
   constructor() {
     const initialHash = Buffer.alloc(32); // all zeros
-    this.initialVersion = new Version([], initialHash);
+    this.initialVersion = new Version([], initialHash, 0);
     this.versionMap.set(initialHash.toString('base64'), this.initialVersion);
   }
 
@@ -78,7 +79,24 @@ export class VersionRegistry {
     this.versionMap.set(hash.toString('base64'), version);
   }
 
+  // Pre-condition 1: all of the dependencies of delta must exist in parent.
+  // Pre-condition 2: delta must be non-conflicting with any delta in parent.
   createVersion(parent: Version, delta: Delta): Version {
+    // Check pre-condition 1:
+    const missingDependency = iterMissingDependencies(delta, parent).next().value;
+    if (missingDependency !== undefined) {
+      throw new Error("Missing dependency: " + missingDependency.toString());
+    }
+    // Check pre-condition 2:
+    const conflictsWith = iterConflicts(delta, parent).next().value;
+    if (conflictsWith !== undefined) {
+      throw new Error("Delta " + delta.toString() + " conflicts with " + conflictsWith.toString());
+    }
+    return this.createVersionUnsafe(parent, delta);
+  }
+
+  // Faster than createVersion, but does not check pre-conditions.
+  createVersionUnsafe(parent: Version, delta: Delta): Version {
     const newHash = bufferXOR(parent.hash, delta.getHash());
     const existingVersion = this.lookupOptional(newHash);
     if (existingVersion !== undefined) {
@@ -90,28 +108,49 @@ export class VersionRegistry {
       }
       return existingVersion;
     } else {
-      const version = new Version([[parent, delta]], newHash);
+      const version = new Version([[parent, delta]], newHash, parent.size + 1);
       this.putVersion(newHash, version);
       return version;
     }
   }
 
+  // Order of deltas should be recent -> early
+  // Or put more precisely: a delta's dependencies should occur AFTER the delta in the array.
   quickVersion(deltas: Array<Delta>): Version {
-    let parent = this.initialVersion;
-    for (const delta of deltas) {
-      parent = this.createVersion(parent, delta);
-    }
-    return parent;
+    return deltas.reduceRight((parentVersion, delta) => this.createVersion(parentVersion, delta), this.initialVersion);
   }
 
   // Get the version whose deltas are a subset of all given versions. This is typically the 'common parent'.
   getIntersection(versions: Array<Version>): Version {
-    // const commonDeltas = this._getCommonDeltas(versions);
-    const commonDeltas = _.intersection(...versions.map(v => [...v.iterDeltas()]));
-    return this.quickVersion(commonDeltas);
-    // const hash = commonDeltas.map(d => d.getHash()).reduce((a, b) => bufferXOR(a,b), this.initialVersion.hash);
-    // const commonVersion = this.lookup(hash); // will never throw error: there's always a common parent
-    // return commonVersion;
+    // treat special case first:
+    if (versions.length === 0) {
+      return this.initialVersion;
+    }
+
+    // sort versions (out place) from few deltas to many (FASTEST):
+    const sortedVersions = versions.slice().sort((versionA,versionB) => versionA.size - versionB.size);
+    const intersection: Array<Delta> = [];
+    for (const delta of sortedVersions[0]) {
+      let allVersionsHaveIt = true;
+      for (let i=1; i<sortedVersions.length; i++) {
+        let thisVersionHasIt = false;
+        for (const otherDelta of sortedVersions[i]) {
+          if (delta === otherDelta) {
+            thisVersionHasIt = true;
+            break;
+          }
+        }
+        if (!thisVersionHasIt) {
+          allVersionsHaveIt = false;
+          break;
+        }
+      }
+      if (allVersionsHaveIt) {
+        intersection.push(delta);
+      }
+    }
+
+    return this.quickVersion(intersection);
   }
 
   // Of the union of all deltas of the versions given, compute the maximal left-closed conflict-free subsets.
@@ -131,10 +170,10 @@ export class VersionRegistry {
     }
 
     const commonVersion = this.getIntersection(versions);
-    const allDeltas = _.union(...versions.map(v => [...v.iterDeltas()]));
-    const diff = _.difference(allDeltas, [...commonVersion.iterDeltas()]);
+    const allDeltas = _.union(...versions.map(v => [...v]));
+    const diff = _.difference(allDeltas, [...commonVersion]);
 
-    printDebug("commonVersion:", ...commonVersion.iterDeltas());
+    printDebug("commonVersion:", ...commonVersion);
     printDebug("diff:", ...diff);
 
     const result: Array<Version> = [];
@@ -151,11 +190,9 @@ export class VersionRegistry {
       else {
         printIndent("deltasToTry=", ...deltasToTry);
 
-        const startVersionDeltas = [...startVersion.iterDeltas()];
-
         for (const delta of deltasToTry) {
-          const missingDependency = delta.getDependencies().find(dep => !startVersionDeltas.includes(dep));
-          if (missingDependency) {
+          const missingDependency = iterMissingDependencies(delta, startVersion).next().value; // just get the first element from the iterator
+          if (missingDependency !== undefined) {
             printIndent("missing dependency:", delta, "->", missingDependency)
             continue;
           }
@@ -166,7 +203,7 @@ export class VersionRegistry {
           // current delta does not have to be included again in next loop iterations
           deltasToTry = deltasToTry.filter(d => d !== delta);
 
-          const [willBeSkipped, willNotBeSkipped] = _.partition(deltasToTry, d => d.getConflicts().includes(delta));
+          const [willBeSkipped, willNotBeSkipped] = _.partition(deltasToTry, d => isConflicting(d, delta));
 
           if (willBeSkipped.length > 0)
             printIndent("will be skipped (conflicts with", delta, "):", ...willBeSkipped);
@@ -183,7 +220,7 @@ export class VersionRegistry {
 
     depthFirst(commonVersion, diff, 0);
 
-    printDebug("result of merge:", ..._.flatten(result.map(v => [...v.iterDeltas(), ","])));
+    printDebug("result of merge:", ..._.flatten(result.map(v => [...v, ","])));
 
     return result;
   }