|
|
@@ -1,599 +0,0 @@
|
|
|
-import {inspect} from "util"; // NodeJS library
|
|
|
-import { Buffer } from "buffer"; // NodeJS library
|
|
|
-import {findDFS} from "../util/dfs";
|
|
|
-import {permutations} from "../util/permutations";
|
|
|
-import {bufferXOR} from "../util/buffer_xor";
|
|
|
-import {PrimitiveDelta, Transaction} from "./delta";
|
|
|
-
|
|
|
-// import * as _ from "lodash";
|
|
|
-
|
|
|
-import {
|
|
|
- Delta,
|
|
|
- // isConflicting,
|
|
|
- // iterMissingDependencies,
|
|
|
- // iterConflicts,
|
|
|
-} from "./delta";
|
|
|
-
|
|
|
-// The difference between two consecutive versions.
|
|
|
-type DiffType = Delta;
|
|
|
-
|
|
|
-type OverridingsMap = Map<Delta,Delta>;
|
|
|
-
|
|
|
-export interface Embedding {
|
|
|
- version: Version; // the embedded ("guest") version
|
|
|
- overridings: OverridingsMap; // overridden deltas: mapping from guest delta to host delta.
|
|
|
-}
|
|
|
-export type Embeddings = Map<string, Embedding>; // key = a simple identifier for the kind of embedding, e.g., "cs", "as", ...
|
|
|
-
|
|
|
-export type ParentLink = [Version, DiffType];
|
|
|
-export type ChildLink = [Version, DiffType];
|
|
|
-
|
|
|
-// A typed link from one version to another.
|
|
|
-// There are two types: 'p' (parent) and 'c' (child)
|
|
|
-// The link also has a 'value', which is a Delta.
|
|
|
-type PathLink = [('p'|'c'), DiffType, Version];
|
|
|
-
|
|
|
-// not exported -> use VersionRegistry to create versions
|
|
|
-export class Version {
|
|
|
- readonly parents: Array<ParentLink>;
|
|
|
- readonly children: Array<ChildLink> = []; // reverse parents
|
|
|
-
|
|
|
- readonly embeddings: Embeddings; // (guest) versions embedded in this (host) version.
|
|
|
- readonly reverseEmbeddings: Map<string, Version[]> = new Map(); // (host) versions in which this (guest) version is embedded.
|
|
|
-
|
|
|
- // 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;
|
|
|
-
|
|
|
- // Number of deltas that version consists of.
|
|
|
- readonly size: number;
|
|
|
-
|
|
|
- // DO NOT USE constructor directly - instead use VersionRegistry.createVersion.
|
|
|
- constructor(parents: Array<ParentLink>, hash: Buffer, size: number, embeddings: (Version) => Embeddings) {
|
|
|
- this.parents = parents;
|
|
|
- this.hash = hash;
|
|
|
- this.size = size;
|
|
|
- this.embeddings = embeddings(this);
|
|
|
- }
|
|
|
-
|
|
|
- implicitSelfEmbedding: Embedding = {
|
|
|
- version: this, // every version embeds itself for all undefined embeddings.
|
|
|
- overridings: new Map(), // no overrides
|
|
|
- };
|
|
|
-
|
|
|
- // 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<DiffType> {
|
|
|
- let current: Version = this;
|
|
|
- while (current.parents.length !== 0) {
|
|
|
- // There may be multiple parents due to commutativity (multiple orders of deltas that yield the same version), but it doesn't matter which one we pick: all paths will yield the same set of deltas.
|
|
|
- const [parent, delta] = current.parents[0];
|
|
|
- yield delta;
|
|
|
- current = parent;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- *iterPrimitiveDeltas(): Iterable<PrimitiveDelta> {
|
|
|
- const executionOrder = [...this].reverse();
|
|
|
- for (const d of executionOrder) {
|
|
|
- yield* d.iterPrimitiveDeltas();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- [inspect.custom](depth: number, options: object): string {
|
|
|
- return "Version{" + [...this].map(d => inspect(d, options)).join(",") + "}";
|
|
|
- }
|
|
|
-
|
|
|
- isSubSetOf(otherVersion: Version): boolean {
|
|
|
- // current implementation is probably quite slow
|
|
|
- for (const delta of this) {
|
|
|
- if (!otherVersion.contains(delta)) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- contains(delta: Delta): boolean {
|
|
|
- for (const d of this) {
|
|
|
- if (d === delta) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- containsPrimitive(delta: PrimitiveDelta): boolean {
|
|
|
- for (const d of this) {
|
|
|
- for (const p of d.iterPrimitiveDeltas()) {
|
|
|
- if (p === delta) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // Returns a sequence of Delta's to be undone/redone to go from this version to the 'otherVersion'
|
|
|
- // Returned sequence is empty if otherVersion === this
|
|
|
- // Returns undefined when there is no path (which is impossible if both versions are part of the same VersionRegistry).
|
|
|
- // TODO: implement Breadth-First Search (BFS) for better performance.
|
|
|
- findPathTo(otherVersion: Version): Array<PathLink> | undefined {
|
|
|
- const getNeighbors = (v: Version) => {
|
|
|
- const parentVersions: [PathLink,Version][] = v.parents.map(([parentVersion, delta]) => [['p', delta, parentVersion], parentVersion]);
|
|
|
- const childVersions: [PathLink,Version][] = v.children.map(([childVersion, delta]) => [['c', delta, childVersion], childVersion]);
|
|
|
- // heuristic: look in newer or older versions first?
|
|
|
- if (v.size < otherVersion.size) {
|
|
|
- // make the common case fast: most of the time,
|
|
|
- // we just want to advance to the next version (i.e., after a user edit)
|
|
|
- return [...childVersions, ...parentVersions];
|
|
|
- } else {
|
|
|
- return [...parentVersions, ...childVersions];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // console.time("findDFS")
|
|
|
- const result = findDFS<Version,PathLink>(this, otherVersion, getNeighbors);
|
|
|
- // console.timeEnd("findDFS");
|
|
|
-
|
|
|
- // if (result)
|
|
|
- // console.log("findPath:", result.map(([linkType])=>linkType));
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- // Like findPathTo, but only searches 'down' (younger versions).
|
|
|
- findDescendant(otherVersion: Version): Array<PathLink> | undefined {
|
|
|
- const getNeighbors = (v: Version) => {
|
|
|
- const result: [PathLink,Version][] = v.children.map(([childVersion, delta]) =>
|
|
|
- [['c', delta, childVersion], childVersion]);
|
|
|
- return result;
|
|
|
- };
|
|
|
- return findDFS<Version,PathLink>(this, otherVersion, getNeighbors);
|
|
|
- }
|
|
|
-
|
|
|
- getEmbedding(key: string): Embedding {
|
|
|
- return this.embeddings.get(key) || this.implicitSelfEmbedding;
|
|
|
- }
|
|
|
-
|
|
|
- getReverseEmbeddings(key: string): Version[] {
|
|
|
- return this.reverseEmbeddings.get(key) || [this];
|
|
|
- }
|
|
|
-
|
|
|
- // Serialize a path of Deltas from a Version in alreadyHave, to fully reconstruct this version.
|
|
|
- serialize(alreadyHave: Set<Version> = new Set()) {
|
|
|
- const deltas = [];
|
|
|
- const versions = [];
|
|
|
- this.serializeInternal(new Set(alreadyHave), new Set<Delta>(), deltas, versions);
|
|
|
- return {
|
|
|
- externalDependencies: [...alreadyHave].map(v => v.hash.toString('hex')),
|
|
|
- deltas,
|
|
|
- versions,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- private serializeInternal(alreadyHaveVersions: Set<Version>, alreadyHaveDeltas: Set<Delta>, deltas, versions) {
|
|
|
- if (alreadyHaveVersions.has(this)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- alreadyHaveVersions.add(this);
|
|
|
- const embeddings = new Array<{guestId: string, v: string, ovr: object}>();
|
|
|
- for (const [guestId, {version, overridings}] of this.embeddings) {
|
|
|
- version.serializeInternal(alreadyHaveVersions, alreadyHaveDeltas, deltas, versions);
|
|
|
- const ovr = {};
|
|
|
- for (const [key,val] of overridings.entries()) {
|
|
|
- ovr[key.hash.toString('hex')] = val.hash.toString('hex');
|
|
|
- }
|
|
|
- embeddings.push({guestId, v: version.hash.toString('hex'), ovr});
|
|
|
- }
|
|
|
-
|
|
|
- if (this.parents.length > 0) {
|
|
|
- const [parentVersion, delta] = this.parents[0];
|
|
|
-
|
|
|
- parentVersion.serializeInternal(alreadyHaveVersions, alreadyHaveDeltas, deltas, versions);
|
|
|
-
|
|
|
- function visitDelta(delta) {
|
|
|
- for (const d of delta.getDependencies()) {
|
|
|
- visitDelta(d);
|
|
|
- }
|
|
|
- if (!alreadyHaveDeltas.has(delta)) {
|
|
|
- deltas.push(delta.serialize());
|
|
|
- alreadyHaveDeltas.add(delta);
|
|
|
- }
|
|
|
- }
|
|
|
- visitDelta(delta);
|
|
|
-
|
|
|
- versions.push({
|
|
|
- id: this.hash.toString('hex'),
|
|
|
- parent: parentVersion.hash.toString('hex'),
|
|
|
- delta: delta.hash.toString('hex'),
|
|
|
- embeddings,
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const initialHash = Buffer.alloc(32); // all zeros
|
|
|
-
|
|
|
-function isConflicting(deltaA: Delta, deltaB: Delta) {
|
|
|
- // for performance, iterate over the delta that has the least conflicts:
|
|
|
- if (deltaA.conflictsWith.length < deltaB.conflictsWith.length) {
|
|
|
- return deltaA.conflictsWith.some(([d]) => d === deltaB);
|
|
|
- }
|
|
|
- else {
|
|
|
- return deltaB.conflictsWith.some(([d]) => d === deltaA);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-export class VersionRegistry {
|
|
|
- readonly initialVersion: Version = new Version([], initialHash, 0, () => new Map());
|
|
|
-
|
|
|
- // Maps version ID (as string, because a Buffer cannot be a map key) to Version
|
|
|
- readonly versionMap: Map<string, Version> = new Map([
|
|
|
- [initialHash.toString('hex'), this.initialVersion], // the initial version, always already there
|
|
|
- ]);
|
|
|
-
|
|
|
- lookupOptional(hash: Buffer): Version | undefined {
|
|
|
- return this.versionMap.get(hash.toString('hex'));
|
|
|
- }
|
|
|
-
|
|
|
- lookup(hash: Buffer): Version {
|
|
|
- const hex = hash.toString('hex');
|
|
|
- const version = this.versionMap.get(hex);
|
|
|
- if (version === undefined) {
|
|
|
- throw new Error("no such version: " + hex);
|
|
|
- }
|
|
|
- return version;
|
|
|
- }
|
|
|
-
|
|
|
- private putVersion(hash: Buffer, version: Version) {
|
|
|
- this.versionMap.set(hash.toString('hex'), version);
|
|
|
- }
|
|
|
-
|
|
|
- // Idempotent
|
|
|
- // 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.
|
|
|
- // Pre-condition 3: 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.
|
|
|
- createVersion(parent: Version, delta: DiffType, embeddings: (Version) => Embeddings = () => new Map()): Version {
|
|
|
- // Check pre-condition 1:
|
|
|
- for (const [dep] of delta.getDependencies()) {
|
|
|
- if (!parent.contains(dep)) {
|
|
|
- throw new Error("createVersion: precondition failed: Missing dependency: " + dep.description);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check pre-condition 2:
|
|
|
- for (const [conflictingDelta] of delta.conflictsWith) {
|
|
|
- if (parent.contains(conflictingDelta)) {
|
|
|
- throw new Error("createVersion: precondition failed: Delta " + delta.description + " conflicts with " + conflictingDelta.description);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const newVersion = this.createVersionUnsafe(parent, delta, embeddings);
|
|
|
-
|
|
|
- // Check pre-condition 3:
|
|
|
- const primitiveDeltas = [...delta.iterPrimitiveDeltas()];
|
|
|
- for (const [guestId, {version: guest, overridings}] of newVersion.embeddings.entries()) {
|
|
|
- const {version: guestParent} = parent.getEmbedding(guestId);
|
|
|
- const guestDiff = guestParent.findDescendant(guest)!;
|
|
|
- for (const [_, guestDelta] of guestDiff) {
|
|
|
- for (const guestPDelta of guestDelta.iterPrimitiveDeltas()) {
|
|
|
- if (!primitiveDeltas.includes(overridings.get(guestPDelta) || guestPDelta)) {
|
|
|
- throw new Error("createVersion: precondition failed: Guest's primitive delta " + guestPDelta.description + " does not occur in host, nor is it overridden.");
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return newVersion;
|
|
|
- }
|
|
|
-
|
|
|
- // Faster than createVersion, but does not check pre-conditions.
|
|
|
- // Idempotent
|
|
|
- createVersionUnsafe(parent: Version, delta: DiffType, embeddings: (Version) => Embeddings = () => new Map()): Version {
|
|
|
- const newHash = bufferXOR(parent.hash, delta.hash);
|
|
|
- // TODO: include embeddings in hash digest.
|
|
|
- const existingVersion = this.lookupOptional(newHash);
|
|
|
- if (existingVersion !== undefined) {
|
|
|
- // this Version already exists
|
|
|
- const havePath = existingVersion.parents.some(([parentVersion, delta]) => parentVersion === parent);
|
|
|
- if (!havePath) {
|
|
|
- // but the path is new (there can be multiple 'paths' to the same version, because of commutation of deltas)
|
|
|
- existingVersion.parents.push([parent, delta]);
|
|
|
- parent.children.push([existingVersion, delta]);
|
|
|
- }
|
|
|
- for (const [guestId, {version, overridings}] of embeddings(existingVersion)) {
|
|
|
- const found = existingVersion.embeddings.get(guestId);
|
|
|
- if (!found) {
|
|
|
- existingVersion.embeddings.set(guestId, {version, overridings});
|
|
|
- // throw new Error("Assertion failed: created version already exists, but does not embed '" + guestId + "'");
|
|
|
- }
|
|
|
- else {
|
|
|
- const {version: v, overridings: o} = found;
|
|
|
- if (v !== version) {
|
|
|
- throw new Error("Assertion failed: created version already exists and embeds a differrent version '" + guestId + "'");
|
|
|
- }
|
|
|
- // Merge overridings:
|
|
|
- for (const [guestDelta, hostDelta] of overridings.entries()) {
|
|
|
- const alreadyHostDelta = o.get(guestDelta) || hostDelta;
|
|
|
- if (hostDelta !== alreadyHostDelta) {
|
|
|
- throw new Error("Assertion failed: created version already exists BUT overrides delta '" + guestDelta.description + "' with another delta.");
|
|
|
- }
|
|
|
- o.set(guestDelta, hostDelta);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return existingVersion;
|
|
|
- } else {
|
|
|
- const newVersion = new Version([[parent, delta]], newHash, parent.size + 1, embeddings);
|
|
|
- // Create reverse parent links:
|
|
|
- for (const [parent,delta] of newVersion.parents) {
|
|
|
- parent.children.push([newVersion, delta]);
|
|
|
- }
|
|
|
- // Create reverse embedding:
|
|
|
- for (const [key, embedding] of newVersion.embeddings.entries()) {
|
|
|
- const reverse = embedding.version.reverseEmbeddings.get(key);
|
|
|
- if (reverse !== undefined) {
|
|
|
- reverse.push(newVersion);
|
|
|
- }
|
|
|
- else {
|
|
|
- embedding.version.reverseEmbeddings.set(key, [newVersion]);
|
|
|
- }
|
|
|
- }
|
|
|
- this.putVersion(newHash, newVersion);
|
|
|
- return newVersion;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Mostly used for testing purposes.
|
|
|
- // 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<DiffType>): Version {
|
|
|
- return deltas.reduceRight((parentVersion, delta) => this.createVersion(parentVersion, delta), this.initialVersion);
|
|
|
- }
|
|
|
-
|
|
|
- // Get the version whose deltas are a subset of all given versions AKA the 'largest common ancestor'.
|
|
|
- getIntersection(versions: Array<Version>): Version {
|
|
|
- // 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<DiffType> = [];
|
|
|
- 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);
|
|
|
- }
|
|
|
-
|
|
|
- private getLCAInternal(versionA: Version, versionB: Version): Version {
|
|
|
- const ancestorsA = [versionA];
|
|
|
- const ancestorsB = [versionB];
|
|
|
- while (true) {
|
|
|
- const a = ancestorsA[ancestorsA.length-1];
|
|
|
- // @ts-ignore: TypeScript doesn't know about 'findLast' method yet.
|
|
|
- if (ancestorsB.findLast(v => v === a)) {
|
|
|
- return a;
|
|
|
- }
|
|
|
- if (a.parents.length > 0) {
|
|
|
- const [parent] = a.parents[0];
|
|
|
- ancestorsA.push(parent);
|
|
|
- }
|
|
|
- const b = ancestorsB[ancestorsB.length-1];
|
|
|
- // @ts-ignore: TypeScript doesn't know about 'findLast' method yet.
|
|
|
- if (ancestorsA.findLast(v => v === b)) {
|
|
|
- return b;
|
|
|
- }
|
|
|
- if (b.parents.length > 0) {
|
|
|
- const [parent] = b.parents[0];
|
|
|
- ancestorsB.push(parent);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- getLCA(versions: Array<Version>): Version {
|
|
|
- if (versions.length === 0) {
|
|
|
- return this.initialVersion;
|
|
|
- }
|
|
|
- return versions.reduce((a,b) => this.getLCAInternal(a,b));
|
|
|
- }
|
|
|
-
|
|
|
- // Idempotent
|
|
|
- // Of the union of all deltas of the versions given, compute the maximal left-closed conflict-free subsets.
|
|
|
- // These are the subsets to which no delta can be added without introducing a conflict or missing dependency.
|
|
|
- merge(versions: Array<Version>, debugNames?: (Delta) => string): Array<Version> {
|
|
|
-
|
|
|
- function printDebug(...args) {
|
|
|
- if (debugNames !== undefined) {
|
|
|
- for (const [i,arg] of args.entries()) {
|
|
|
- try {
|
|
|
- const name = debugNames(arg);
|
|
|
- if (name !== undefined) {
|
|
|
- args[i] = name;
|
|
|
- }
|
|
|
- }
|
|
|
- catch (e) {}
|
|
|
- }
|
|
|
- console.log(...args);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const lca = this.getLCA(versions);
|
|
|
- // printDebug("lca:", ...[...lca].map(d => d.description));
|
|
|
-
|
|
|
- // Tuple
|
|
|
- // Delta: a delta that is at least one of 'versions' but not in 'lca'
|
|
|
- // Map: a mapping from all guest-IDs to 'diffs':
|
|
|
- // the Delta of the guest that corresponds to the Delta in the host.
|
|
|
- type DeltaWithEmbedding = [Delta, Map<string, [DeltaWithEmbedding|null, OverridingsMap]>];
|
|
|
-
|
|
|
- // ATTENTION: Constructing 'diff' must be made recursive (guest could also be a host)
|
|
|
-
|
|
|
- function visitEmbeddings(currentHost: Version, nextHost: Version) {
|
|
|
- const guestDeltaMap = new Map();
|
|
|
- for (const [guestId, {version: nextGuest, overridings}] of nextHost.embeddings.entries()) {
|
|
|
- const currentGuest = currentHost.getEmbedding(guestId).version;
|
|
|
- const guestPath = currentGuest.findDescendant(nextGuest)!;
|
|
|
- if (guestPath.length > 1) {
|
|
|
- throw new Error("Did not expect guestPath to be longer than one delta")
|
|
|
- }
|
|
|
- if (guestPath.length === 1) {
|
|
|
- const guestDelta = guestPath[0][1];
|
|
|
- if (currentHost === currentGuest && nextHost === nextGuest) {
|
|
|
- // prevent infinite recursion in case of self-embedding:
|
|
|
- guestDeltaMap.set(guestId, [[guestDelta, guestDeltaMap], overridings]);
|
|
|
- }
|
|
|
- else {
|
|
|
- const recursiveGuestDeltaMap = visitEmbeddings(currentGuest, nextGuest);
|
|
|
- guestDeltaMap.set(guestId, [[guestDelta, recursiveGuestDeltaMap], overridings]);
|
|
|
- }
|
|
|
- }
|
|
|
- else {
|
|
|
- guestDeltaMap.set(guestId, [null, new Map()]);
|
|
|
- }
|
|
|
- }
|
|
|
- return guestDeltaMap;
|
|
|
- }
|
|
|
-
|
|
|
- let diff: Array<DeltaWithEmbedding> = [];
|
|
|
- for (const v of versions) {
|
|
|
- const path = lca.findDescendant(v)!;
|
|
|
- // all deltas on path from lca to 'v':
|
|
|
- let currentVersion = lca;
|
|
|
- for (const [_, delta, nextVersion] of path) {
|
|
|
- const guestDeltaMap = visitEmbeddings(currentVersion, nextVersion);
|
|
|
- // The following condition may be wrong, check this later:
|
|
|
- if (!diff.some(([d]) => d === delta)) {
|
|
|
- diff.push([delta, guestDeltaMap]);
|
|
|
- }
|
|
|
- currentVersion = nextVersion;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Now we're ready to actually start merging...
|
|
|
-
|
|
|
- const result: Array<Version> = [];
|
|
|
-
|
|
|
- // Recursively attempts to add deltas from 'deltasToTry' to 'startVersion'
|
|
|
- // When nothing can be added anymore without introducing a conflict, we have a 'maximal version' (a result).
|
|
|
- // It's possible that there is more than one 'maximal version'.
|
|
|
- // Precondition: We assume that any single delta of 'deltasToTry' will not be conflicting with 'startVersion'.
|
|
|
- const depthFirst = (startVersion: Version, candidates: Array<DeltaWithEmbedding>, depth: number) => {
|
|
|
- function printIndent(...args) {
|
|
|
- printDebug(" ".repeat(depth), ...args);
|
|
|
- }
|
|
|
- // printIndent("deltasToTry=", ...candidates.map(([d])=>d));
|
|
|
-
|
|
|
- let couldNotRecurse = true;
|
|
|
-
|
|
|
- for (const [delta, guestDeltaMap] of candidates) {
|
|
|
- const haveMissingDependency = delta.getDependencies().some(([dependency]) => !startVersion.contains(dependency));
|
|
|
- if (haveMissingDependency) {
|
|
|
- printIndent("missing dependency, trying next delta")
|
|
|
- continue; // skip this delta, but keep it in deltasToTry (its missing dependency may be in deltasToTry)
|
|
|
- }
|
|
|
-
|
|
|
- // ATTENTION: Constructing embeddings must be made recursive (guest could also be a host)
|
|
|
-
|
|
|
- // 'delta' can be added
|
|
|
- // printIndent("including", delta, " => new version: ", delta, ...startVersion);
|
|
|
-
|
|
|
- const createMergedVersionRecursive = (startVersion, [delta, guestDeltaMap]) => {
|
|
|
- const embeddings = new Map();
|
|
|
- const selfEmbeddingKeys = new Set<string>();
|
|
|
- for (const [guestId, [guestDelta, overridings]] of guestDeltaMap.entries()) {
|
|
|
- const {version: guestStartVersion} = startVersion.getEmbedding(guestId);
|
|
|
- // console.log(guestId, "guestStartVersion: ", guestStartVersion);
|
|
|
- let nextGuestVersion;
|
|
|
- if (guestDelta === null) {
|
|
|
- nextGuestVersion = guestStartVersion;
|
|
|
- } else {
|
|
|
- if (guestStartVersion === startVersion && guestDelta[0] === delta && guestDelta[1] === guestDeltaMap) {
|
|
|
- selfEmbeddingKeys.add(guestId);
|
|
|
- continue;
|
|
|
- }
|
|
|
- else {
|
|
|
- nextGuestVersion = createMergedVersionRecursive(guestStartVersion, guestDelta);
|
|
|
- }
|
|
|
- }
|
|
|
- embeddings.set(guestId, {version: nextGuestVersion, overridings});
|
|
|
- }
|
|
|
- // printIndent("Creating version (", delta, ...[...startVersion].map(d => d), ") with embeddings", embeddings);
|
|
|
- const nextVersion = this.createVersion(startVersion, delta, newVersion => {
|
|
|
- // add self-embeddings:
|
|
|
- for (const guestId of selfEmbeddingKeys) {
|
|
|
- embeddings.set(guestId, {version: newVersion, overridings: new Map()});
|
|
|
- }
|
|
|
- return embeddings;
|
|
|
- });
|
|
|
- // printIndent("Created version (", ...[...nextVersion].map(d => d), ") with embeddings", embeddings);
|
|
|
- return nextVersion;
|
|
|
- }
|
|
|
-
|
|
|
- const nextVersion = createMergedVersionRecursive(startVersion, [delta, guestDeltaMap]);
|
|
|
-
|
|
|
- // current delta does not have to be included again in next loop iterations
|
|
|
- candidates = candidates.filter(([d]) => d !== delta);
|
|
|
-
|
|
|
- // const [conflicting, nonConflicting] = _.partition(deltasToTry, ([d]) => isConflicting(d, delta));
|
|
|
- const nonConflicting = candidates.filter(([candidate]) => !isConflicting(candidate, delta));
|
|
|
-
|
|
|
- // if (conflicting.length > 0)
|
|
|
- // printIndent("will be skipped (conflicts with", delta, "):", ...conflicting);
|
|
|
-
|
|
|
- depthFirst(nextVersion, nonConflicting, depth+1);
|
|
|
- couldNotRecurse = false;
|
|
|
-
|
|
|
- if (nonConflicting.length === candidates.length) {
|
|
|
- // all deltas from deltasToTry were included -> no need to try alternatives
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (couldNotRecurse) {
|
|
|
- // possibly have a new maximal version
|
|
|
- if (!result.some(v => startVersion.isSubSetOf(v))) {
|
|
|
- // printIndent("new result");
|
|
|
- result.push(startVersion);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- depthFirst(lca, diff, 0);
|
|
|
-
|
|
|
- // printDebug("result of merge:", ..._.flatten(result.map(v => [...v, ","])));
|
|
|
-
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- // Idempotent
|
|
|
- // Calls the merge-function for every permutation of 'versions'
|
|
|
- // This is useful for didactic purposes: a path is created from every input to at least one output.
|
|
|
- // Of course it won't scale to many inputs.
|
|
|
- crazyMerge(versions: Array<Version>, debugNames?: (Delta) => string): Array<Version> {
|
|
|
- let result;
|
|
|
- for (const v of permutations(versions)) {
|
|
|
- // whatever the permutation, result will always be the same:
|
|
|
- result = this.merge(v, debugNames);
|
|
|
- }
|
|
|
- return result;
|
|
|
- }
|
|
|
-}
|