UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

249 lines (245 loc) 9.6 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 { IdentifierInterval } from "./identifierinterval"; import { isInt32 } from "./int32"; import { LogootSBlock } from "./logootsblock"; /** * @param aNode may be null * @returns Height of aNode or 0 if aNode is null */ function heightOf(aNode) { if (aNode !== null) { return aNode.height; } else { return 0; } } /** * @param aNode may be null * @returns size of aNode (including children sizes) or 0 if aNode is null */ function subtreeSizeOf(aNode) { if (aNode !== null) { return aNode.sizeNodeAndChildren; } else { return 0; } } export function mkNodeAt(id, length) { console.assert(isInt32(length), "length ∈ int32"); console.assert(length > 0, "length > 0"); const idi = new IdentifierInterval(id, length - 1); const newBlock = new LogootSBlock(idi, 0); return RopesNodes.leaf(newBlock, 0, length); } export class RopesNodes { // Creation constructor(block, actualBegin, length, left, right) { console.assert(isInt32(actualBegin), "actualBegin ∈ int32"); console.assert(block.idInterval.begin <= actualBegin, "actualBegin must be greater than or equal to idInterval.begin"); this.block = block; this.actualBegin = actualBegin; this.length = length; this.left = left; this.right = right; this.height = Math.max(heightOf(left), heightOf(right)) + 1; this.sizeNodeAndChildren = length + subtreeSizeOf(left) + subtreeSizeOf(right); } static fromPlain(o) { if (isObject(o) && isInt32(o.actualBegin) && isInt32(o.length) && o.length >= 0) { const block = LogootSBlock.fromPlain(o.block); if (block !== null && block.idInterval.begin <= o.actualBegin && (block.idInterval.end - block.idInterval.begin) >= o.length - 1) { const right = RopesNodes.fromPlain(o.right); const left = RopesNodes.fromPlain(o.left); return new RopesNodes(block, o.actualBegin, o.length, left, right); } } return null; } static leaf(block, offset, lenth) { console.assert(isInt32(offset), "aOffset ∈ int32"); console.assert(isInt32(lenth), "lenth ∈ int32"); console.assert(lenth > 0, "lenth > 0"); block.addBlock(offset, lenth); // Mutation return new RopesNodes(block, offset, lenth, null, null); } get actualEnd() { return this.actualBegin + this.length - 1; } getIdBegin() { return this.block.idInterval.getBaseId(this.actualBegin); } getIdEnd() { return this.block.idInterval.getBaseId(this.actualEnd); } get max() { if (this.right !== null) { return this.right.max; } return this.getIdEnd(); } get min() { if (this.left !== null) { return this.left.min; } return this.getIdBegin(); } addString(length) { console.assert(isInt32(length), "length ∈ int32"); // `length" may be negative this.sizeNodeAndChildren += length; } appendEnd(length) { console.assert(isInt32(length), "length ∈ int32"); console.assert(length > 0, "" + length, " > 0"); const b = this.actualEnd + 1; this.length += length; this.block.addBlock(b, length); return this.block.idInterval.getBaseId(b); } appendBegin(length) { console.assert(isInt32(length), "length ∈ int32"); console.assert(length > 0, "" + length, " > 0"); this.actualBegin -= length; this.length += length; this.block.addBlock(this.actualBegin, length); return this.getIdBegin(); } /** * Delete a interval of identifiers belonging to this node * Reduces the node"s {@link RopesNodes#length} and/or shifts its {@link RopesNodes#offset} * May also trigger a split of the current node if the deletion cuts it in two parts * * @param {number} begin The start of the interval to delete * @param {number} end The end of the interval to delete * @returns {RopesNodes | null} The resulting block if a split occured, null otherwise */ deleteOffsets(begin, end) { console.assert(isInt32(begin), "begin ∈ int32"); console.assert(isInt32(end), "end ∈ int32"); console.assert(begin <= end, "begin <= end: " + begin, " <= " + end); console.assert(this.block.idInterval.begin <= begin, "this.block.idInterval.begin <= to begin: " + this.block.idInterval.begin, " <= " + begin); console.assert(end <= this.block.idInterval.end, "end <= this.block.idInterval.end: " + end, " <= " + this.block.idInterval.end); let ret = null; // Some identifiers may have already been deleted by a previous operation // Need to update the range of the deletion accordingly // NOTE: actualEnd can be < to actualBegin if all the range has previously been deleted const actualBegin = Math.max(this.actualBegin, begin); const actualEnd = Math.min(this.actualEnd, end); if (actualBegin <= actualEnd) { const sizeToDelete = actualEnd - actualBegin + 1; this.block.delBlock(sizeToDelete); if (sizeToDelete !== this.length) { if (actualBegin === this.actualBegin) { // Deleting the beginning of the block this.actualBegin = actualEnd + 1; } else if (actualEnd !== this.actualEnd) { // Deleting the middle of the block ret = this.split(actualEnd - this.actualBegin + 1, null); } } this.length = this.length - sizeToDelete; } return ret; } split(size, node) { const newRight = new RopesNodes(this.block, this.actualBegin + size, this.length - size, node, this.right); this.length = size; this.right = newRight; this.height = Math.max(this.height, newRight.height); return newRight; } leftSubtreeSize() { return subtreeSizeOf(this.left); } rightSubtreeSize() { return subtreeSizeOf(this.right); } sumDirectChildren() { this.height = Math.max(heightOf(this.left), heightOf(this.right)) + 1; this.sizeNodeAndChildren = this.leftSubtreeSize() + this.rightSubtreeSize() + this.length; } replaceChildren(node, by) { if (this.left === node) { this.left = by; } else if (this.right === node) { this.right = by; } } balanceScore() { return heightOf(this.right) - heightOf(this.left); } become(node) { this.sizeNodeAndChildren = -this.length + node.length; this.length = node.length; this.actualBegin = node.actualBegin; this.block = node.block; } isAppendableAfter(replicaNumber, length) { return this.block.isMine(replicaNumber) && this.block.idInterval.end === this.actualEnd && this.block.idInterval.idEnd.hasPlaceAfter(length); } isAppendableBefore(replicaNumber, length) { return this.block.isMine(replicaNumber) && this.block.idInterval.begin === this.actualBegin && this.block.idInterval.idBegin.hasPlaceBefore(length); } toString() { const current = this.getIdentifierInterval().toString(); const leftToString = (this.left !== null) ? this.left.toString() : "\t#"; const rightToString = (this.right !== null) ? this.right.toString() : "\t#"; return rightToString.replace(/(\t+)/g, "\t$1") + "\n" + "\t" + current + "\n" + leftToString.replace(/(\t+)/g, "\t$1"); } /** * @return linear representation */ toList() { const idInterval = this.getIdentifierInterval(); const leftList = (this.left !== null) ? this.left.toList() : []; const rightList = (this.right !== null) ? this.right.toList() : []; return leftList.concat(idInterval, rightList); } getIdentifierInterval() { return new IdentifierInterval(this.getIdBegin(), this.actualEnd); } /** * @return list of blocks (potentially with occurrences) */ getBlocks() { let result = [this.block]; const left = this.left; if (left !== null) { result = result.concat(left.getBlocks()); } const right = this.right; if (right !== null) { result = result.concat(right.getBlocks()); } return result; } } //# sourceMappingURL=ropesnodes.js.map