UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

358 lines (354 loc) 13.5 kB
/* This file is part of MUTE-structs. Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { isObject } from "./data-validation"; import { IdentifierTuple } from "./identifiertuple"; import { INT32_BOTTOM, INT32_TOP, isInt32 } from "./int32"; var Identifier = /** @class */ (function () { // Creation function Identifier(tuples) { console.assert(tuples.length > 0, "tuples must not be empty"); // Last random must be different of INT32_BOTTOM // This ensures a dense set. var lastRandom = tuples[tuples.length - 1].random; console.assert(lastRandom > INT32_BOTTOM); this.tuples = tuples; } Identifier.fromPlain = function (o) { if (isObject(o) && Array.isArray(o.tuples) && o.tuples.length > 0) { var tuples = o.tuples.map(IdentifierTuple.fromPlain) .filter(function (v) { return v !== null; }); if (o.tuples.length === tuples.length) { return new Identifier(tuples); } } return null; }; /** * Generate a new Identifier with the same base as the provided one but with a different offset * * @param {Identifier} id The identifier to partly copy * @param {number} offset The last offset of the new Identifier * @return {IdentifierTuple} The generated Identifier */ Identifier.fromBase = function (id, offset) { console.assert(isInt32(offset), "offset ∈ int32"); var tuples = id.tuples.map(function (tuple, i) { if (i === id.length - 1) { return IdentifierTuple.fromBase(tuple, offset); } return tuple; }); return new Identifier(tuples); }; Object.defineProperty(Identifier.prototype, "generator", { /** * @return replica which generated this identifier. */ get: function () { return this.tuples[this.tuples.length - 1].replicaNumber; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "length", { /** * Shortcut to retrieve the length of the Identifier * * @return {number} The length */ get: function () { return this.tuples.length; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "replicaNumber", { get: function () { return this.tuples[this.length - 1].replicaNumber; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "clock", { get: function () { return this.tuples[this.length - 1].clock; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "dot", { get: function () { return { replicaNumber: this.replicaNumber, clock: this.clock, }; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "lastOffset", { /** * Retrieve the offset of the last tuple of the identifier * * @return {number} The offset */ get: function () { return this.tuples[this.length - 1].offset; }, enumerable: true, configurable: true }); Object.defineProperty(Identifier.prototype, "base", { get: function () { var result = this.tuples .reduce(function (acc, tuple) { return (acc.concat(tuple.asArray())); }, []); result.pop(); // remove last offset return result; }, enumerable: true, configurable: true }); /** * Retrieve the longest common prefix shared by this identifier with another one * * @param {Identifier} other The other identifier * @return {IdentifierTuple[]} The longest common prefix */ Identifier.prototype.longestCommonPrefix = function (other) { var commonPrefix = []; var minLength = Math.min(this.tuples.length, other.tuples.length); var i = 0; while (i < minLength && this.tuples[i].equals(other.tuples[i])) { commonPrefix.push(this.tuples[i]); i++; } return commonPrefix; }; /** * Retrieve the longest common base shared by this identifier with another one * * @param {Identifier} other The other identifier * @return {IdentifierTuple[]} The longest common base */ Identifier.prototype.longestCommonBase = function (other) { var commonBase = []; var minLength = Math.min(this.tuples.length, other.tuples.length); var i = 0; var stop = false; while (!stop && i < minLength) { var tuple = this.tuples[i]; var otherTuple = other.tuples[i]; if (tuple.equals(otherTuple)) { commonBase.push(tuple); } else { stop = true; if (tuple.equalsBase(otherTuple)) { commonBase.push(tuple); } } i++; } return commonBase; }; /** * Check if this identifier is a prefix of another one * * @param {Identifier} other The other identifier * @return {boolean} Is this identifier a prefix of other */ Identifier.prototype.isPrefix = function (other) { return this.isBasePrefix(other) && this.lastOffset === other.tuples[this.length - 1].offset; }; /** * Check if the base of this identifier is a prefix of the other one * * @param {Identifier} other The other identifier * @return {boolean} Is this base a prefix of the other one */ Identifier.prototype.isBasePrefix = function (other) { var _this = this; return this.length <= other.length && this.tuples.every(function (tuple, index) { var otherTuple = other.tuples[index]; if (index === _this.tuples.length - 1) { return tuple.equalsBase(otherTuple); } return tuple.equals(otherTuple); }); }; /** * Compute the common prefix between this identifier and the other one * and return its length * * @param other The other identifier * @return {number} The length of the common prefix */ Identifier.prototype.commonPrefixLength = function (other) { var minLength = Math.min(this.tuples.length, other.tuples.length); var i = 0; while (i < minLength && this.tuples[i].equals(other.tuples[i])) { i++; } return i; }; Identifier.prototype.equals = function (other) { return this.equalsBase(other) && this.lastOffset === other.lastOffset; }; /** * Check if two identifiers share the same base * Two identifiers share the same base if only the offset * of the last tuple of each identifier differs. * * @param {Identifier} other The other identifier * @return {boolean} Are the bases equals */ Identifier.prototype.equalsBase = function (other) { var _this = this; return this.length === other.length && this.tuples.every(function (tuple, index) { var otherTuple = other.tuples[index]; if (index < _this.length - 1) { return tuple.equals(otherTuple); } return tuple.equalsBase(otherTuple); }); }; /** * Compare this identifier to another one to order them * Ordering.Less means that this is less than other * Ordering.Greater means that this is greater than other * Ordering.Equal means that this is equal to other * * @param {Identifier} other The identifier to compare * @return {Ordering} The order of the two identifier */ Identifier.prototype.compareTo = function (other) { if (this.equals(other)) { return 0 /* Equal */; } if (this.isPrefix(other)) { return -1 /* Less */; } if (other.isPrefix(this)) { return 1 /* Greater */; } var index = this.commonPrefixLength(other); return this.tuples[index].compareTo(other.tuples[index]); }; /** * Check if we can generate new identifiers using * the same base as this without overflowing * * @param {number} length The number of characters we want to add * @return {boolean} */ Identifier.prototype.hasPlaceAfter = function (length) { // Precondition: the node which contains this identifier must be appendableAfter() console.assert(isInt32(length), "length ∈ int32"); console.assert(length > 0, "length > 0 "); // Prevent an overflow when computing lastOffset + length return this.lastOffset <= INT32_TOP - length; }; /** * Check if we can generate new identifiers using * the same base as this without underflowing * * @param {number} length The number of characters we want to add * @return {boolean} */ Identifier.prototype.hasPlaceBefore = function (length) { // Precondition: the node which contains this identifier must be appendableBefore() console.assert(isInt32(length), "length ∈ int32"); console.assert(length > 0, "length > 0 "); // Prevent an underflow when computing lastOffset - length return this.lastOffset >= INT32_BOTTOM + length; }; /** * Compute the offset of the last identifier we can generate using * the same base as this without overflowing on next * * @param {Identifier} next The next identifier * @param {number} max The desired offset * @return {number} The actual offset we can use */ Identifier.prototype.maxOffsetBeforeNext = function (next, max) { console.assert(isInt32(max), "max ∈ int32"); console.assert(this.compareTo(next) === -1 /* Less */, "this must be less than next"); if (this.equalsBase(next)) { // Happen if we receive append/prepend operations in causal disorder console.assert(max < next.lastOffset, "max must be less than next.lastOffset"); return max; } if (this.isBasePrefix(next)) { // Happen if we receive split operations in causal disorder var offset = next.tuples[this.length - 1].offset; return Math.min(offset, max); } // Bases differ return max; }; /** * Compute the offset of the last identifier we can generate using * the same base as this without underflowing on prev * * @param {Identifier} prev The previous identifier * @param {number} min The desired offset * @return {number} The actual offset we can use */ Identifier.prototype.minOffsetAfterPrev = function (prev, min) { console.assert(isInt32(min), "min ∈ int32"); if (this.equalsBase(prev)) { // Happen if we receive append/prepend operations in causal disorder console.assert(min > prev.lastOffset, "min must be greater than prev.lastOffset"); return min; } if (this.isBasePrefix(prev)) { // Happen if we receive split operations in causal disorder var offset = prev.tuples[this.length - 1].offset; return Math.max(offset + 1, min); } // Bases differ return min; }; /** * Generate a new identifier by concatening another identifier to the current one. * @param {Identifier} id The identifier to concatenate * @returns {Identifier} The resulting identifier, this + id */ Identifier.prototype.concat = function (id) { var tuples = this.tuples.concat(id.tuples); return new Identifier(tuples); }; Identifier.prototype.getTail = function (index) { console.assert(0 < index && index < this.length, "index should belong to [0, this.length["); var tailTuples = this.tuples.slice(index); return new Identifier(tailTuples); }; Identifier.prototype.digest = function () { return this.tuples.reduce(function (prev, tuple) { return (prev * 17 + tuple.digest()) | 0; }, 0); }; Identifier.prototype.toString = function () { return "Id[" + this.tuples.join(",") + "]"; }; return Identifier; }()); export { Identifier }; //# sourceMappingURL=identifier.js.map