| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.VersionRegistry = exports.Version = void 0;
- const util_1 = require("util"); // NodeJS library
- const buffer_1 = require("buffer"); // NodeJS library
- const dfs_1 = require("./util/dfs");
- const permutations_1 = require("./util/permutations");
- const buffer_xor_1 = require("./util/buffer_xor");
- // not exported -> use VersionRegistry to create versions
- class Version {
- // DO NOT USE constructor directly - instead use VersionRegistry.createVersion.
- constructor(parents, hash, size, embeddings) {
- this.children = []; // reverse parents
- this.reverseEmbeddings = new Map(); // (host) versions in which this (guest) version is embedded.
- this.implicitSelfEmbedding = {
- version: this,
- overridings: new Map(), // no overrides
- };
- this.parents = parents;
- this.hash = hash;
- this.size = size;
- this.embeddings = embeddings(this);
- }
- // 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]() {
- let current = 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() {
- const executionOrder = [...this].reverse();
- for (const d of executionOrder) {
- yield* d.iterPrimitiveDeltas();
- }
- }
- [util_1.inspect.custom](depth, options) {
- return "Version{" + [...this].map(d => (0, util_1.inspect)(d, options)).join(",") + "}";
- }
- isSubSetOf(otherVersion) {
- // current implementation is probably quite slow
- for (const delta of this) {
- if (!otherVersion.contains(delta)) {
- return false;
- }
- }
- return true;
- }
- contains(delta) {
- for (const d of this) {
- if (d === delta) {
- return true;
- }
- }
- return false;
- }
- containsPrimitive(delta) {
- 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) {
- const getNeighbors = (v) => {
- const parentVersions = v.parents.map(([parentVersion, delta]) => [['p', delta, parentVersion], parentVersion]);
- const childVersions = 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 = (0, dfs_1.findDFS)(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) {
- const getNeighbors = (v) => {
- const result = v.children.map(([childVersion, delta]) => [['c', delta, childVersion], childVersion]);
- return result;
- };
- return (0, dfs_1.findDFS)(this, otherVersion, getNeighbors);
- }
- getEmbedding(key) {
- return this.embeddings.get(key) || this.implicitSelfEmbedding;
- }
- getReverseEmbeddings(key) {
- return this.reverseEmbeddings.get(key) || [this];
- }
- // Serialize a path of Deltas from a Version in alreadyHave, to fully reconstruct this version.
- serialize(alreadyHave = new Set()) {
- const deltas = [];
- const versions = [];
- this.serializeInternal(new Set(alreadyHave), new Set(), deltas, versions);
- return {
- externalDependencies: [...alreadyHave].map(v => v.hash.toString('hex')),
- deltas,
- versions,
- };
- }
- serializeInternal(alreadyHaveVersions, alreadyHaveDeltas, deltas, versions) {
- if (alreadyHaveVersions.has(this)) {
- return;
- }
- alreadyHaveVersions.add(this);
- const embeddings = new Array();
- 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,
- });
- }
- }
- }
- exports.Version = Version;
- const initialHash = buffer_1.Buffer.alloc(32); // all zeros
- function isConflicting(deltaA, deltaB) {
- // 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);
- }
- }
- class VersionRegistry {
- constructor() {
- this.initialVersion = new Version([], initialHash, 0, () => new Map());
- // Maps version ID (as string, because a Buffer cannot be a map key) to Version
- this.versionMap = new Map([
- [initialHash.toString('hex'), this.initialVersion], // the initial version, always already there
- ]);
- }
- lookupOptional(hash) {
- return this.versionMap.get(hash.toString('hex'));
- }
- lookup(hash) {
- const hex = hash.toString('hex');
- const version = this.versionMap.get(hex);
- if (version === undefined) {
- throw new Error("no such version: " + hex);
- }
- return version;
- }
- putVersion(hash, 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, delta, embeddings = () => new Map()) {
- // 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, delta, embeddings = () => new Map()) {
- const newHash = (0, buffer_xor_1.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) {
- 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) {
- // 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 = [];
- 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);
- }
- getLCAInternal(versionA, versionB) {
- 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) {
- 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, debugNames) {
- 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);
- // ATTENTION: Constructing 'diff' must be made recursive (guest could also be a host)
- function visitEmbeddings(currentHost, nextHost) {
- 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 = [];
- 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 = [];
- // 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, candidates, depth) => {
- 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();
- 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, debugNames) {
- let result;
- for (const v of (0, permutations_1.permutations)(versions)) {
- // whatever the permutation, result will always be the same:
- result = this.merge(v, debugNames);
- }
- return result;
- }
- }
- exports.VersionRegistry = VersionRegistry;
- //# sourceMappingURL=version.js.map
|