mute-structs
Version:
NodeJS module providing an implementation of the LogootSplit CRDT algorithm
364 lines (360 loc) • 17 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 __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