mute-structs
Version:
NodeJS module providing an implementation of the LogootSplit CRDT algorithm
716 lines (712 loc) • 29.9 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/>.
*/
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
import { isObject } from "./data-validation";
import { Identifier } from "./identifier";
import { IdentifierInterval } from "./identifierinterval";
import * as IDFactory from "./idfactory";
import { isInt32 } from "./int32";
import { compareBase, } from "./iteratorhelperidentifier";
import { LogootSBlock } from "./logootsblock";
import { LogootSDel } from "./operations/delete/logootsdel";
import { TextDelete } from "./operations/delete/textdelete";
import { LogootSAdd } from "./operations/insert/logootsadd";
import { TextInsert } from "./operations/insert/textinsert";
import { mkNodeAt, RopesNodes } from "./ropesnodes";
import * as TextUtils from "./textutils";
function leftChildOf(aNode) {
return aNode.left;
}
function rightChildOf(aNode) {
return aNode.right;
}
var LogootSRopes = /** @class */ (function () {
function LogootSRopes(replica, clock, root, str) {
var e_1, _a;
if (replica === void 0) { replica = 0; }
if (clock === void 0) { clock = 0; }
if (root === void 0) { root = null; }
if (str === void 0) { str = ""; }
console.assert(isInt32(replica), "replica ∈ int32");
console.assert(isInt32(clock), "clock ∈ int32");
this.replicaNumber = replica;
this.clock = clock;
this.root = root;
this.str = str;
var baseToBlock = {};
if (root !== null) {
console.assert(str.length === root.sizeNodeAndChildren, "str length must match the number of elements in the model");
var blocks = root.getBlocks();
try {
for (var blocks_1 = __values(blocks), blocks_1_1 = blocks_1.next(); !blocks_1_1.done; blocks_1_1 = blocks_1.next()) {
var b = blocks_1_1.value;
var key = b.idInterval.base.join(",");
baseToBlock[key] = b;
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (blocks_1_1 && !blocks_1_1.done && (_a = blocks_1.return)) _a.call(blocks_1);
}
finally { if (e_1) throw e_1.error; }
}
}
else {
console.assert(str.length === 0, "str must be empty when no root is provided");
}
this.mapBaseToBlock = baseToBlock;
}
LogootSRopes.empty = function () {
return new LogootSRopes(0, 0);
};
LogootSRopes.fromPlain = function (o) {
if (isObject(o) &&
isInt32(o.replicaNumber) &&
isInt32(o.clock) &&
typeof o.str === "string") {
var root = RopesNodes.fromPlain(o.root);
if ((root !== null && o.str.length === root.sizeNodeAndChildren) ||
(root === null && o.str.length === 0)) {
return new LogootSRopes(o.replicaNumber, o.clock, root, o.str);
}
}
return null;
};
Object.defineProperty(LogootSRopes.prototype, "height", {
get: function () {
return (this.root) ? this.root.height : 0;
},
enumerable: true,
configurable: true
});
LogootSRopes.prototype.getBlock = function (idInterval) {
var mapBaseToBlock = this.mapBaseToBlock;
var key = idInterval.base.join(",");
var result;
if (mapBaseToBlock.hasOwnProperty(key)) {
result = mapBaseToBlock[key];
}
else {
result = new LogootSBlock(idInterval, 0);
this.mapBaseToBlock[key] = result;
}
return result;
};
/**
* Add a interval of identifiers and its corresponding string to the model
*
* @param {string} str The inserted string
* @param {IdentifierInterval} idi The corresponding interval of identifiers
* @param {RopesNodes} from The starting point of the search
* @param {number} startOffset ???
*/
LogootSRopes.prototype.addBlockFrom = function (str, idi, from, startOffset) {
var _this = this;
var result = this.addBlockFromRec(str, idi, from, startOffset);
result.forEach(function (textInsert) {
_this.applyTextInsert(textInsert);
});
return result;
};
LogootSRopes.prototype.addBlockFromRec = function (str, idi, from, startOffset) {
var author = idi.idBegin.replicaNumber;
var path = [];
var result = [];
var con = true;
var i = startOffset;
while (con) {
path.push(from);
// B1 is the block we are adding
// B2 is the block to which we are comparing
switch (compareBase(idi, from.getIdentifierInterval())) {
case 0 /* B1_AFTER_B2 */: {
if (from.right === null) {
from.right = RopesNodes.leaf(this.getBlock(idi), idi.begin, str.length);
i = i + from.leftSubtreeSize() + from.length;
result.push(new TextInsert(i, str, author));
con = false;
}
else {
i = i + from.leftSubtreeSize() + from.length;
from = from.right;
}
break;
}
case 1 /* B1_BEFORE_B2 */: {
if (from.left === null) {
from.left = RopesNodes.leaf(this.getBlock(idi), idi.begin, str.length);
result.push(new TextInsert(i, str, author));
con = false;
}
else {
from = from.left;
}
break;
}
case 2 /* B1_INSIDE_B2 */: {
// split b2 the object node
var indexOffset = from.getIdBegin().length - 1;
var offsetToSplit = idi.idBegin.tuples[indexOffset].offset;
var rp = RopesNodes.leaf(this.getBlock(idi), idi.begin, str.length);
path.push(from.split(offsetToSplit - from.actualBegin + 1, rp));
i = i + from.leftSubtreeSize();
result.push(new TextInsert(i + offsetToSplit - from.actualBegin + 1, str, author));
con = false;
break;
}
case 3 /* B2_INSIDE_B1 */: {
// split b1 the node to insert
var indexOffset = idi.idBegin.length - 1;
var offsetToSplit = from.getIdBegin().tuples[indexOffset].offset;
var ls = str.substr(0, offsetToSplit + 1 - idi.begin);
var idi1 = new IdentifierInterval(idi.idBegin, offsetToSplit);
if (from.left === null) {
from.left = RopesNodes.leaf(this.getBlock(idi1), idi1.begin, ls.length);
result.push(new TextInsert(i, ls, author));
}
else {
Array.prototype.push.apply(result, this.addBlockFromRec(ls, idi1, from.left, i));
}
// i=i+ls.size()
ls = str.substr(offsetToSplit + 1 - idi.begin, str.length);
var newIdBegin = Identifier.fromBase(idi.idBegin, offsetToSplit + 1);
idi1 = new IdentifierInterval(newIdBegin, idi.end);
i = i + from.leftSubtreeSize() + from.length;
if (from.right === null) {
from.right = RopesNodes.leaf(this.getBlock(idi1), idi1.begin, ls.length);
result.push(new TextInsert(i, ls, author));
}
else {
Array.prototype.push.apply(result, this.addBlockFromRec(ls, idi1, from.right, i));
}
con = false;
break;
}
case 4 /* B1_CONCAT_B2 */: {
// node to insert concat the node
if (from.left !== null) {
var split = from.getIdBegin().minOffsetAfterPrev(from.left.max, idi.begin);
var l = str.substr(split - idi.begin, str.length);
if (l.length > 0) {
from.appendBegin(l.length);
result.push(new TextInsert(i + from.leftSubtreeSize(), l, author));
this.ascendentUpdate(path, l.length);
}
// check if previous is smaller or not
if ((split - 1) >= idi.begin) {
str = str.substr(0, split - idi.begin);
idi = new IdentifierInterval(idi.idBegin, split - 1);
from = from.left;
}
else {
con = false;
}
}
else {
result.push(new TextInsert(i, str, author));
from.appendBegin(str.length);
this.ascendentUpdate(path, str.length);
con = false;
}
break;
}
case 5 /* B2_CONCAT_B1 */: {
// concat at end
if (from.right !== null) {
var split = from.getIdEnd().maxOffsetBeforeNext(from.right.min, idi.end);
var l = str.substr(0, split + 1 - idi.begin);
i = i + from.leftSubtreeSize() + from.length;
if (l.length > 0) {
from.appendEnd(l.length);
result.push(new TextInsert(i, l, author));
this.ascendentUpdate(path, l.length);
}
if (idi.end >= (split + 1)) {
str = str.substr(split + 1 - idi.begin, str.length);
var newIdBegin = Identifier.fromBase(idi.idBegin, split + 1);
idi = new IdentifierInterval(newIdBegin, idi.end);
from = from.right;
i = i + l.length;
}
else {
con = false;
}
}
else {
i = i + from.leftSubtreeSize() + from.length;
result.push(new TextInsert(i, str, author));
from.appendEnd(str.length);
this.ascendentUpdate(path, str.length);
con = false;
}
break;
}
case 6 /* B1_EQUALS_B2 */: {
con = false;
break;
}
}
}
this.balance(path);
return result;
};
// FIXME: Put this function elsewhere?
LogootSRopes.prototype.applyTextInsert = function (textInsert) {
this.str = TextUtils.insert(this.str, textInsert.index, textInsert.content);
};
// FIXME: Put this function elsewhere?
LogootSRopes.prototype.applyTextDelete = function (textDelete) {
var end = textDelete.index + textDelete.length - 1;
this.str = TextUtils.del(this.str, textDelete.index, end);
};
LogootSRopes.prototype.addBlock = function (str, id) {
var author = id.replicaNumber;
var idi = new IdentifierInterval(id, id.lastOffset + str.length - 1);
if (this.root === null) {
var bl = new LogootSBlock(idi, 0);
this.mapBaseToBlock[bl.idInterval.base.join(",")] = bl;
this.root = RopesNodes.leaf(bl, id.lastOffset, str.length);
var textInsert = new TextInsert(0, str, author);
this.applyTextInsert(textInsert);
return [textInsert];
}
else {
return this.addBlockFrom(str, idi, this.root, 0);
}
};
LogootSRopes.prototype.mkNode = function (id1, id2, length) {
console.assert(isInt32(length) && length > 0, "length ∈ int32");
console.assert(length > 0, "length > 0");
var id = IDFactory.createBetweenPosition(id1, id2, this.replicaNumber, this.clock++);
var node = mkNodeAt(id, length);
this.mapBaseToBlock[node.getIdentifierInterval().base.join(",")] = node.block;
return node;
};
LogootSRopes.prototype.insertLocal = function (pos, l) {
console.assert(isInt32(pos), "pos ∈ int32");
if (this.root === null) { // empty tree
this.root = this.mkNode(null, null, l.length);
this.str = TextUtils.insert(this.str, pos, l);
return new LogootSAdd(this.root.getIdBegin(), l);
}
else {
var newNode = void 0;
var length = this.str.length;
this.str = TextUtils.insert(this.str, pos, l);
var path = void 0;
if (pos === 0) { // begin of string
path = [];
path.push(this.root);
var n = this.getXest(leftChildOf, path);
newNode = this.mkNode(null, n.getIdBegin(), l.length);
n.left = newNode;
}
else if (pos >= length) { // end
path = [];
path.push(this.root);
var n = this.getXest(rightChildOf, path);
if (n.isAppendableAfter(this.replicaNumber, l.length)) { // append
var id3 = n.appendEnd(l.length);
this.ascendentUpdate(path, l.length);
return new LogootSAdd(id3, l);
}
else { // add at end
newNode = this.mkNode(n.getIdEnd(), null, l.length);
n.right = newNode;
}
}
else { // middle
var inPos = this.searchNode(pos);
// TODO: why non-null?
if (inPos.i > 0) { // split
var id1 = inPos.node.block.idInterval.getBaseId(inPos.node.actualBegin + inPos.i - 1);
var id2 = inPos.node.block.idInterval.getBaseId(inPos.node.actualBegin + inPos.i);
newNode = this.mkNode(id1, id2, l.length);
path = inPos.path;
path.push(inPos.node.split(inPos.i, newNode));
}
else {
var prev = this.searchNode(pos - 1);
// TODO: why non-null?
if (prev.node.isAppendableAfter(this.replicaNumber, l.length)) {
// append after
var id4 = prev.node.appendEnd(l.length);
this.ascendentUpdate(prev.path, l.length);
return new LogootSAdd(id4, l);
}
else {
newNode = this.mkNode(prev.node.getIdEnd(), inPos.node.getIdBegin(), l.length);
newNode.right = prev.node.right;
prev.node.right = newNode;
path = prev.path;
path.push(newNode);
}
}
}
this.balance(path);
return new LogootSAdd(newNode.getIdBegin(), l);
}
};
LogootSRopes.prototype.getXest = function (aChildOf, aPath) {
var n = aPath[aPath.length - 1];
var child = aChildOf(n);
while (child !== null) {
n = child;
aPath[aPath.length] = child;
child = aChildOf(child);
}
return n;
};
LogootSRopes.prototype.searchPos = function (id, path) {
var i = 0;
var node = this.root;
while (node !== null) {
path.push(node);
if (id.compareTo(node.getIdBegin()) === -1) {
node = node.left;
}
else if (id.compareTo(node.getIdEnd()) === 1) {
i = i + node.leftSubtreeSize() + node.length;
node = node.right;
}
else {
if (id.equalsBase(node.getIdBegin())) {
return i + node.leftSubtreeSize();
}
else {
// Could not find the identifier, stop the search
node = null;
}
}
}
// FIXME: Clear path?
return -1;
};
LogootSRopes.prototype.searchNode = function (pos) {
console.assert(isInt32(pos), "pos ∈ int32");
var node = this.root;
var path = [];
while (node !== null) {
path.push(node);
var before = node.leftSubtreeSize();
if (pos < before) {
node = node.left;
}
else if (pos < before + node.length) {
return {
i: pos - before,
node: node,
path: path,
};
}
else {
pos -= before + node.length;
node = node.right;
}
}
return null;
};
LogootSRopes.prototype.ascendentUpdate = function (path, length) {
var e_2, _a;
console.assert(isInt32(length), "length ∈ int32");
try {
// `length" may be negative
for (var path_1 = __values(path), path_1_1 = path_1.next(); !path_1_1.done; path_1_1 = path_1.next()) {
var item = path_1_1.value;
item.addString(length);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (path_1_1 && !path_1_1.done && (_a = path_1.return)) _a.call(path_1);
}
finally { if (e_2) throw e_2.error; }
}
};
LogootSRopes.prototype.delBlock = function (idInterval, author) {
var _this = this;
var l = [];
var i;
while (true) {
var path = [];
i = this.searchPos(idInterval.idBegin, path);
if (i === -1) {
// Could not find the first identifier from the interval
if (idInterval.begin < idInterval.end) {
// Shifting the interval and resuming the search
var newIdBegin = Identifier.fromBase(idInterval.idBegin, idInterval.begin + 1);
idInterval =
new IdentifierInterval(newIdBegin, idInterval.end);
}
else {
break;
}
}
else {
// Was able to find the position of the identifier
var node = path[path.length - 1]; // Retrieving the node containing the identifier
var end = Math.min(idInterval.end, node.actualEnd);
var pos = i + idInterval.begin - node.actualBegin;
var length = end - idInterval.begin + 1;
l.push(new TextDelete(pos, length, author));
var t = node.deleteOffsets(idInterval.begin, end);
if (node.length === 0) { // del node
this.delNode(path);
}
else if (t !== null) {
path.push(t);
this.balance(path);
}
else {
// TODO: Check second argument
this.ascendentUpdate(path, idInterval.begin - end - 1);
}
if (end === idInterval.end) {
break;
}
else {
// TODO: Check if still valid
var newIdBegin = Identifier.fromBase(idInterval.idBegin, end);
idInterval = new IdentifierInterval(newIdBegin, idInterval.end);
}
}
}
l.forEach(function (textDelete) {
_this.applyTextDelete(textDelete);
});
return l;
};
LogootSRopes.prototype.delLocal = function (begin, end) {
console.assert(isInt32(begin), "begin ∈ int32");
console.assert(isInt32(end), "end ∈ int32");
console.assert(begin <= end, "" + begin, " <= " + end);
this.str = TextUtils.del(this.str, begin, end);
var length = end - begin + 1;
var li = [];
do {
var start = this.searchNode(begin);
if (start !== null) {
var newBegin = start.node.actualBegin + start.i;
var newEnd = Math.min(newBegin + length - 1, start.node.actualEnd);
var prevIdBegin = start.node.getIdBegin();
var newIdBegin = Identifier.fromBase(prevIdBegin, newBegin);
li.push(new IdentifierInterval(newIdBegin, newEnd));
var r = start.node.deleteOffsets(newBegin, newEnd);
length -= newEnd - newBegin + 1;
if (start.node.length === 0) {
this.delNode(start.path);
}
else if (r !== null) { // node has been splited
start.path.push(r);
this.balance(start.path);
}
else {
this.ascendentUpdate(start.path, newBegin - newEnd - 1);
}
}
else {
length = 0;
}
} while (length > 0);
return new LogootSDel(li, this.replicaNumber);
};
LogootSRopes.prototype.delNode = function (path) {
var node = path[path.length - 1];
if (node.block.nbElement === 0) {
delete this.mapBaseToBlock[node.block.idInterval.base.join(",")];
}
if (node.right === null) {
if (node === this.root) {
this.root = node.left;
}
else {
path.pop();
path[path.length - 1].replaceChildren(node, node.left);
}
}
else if (node.left === null) {
if (node === this.root) {
this.root = node.right;
}
else {
path.pop();
path[path.length - 1].replaceChildren(node, node.right);
}
}
else { // two children
path.push(node.right);
var min = this.getMinPath(path);
node.become(min);
path.pop();
path[path.length - 1].replaceChildren(min, min.right);
}
this.balance(path);
};
LogootSRopes.prototype.getMinPath = function (path) {
console.assert(path.length !== 0, "path has at least one item");
var node = path[path.length - 1]; // precondition
while (node.left !== null) {
node = node.left;
path.push(node);
}
return node;
};
// TODO: Implémenter la balance de Google (voir AVL.js) et vérifier ses performances en comparaison
LogootSRopes.prototype.balance = function (path) {
while (path.length > 0) {
var node = path.pop(); // Loop condition
var father = path.length === 0 ? null : path[path.length - 1];
node.sumDirectChildren();
var balance = node.balanceScore();
while (Math.abs(balance) >= 2) {
if (balance >= 2) {
if (node.right !== null && node.right.balanceScore() <= -1) {
father = this.rotateRL(node, father); // Double left
}
else {
father = this.rotateLeft(node, father);
}
}
else {
if (node.left !== null && node.left.balanceScore() >= 1) {
father = this.rotateLR(node, father); // Double right
}
else {
father = this.rotateRight(node, father);
}
}
path.push(father);
balance = node.balanceScore();
}
}
};
LogootSRopes.prototype.rotateLeft = function (node, father) {
console.assert(node.right !== null, "There exists a right node");
console.assert((node === this.root) === (father === null), "The father is null when we are rotating left the root");
var r = node.right; // precondition
if (node === this.root) {
this.root = r;
}
else {
// FIXME: Should we not replace the left child in this case?
father.replaceChildren(node, r);
// FIXME: This assert fails from time to time, verify its correctness
// console.assert((father as RopesNodes).left !== null, "There exists a left node")
}
node.right = r.left;
r.left = node;
node.sumDirectChildren();
r.sumDirectChildren();
return r;
};
LogootSRopes.prototype.rotateRight = function (node, father) {
console.assert(node.left !== null, "There exists a left node");
console.assert((node === this.root) === (father === null), "The father is null when we are rotating right the root");
var r = node.left; // precondition
if (node === this.root) {
this.root = r;
}
else {
// FIXME: Should we not replace the right child in this case?
father.replaceChildren(node, r);
// FIXME: This assert fails from time to time, verify its correctness
// console.assert((father as RopesNodes).right !== null, "There exists a right node")
}
node.left = r.right;
r.right = node;
node.sumDirectChildren();
r.sumDirectChildren();
return r;
};
LogootSRopes.prototype.rotateRL = function (node, father) {
console.assert(node.right !== null, "There exists a right node");
var rightNode = node.right; // precondition
console.assert(rightNode.left !== null, "There exists a left node of the right node");
this.rotateRight(rightNode, node);
return this.rotateLeft(node, father);
};
LogootSRopes.prototype.rotateLR = function (node, father) {
console.assert(node.left !== null, "There exists a left node");
var leftNode = node.left; // precondition
console.assert(leftNode.right !== null, "There exists a right node of the left node");
this.rotateLeft(leftNode, node);
return this.rotateRight(node, father);
};
LogootSRopes.prototype.getNext = function (path) {
var node = path[path.length - 1];
if (node.right === null) {
if (path.length > 1) {
var father = path[path.length - 2];
if (father.left === node) {
path.pop();
return true;
}
}
return false;
}
else {
path.push(node.right);
this.getXest(leftChildOf, path);
return true;
}
};
/**
* @return tree digest
*/
LogootSRopes.prototype.digest = function () {
var e_3, _a;
var result = 0;
var root = this.root;
if (root !== null) {
var linearRpr = root.toList();
try {
for (var linearRpr_1 = __values(linearRpr), linearRpr_1_1 = linearRpr_1.next(); !linearRpr_1_1.done; linearRpr_1_1 = linearRpr_1.next()) {
var idi = linearRpr_1_1.value;
result = (result * 17 + idi.digest()) | 0;
// Convert to 32bits integer
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (linearRpr_1_1 && !linearRpr_1_1.done && (_a = linearRpr_1.return)) _a.call(linearRpr_1);
}
finally { if (e_3) throw e_3.error; }
}
}
return result;
};
LogootSRopes.prototype.toList = function () {
if (this.root !== null) {
return this.root.toList();
}
return [];
};
return LogootSRopes;
}());
export { LogootSRopes };
//# sourceMappingURL=logootsropes.js.map