|
@@ -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;
|
|
|
}
|