mute-structs
Version:
NodeJS module providing an implementation of the LogootSplit CRDT algorithm
358 lines (354 loc) • 13.5 kB
JavaScript
/*
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