UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

716 lines (712 loc) 29.9 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/>. */ 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