|
@@ -1,6 +1,7 @@
|
|
|
import {inspect} from "util"; // NodeJS library
|
|
|
import { Buffer } from "buffer"; // NodeJS library
|
|
|
import {findDFS} from "../util/dfs";
|
|
|
+import {bufferXOR} from "./buffer_xor";
|
|
|
|
|
|
import * as _ from "lodash";
|
|
|
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
iterConflicts,
|
|
|
} from "./delta";
|
|
|
|
|
|
-import {bufferXOR} from "./buffer_xor";
|
|
|
|
|
|
// Wrapper class, each time a version is embedded into another version, an instance of this class is contructed.
|
|
|
// More precisely, if a version is embedded by N other versions, then N instances of EmbeddedVersion are constructed.
|
|
@@ -25,6 +25,16 @@ export interface EmbeddedVersion {
|
|
|
|
|
|
export type EmbeddedVersionMap = Map<string, EmbeddedVersion>;
|
|
|
|
|
|
+// Just a shorthand
|
|
|
+export function embed(...triples: [string, Version, Map<Delta,Delta>][]): EmbeddedVersionMap {
|
|
|
+ return new Map(triples.map(([id, version, overriddenDeltas]) => [id, {embedded: version, overriddenDeltas}]));
|
|
|
+}
|
|
|
+
|
|
|
+// Just a shorthand
|
|
|
+export function overrideDeltas(...tuples: [Delta,Delta][]): Map<Delta,Delta> {
|
|
|
+ return new Map(tuples);
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
// A typed link from one version to another.
|
|
|
// There are two types: 'p' (parent) and 'c' (child)
|
|
@@ -35,6 +45,7 @@ type VersionLink = [('p'|'c'), Delta];
|
|
|
export class Version {
|
|
|
readonly parents: Array<[Version, Delta]>;
|
|
|
readonly children: Array<[Version, Delta]> = [];
|
|
|
+ private readonly embeddings: EmbeddedVersionMap; // do not access directly - use getEmbedded() instead
|
|
|
|
|
|
// 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;
|
|
@@ -42,11 +53,12 @@ export class Version {
|
|
|
readonly size: number;
|
|
|
|
|
|
// DO NOT USE constructor directly - instead use VersionRegistry.createVersion.
|
|
|
- constructor(parents: Array<[Version, Delta]>, hash: Buffer, size: number) {
|
|
|
+ constructor(parents: Array<[Version, Delta]>, embeddings: EmbeddedVersionMap, hash: Buffer, size: number) {
|
|
|
this.parents = parents;
|
|
|
for (const [parent,delta] of parents) {
|
|
|
parent.children.push([this, delta]);
|
|
|
}
|
|
|
+ this.embeddings = embeddings;
|
|
|
this.hash = hash;
|
|
|
this.size = size;
|
|
|
}
|
|
@@ -95,12 +107,34 @@ export class Version {
|
|
|
|
|
|
return findDFS<Version,VersionLink>(this, otherVersion, getNeighbors);
|
|
|
}
|
|
|
+
|
|
|
+ getEmbedded(id: string): EmbeddedVersion | undefined {
|
|
|
+ return this.embeddings.get(id);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// The initial, empty version.
|
|
|
const initialHash = Buffer.alloc(32); // all zeros
|
|
|
-export const initialVersion = new Version([], initialHash, 0);
|
|
|
|
|
|
+export class InitialVersion extends Version {
|
|
|
+ private static instance: InitialVersion = new InitialVersion(); // singleton pattern
|
|
|
+ private static embedded: EmbeddedVersion = {embedded: InitialVersion.instance, overriddenDeltas: new Map()};
|
|
|
+
|
|
|
+ private constructor() {
|
|
|
+ super([], new Map(), initialHash, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ static getInstance(): InitialVersion {
|
|
|
+ return InitialVersion.instance;
|
|
|
+ }
|
|
|
+
|
|
|
+ // override: we pretend that the initial version has all other empty models (also represented by the initial version) embedded into it.
|
|
|
+ getEmbedded(id: string): EmbeddedVersion | undefined {
|
|
|
+ return InitialVersion.embedded;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// The initial, empty version.
|
|
|
+export const initialVersion = InitialVersion.getInstance();
|
|
|
|
|
|
export class VersionRegistry {
|
|
|
// Maps version ID (as string, because a Buffer cannot be a map key) to Version
|
|
@@ -127,23 +161,58 @@ export class VersionRegistry {
|
|
|
|
|
|
// 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 {
|
|
|
+ // Pre-condition 3: this version and its parent must embed the same types of "guest" versions (e.g., "cs", "as", ...)
|
|
|
+ // Pre-condition 4: at least one of the parents of every embedded version must be embedded by the parent of this version.
|
|
|
+ // Pre-condition 5: if the to-be-created ("host") version embeds other ("guest") versions,
|
|
|
+ // then all of the guest versions' deltas must exist in the host version, or be explicitly overridden.
|
|
|
+ // Idempotent
|
|
|
+ createVersion(parent: Version, delta: Delta, embeddings: EmbeddedVersionMap = new Map()): 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) {
|
|
|
- console.log(delta);
|
|
|
throw new Error("Delta " + delta.toString() + " conflicts with " + conflictsWith.toString());
|
|
|
}
|
|
|
- return this.createVersionUnsafe(parent, delta);
|
|
|
+
|
|
|
+ const primitives = [...delta.iterPrimitiveDeltas()];
|
|
|
+
|
|
|
+ for (const [guestId, {embedded, overriddenDeltas}] of embeddings.entries()) {
|
|
|
+ const parentEmbedding = parent.getEmbedded(guestId);
|
|
|
+ // Check pre-condition 3:
|
|
|
+ if (parentEmbedding === undefined) {
|
|
|
+ throw new Error("Attempted to create illegal version: parent version does not embed '" + guestId + "'");
|
|
|
+ }
|
|
|
+ const {embedded: parentEmbedded} = parentEmbedding;
|
|
|
+ // Check pre-condition 4:
|
|
|
+ const found = embedded.parents.find(([parentOfEmbedded]) => (parentOfEmbedded === parentEmbedded));
|
|
|
+ if (found === undefined) {
|
|
|
+ throw new Error("Attempted to create illegal version that embeds a guest, whose parent is not embedded by the parent of the to-be-created version.");
|
|
|
+ }
|
|
|
+ // Check pre-condition 5:
|
|
|
+ // console.log("primitives:", primitives);
|
|
|
+ const [parentOfEmbedded, embeddedDelta] = found;
|
|
|
+ for (const embeddedPrimitive of embeddedDelta.iterPrimitiveDeltas()) {
|
|
|
+ // console.log("embeddedPrimitive:", embeddedPrimitive);
|
|
|
+ const haveOriginal = primitives.includes(embeddedPrimitive);
|
|
|
+ const overridden = overriddenDeltas.get(embeddedPrimitive);
|
|
|
+ const haveOverridden = ((overridden !== undefined) && primitives.includes(overridden));
|
|
|
+ // console.log(haveOriginal, haveOverridden)
|
|
|
+ if (!haveOriginal && !haveOverridden || (haveOriginal && haveOverridden)) { // logical XOR
|
|
|
+ throw new Error("Attempt to create illegal version: must contain all deltas (some possibly overridden) of all embedded versions.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return this.createVersionUnsafe(parent, delta, embeddings);
|
|
|
}
|
|
|
|
|
|
// Faster than createVersion, but does not check pre-conditions.
|
|
|
- createVersionUnsafe(parent: Version, delta: Delta): Version {
|
|
|
+ // Idempotent
|
|
|
+ createVersionUnsafe(parent: Version, delta: Delta, embeddings: EmbeddedVersionMap = new Map()): Version {
|
|
|
const newHash = bufferXOR(parent.hash, delta.getHash());
|
|
|
const existingVersion = this.lookupOptional(newHash);
|
|
|
if (existingVersion !== undefined) {
|
|
@@ -156,7 +225,7 @@ export class VersionRegistry {
|
|
|
}
|
|
|
return existingVersion;
|
|
|
} else {
|
|
|
- const version = new Version([[parent, delta]], newHash, parent.size + 1);
|
|
|
+ const version = new Version([[parent, delta]], embeddings, newHash, parent.size + 1);
|
|
|
this.putVersion(newHash, version);
|
|
|
return version;
|
|
|
}
|