UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

364 lines (360 loc) 17 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 __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spread = (this && this.__spread) || function () { for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); return ar; }; import { isObject } from "../data-validation"; import { Identifier } from "../identifier"; import { IdentifierInterval } from "../identifierinterval"; import { createAtPosition, MAX_TUPLE, MIN_TUPLE } from "../idfactory"; import { isInt32 } from "../int32"; var RenamingMap = /** @class */ (function () { function RenamingMap(replicaNumber, clock, renamedIdIntervals) { var _this = this; this.replicaNumber = replicaNumber; this.clock = clock; this.renamedIdIntervals = renamedIdIntervals; this.indexes = []; var index = 0; renamedIdIntervals.forEach(function (idInterval) { _this.indexes.push(index); index += idInterval.length; }); this.maxOffset = index - 1; } RenamingMap.fromPlain = function (o) { if (isObject(o) && isInt32(o.replicaNumber) && isInt32(o.clock) && Array.isArray(o.renamedIdIntervals) && o.renamedIdIntervals.length > 0) { var renamedIdIntervals = o.renamedIdIntervals .map(IdentifierInterval.fromPlain) .filter(function (v) { return v !== null; }); if (o.renamedIdIntervals.length === renamedIdIntervals.length) { return new RenamingMap(o.replicaNumber, o.clock, renamedIdIntervals); } } return null; }; Object.defineProperty(RenamingMap.prototype, "firstId", { get: function () { return this.renamedIdIntervals[0].idBegin; }, enumerable: true, configurable: true }); Object.defineProperty(RenamingMap.prototype, "lastId", { get: function () { return this.renamedIdIntervals[this.renamedIdIntervals.length - 1].idEnd; }, enumerable: true, configurable: true }); Object.defineProperty(RenamingMap.prototype, "newFirstId", { get: function () { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, 0); }, enumerable: true, configurable: true }); Object.defineProperty(RenamingMap.prototype, "newLastId", { get: function () { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, this.maxOffset); }, enumerable: true, configurable: true }); Object.defineProperty(RenamingMap.prototype, "newRandom", { get: function () { return this.firstId.tuples[0].random; }, enumerable: true, configurable: true }); RenamingMap.prototype.renameIds = function (idsToRename, initialIndex) { var _this = this; var renamedIds = this.renamedIdIntervals.flatMap(function (idInterval) { return idInterval.toIds(); }); var currentIndex = initialIndex; return idsToRename.map(function (idToRename) { while (currentIndex < renamedIds.length - 1 && idToRename.compareTo(renamedIds[currentIndex + 1]) >= 0 /* Equal */) { currentIndex++; } if (currentIndex === -1) { // idToRename < firstId return _this.renameIdLessThanFirstId(idToRename); } else if (currentIndex < renamedIds.length && idToRename.compareTo(renamedIds[currentIndex]) === 0 /* Equal */) { // idToRename ∈ renamedIds return _this.renameIdFromIndex(currentIndex); } else if (currentIndex === renamedIds.length - 1) { // lastId < idToRename return _this.renameIdGreaterThanLastId(idToRename); } else { // firstId < idToRename < lastId var predecessorId = renamedIds[currentIndex]; return _this.renameIdFromPredecessorId(idToRename, predecessorId, currentIndex); } }); }; RenamingMap.prototype.initRenameIds = function (idsToRename) { var firstIdToRename = idsToRename[0]; var initialIndex = this.findIndexOfIdOrPredecessor(firstIdToRename); return this.renameIds(idsToRename, initialIndex); }; RenamingMap.prototype.initRenameSeq = function (idsToRename) { return this.renameIds(idsToRename, -1); }; RenamingMap.prototype.renameIdLessThanFirstId = function (id) { console.assert(id.compareTo(this.firstId) === -1 /* Less */); var closestPredecessorOfFirstId = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1); var closestPredecessorOfNewFirstId = Identifier.fromBase(this.newFirstId, this.newFirstId.lastOffset - 1); if (closestPredecessorOfFirstId.length + 1 < id.length && closestPredecessorOfFirstId.isPrefix(id) && id.tuples[closestPredecessorOfFirstId.length].compareTo(MAX_TUPLE) === 0 /* Equal */) { var tail = id.getTail(closestPredecessorOfFirstId.length + 1); return closestPredecessorOfNewFirstId.concat(tail); } if (id.compareTo(this.newFirstId) === -1 /* Less */) { return id; } return closestPredecessorOfNewFirstId.concat(id); }; RenamingMap.prototype.renameIdGreaterThanLastId = function (id) { console.assert(this.lastId.compareTo(id) === -1 /* Less */); if (this.newLastId.compareTo(this.lastId) === -1 /* Less */ && this.lastId.length + 1 < id.length && this.lastId.isPrefix(id) && id.tuples[this.lastId.length].compareTo(MIN_TUPLE) === 0 /* Equal */) { var tail = id.getTail(this.lastId.length + 1); return tail; } if (this.newLastId.compareTo(id) === -1 /* Less */) { return id; } return this.newLastId.concat(id); }; RenamingMap.prototype.renameIdFromIndex = function (index) { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, index); }; RenamingMap.prototype.renameIdFromPredecessorId = function (id, predecessorId, index) { var newPredecessorId = createAtPosition(this.replicaNumber, this.clock, this.newRandom, index); // Several cases possible // 1. id is such as id = predecessorId + MIN_TUPLE + tail // with tail < predecessorId if (predecessorId.length + 1 < id.length) { var tail = id.getTail(predecessorId.length + 1); if (predecessorId.isPrefix(id) && id.tuples[predecessorId.length].compareTo(MIN_TUPLE) === 0 /* Equal */ && tail.compareTo(predecessorId) === -1 /* Less */) { return newPredecessorId.concat(tail); } } // 2. id is such as id = closestPredecessorOfSuccessorId + MAX_TUPLE + tail // with successorId < tail var successorId = this.findIdFromIndex(index + 1); if (successorId.length + 1 < id.length) { var tail = id.getTail(successorId.length + 1); var closestPredecessorOfSuccessorId = Identifier.fromBase(successorId, successorId.lastOffset - 1); if (closestPredecessorOfSuccessorId.isPrefix(id) && id.tuples[successorId.length].compareTo(MAX_TUPLE) === 0 /* Equal */ && successorId.compareTo(tail) === -1 /* Less */) { return newPredecessorId.concat(tail); } } return newPredecessorId.concat(id); }; RenamingMap.prototype.reverseRenameId = function (id) { if (this.hasBeenRenamed(id)) { // id ∈ renamedIds return this.findIdFromIndex(id.lastOffset); } var closestPredecessorOfNewFirstId = Identifier.fromBase(this.newFirstId, this.newFirstId.lastOffset - 1); var closestSuccessorOfNewLastId = Identifier.fromBase(this.newLastId, this.newLastId.lastOffset + 1); var minFirstId = this.firstId.compareTo(closestPredecessorOfNewFirstId) === -1 /* Less */ ? this.firstId : closestPredecessorOfNewFirstId; var maxLastId = this.lastId.compareTo(this.newLastId) === 1 /* Greater */ ? this.lastId : closestSuccessorOfNewLastId; if (id.compareTo(minFirstId) === -1 /* Less */ || maxLastId.compareTo(id) === -1 /* Less */) { return id; } if (id.compareTo(this.newFirstId) === -1 /* Less */) { // closestPredecessorOfNewFirstId < id < newFirstId console.assert(this.newFirstId.compareTo(this.firstId) === -1 /* Less */, "Reaching this case should imply that newFirstId < firstId"); var end = id.getTail(1); // Since closestPredecessorOfNewFirstId is not assigned to any element, // it should be impossible to generate id such as // id = closestPredecessorOfNewFirstId + end with end < newFirstId // Thus we don't have to handle this particular case console.assert(this.newFirstId.compareTo(end) === -1 /* Less */, "end should be such as newFirstId < end"); if (end.tuples[0].random === this.newRandom) { // newFirstId < end < firstId console.assert(this.newFirstId.compareTo(end) === -1 /* Less */ && end.compareTo(this.firstId) === -1 /* Less */, "end.tuples[0].random = this.newRandom should imply that newFirstId < end < firstId"); // This case corresponds to the following scenarios: // 1. end was inserted concurrently to the rename operation with // newFirstId < end < firstId // so with // newFirst.random = end.random = firstId.random // and // newFirst.author < end.author < firstId.author // id was thus obtained by concatenating closestPredecessorOfNewFirstId + end // 2. id was inserted between other ids from case 1., after the renaming // In both cases, just need to return end to revert the renaming return end; } else { // firstId < end var closestPredecessorOfFirstId = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1); return new Identifier(__spread(closestPredecessorOfFirstId.tuples, [ MAX_TUPLE ], end.tuples)); } } if (this.lastId.compareTo(this.newLastId) === -1 /* Less */ && this.newLastId.compareTo(id) === -1 /* Less */ && id.compareTo(closestSuccessorOfNewLastId) === -1 /* Less */) { // lastId < newLastId < id < closestSuccessorOfNewLastId // id = newLastId + tail var tail2 = id.getTail(1); if (tail2.compareTo(this.lastId) === -1 /* Less */) { return new Identifier(__spread(this.lastId.tuples, [ MIN_TUPLE ], tail2.tuples)); } else if (this.lastId.compareTo(tail2) === -1 /* Less */ && tail2.compareTo(this.newLastId) === -1 /* Less */) { return tail2; } else { return id; } } if (this.newLastId.compareTo(id) === -1 /* Less */ && id.compareTo(this.lastId) === -1 /* Less */) { // newLastId < id < lastId < lastId + MIN_TUPLE + id return new Identifier(__spread(this.lastId.tuples, [ MIN_TUPLE ], id.tuples)); } // newFirstId < id < newLastId var tail = id.getTail(1); var _a = __read(this.findPredecessorAndSuccessorFromIndex(id.tuples[0].offset), 2), predecessorId = _a[0], successorId = _a[1]; if (tail.compareTo(predecessorId) === -1 /* Less */) { // tail < predecessorId < predecessorId + MIN_TUPLE + tail < successorId return new Identifier(__spread(predecessorId.tuples, [ MIN_TUPLE ], tail.tuples)); } else if (successorId.compareTo(tail) === -1 /* Less */) { // predecessorId < closestPredecessorOfSuccessorId + MAX_TUPLE + tail < successorId < tail var closestPredecessorOfSuccessorId = Identifier.fromBase(successorId, successorId.lastOffset - 1); return new Identifier(__spread(closestPredecessorOfSuccessorId.tuples, [ MAX_TUPLE ], tail.tuples)); } return tail; }; RenamingMap.prototype.hasBeenRenamed = function (id) { return id.equalsBase(this.newFirstId) && 0 <= id.lastOffset && id.lastOffset <= this.maxOffset; }; RenamingMap.prototype.findIndexOfIdOrPredecessor = function (id) { var l = 0; var r = this.renamedIdIntervals.length; while (l < r) { var m = Math.floor((l + r) / 2); var other = this.renamedIdIntervals[m]; if (other.idEnd.compareTo(id) === -1 /* Less */) { l = m + 1; } else if (id.compareTo(other.idBegin) === -1 /* Less */) { r = m; } else { // other.idBegin <= id <= other.idEnd // But could also means that id splits other var offset = id.tuples[other.idBegin.length - 1].offset; var diff = offset - other.begin; return this.indexes[m] + diff; } } // Could not find id in the renamedIdIntervals // Return the predecessor's index in this case if (this.indexes.length <= l) { // lastId < id return this.maxOffset; } return this.indexes[l] - 1; }; RenamingMap.prototype.findIdFromIndex = function (index) { var _a = __read(this.findPositionFromIndex(index), 2), idIntervalIndex = _a[0], offset = _a[1]; var idBegin = this.renamedIdIntervals[idIntervalIndex].idBegin; return Identifier.fromBase(idBegin, offset); }; RenamingMap.prototype.findPredecessorAndSuccessorFromIndex = function (index) { var _a = __read(this.findPositionFromIndex(index), 2), predecessorIndex = _a[0], predecessorOffset = _a[1]; var predecessorIdInterval = this.renamedIdIntervals[predecessorIndex]; var predecessorId = Identifier.fromBase(predecessorIdInterval.idBegin, predecessorOffset); var successorId = predecessorOffset !== predecessorIdInterval.end ? Identifier.fromBase(predecessorId, predecessorOffset + 1) : this.renamedIdIntervals[predecessorIndex + 1].idBegin; return [predecessorId, successorId]; }; RenamingMap.prototype.findPositionFromIndex = function (index) { var l = 0; var r = this.renamedIdIntervals.length; while (l <= r) { var m = Math.floor((l + r) / 2); var otherIndex = this.indexes[m]; var otherIdInterval = this.renamedIdIntervals[m]; if (otherIndex + otherIdInterval.length <= index) { l = m + 1; } else if (index < otherIndex) { r = m; } else { var offset = index - otherIndex + otherIdInterval.begin; return [m, offset]; } } throw Error("Should have found the id in the renamedIdIntervals"); }; return RenamingMap; }()); export { RenamingMap }; //# sourceMappingURL=renamingmap.js.map