mute-structs
Version:
NodeJS module providing an implementation of the LogootSplit CRDT algorithm
330 lines (326 loc) • 15.1 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/>.
*/
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";
export class RenamingMap {
constructor(replicaNumber, clock, renamedIdIntervals) {
this.replicaNumber = replicaNumber;
this.clock = clock;
this.renamedIdIntervals = renamedIdIntervals;
this.indexes = [];
let index = 0;
renamedIdIntervals.forEach((idInterval) => {
this.indexes.push(index);
index += idInterval.length;
});
this.maxOffset = index - 1;
}
static fromPlain(o) {
if (isObject(o) &&
isInt32(o.replicaNumber) && isInt32(o.clock) &&
Array.isArray(o.renamedIdIntervals) &&
o.renamedIdIntervals.length > 0) {
const renamedIdIntervals = o.renamedIdIntervals
.map(IdentifierInterval.fromPlain)
.filter((v) => v !== null);
if (o.renamedIdIntervals.length === renamedIdIntervals.length) {
return new RenamingMap(o.replicaNumber, o.clock, renamedIdIntervals);
}
}
return null;
}
get firstId() {
return this.renamedIdIntervals[0].idBegin;
}
get lastId() {
return this.renamedIdIntervals[this.renamedIdIntervals.length - 1].idEnd;
}
get newFirstId() {
return createAtPosition(this.replicaNumber, this.clock, this.newRandom, 0);
}
get newLastId() {
return createAtPosition(this.replicaNumber, this.clock, this.newRandom, this.maxOffset);
}
get newRandom() {
return this.firstId.tuples[0].random;
}
renameIds(idsToRename, initialIndex) {
const renamedIds = this.renamedIdIntervals.flatMap((idInterval) => idInterval.toIds());
let currentIndex = initialIndex;
return idsToRename.map((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
const predecessorId = renamedIds[currentIndex];
return this.renameIdFromPredecessorId(idToRename, predecessorId, currentIndex);
}
});
}
initRenameIds(idsToRename) {
const firstIdToRename = idsToRename[0];
const initialIndex = this.findIndexOfIdOrPredecessor(firstIdToRename);
return this.renameIds(idsToRename, initialIndex);
}
initRenameSeq(idsToRename) {
return this.renameIds(idsToRename, -1);
}
renameIdLessThanFirstId(id) {
console.assert(id.compareTo(this.firstId) === -1 /* Less */);
const closestPredecessorOfFirstId = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1);
const 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 */) {
const tail = id.getTail(closestPredecessorOfFirstId.length + 1);
return closestPredecessorOfNewFirstId.concat(tail);
}
if (id.compareTo(this.newFirstId) === -1 /* Less */) {
return id;
}
return closestPredecessorOfNewFirstId.concat(id);
}
renameIdGreaterThanLastId(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 */) {
const tail = id.getTail(this.lastId.length + 1);
return tail;
}
if (this.newLastId.compareTo(id) === -1 /* Less */) {
return id;
}
return this.newLastId.concat(id);
}
renameIdFromIndex(index) {
return createAtPosition(this.replicaNumber, this.clock, this.newRandom, index);
}
renameIdFromPredecessorId(id, predecessorId, index) {
const 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) {
const 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
const successorId = this.findIdFromIndex(index + 1);
if (successorId.length + 1 < id.length) {
const tail = id.getTail(successorId.length + 1);
const 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);
}
reverseRenameId(id) {
if (this.hasBeenRenamed(id)) {
// id ∈ renamedIds
return this.findIdFromIndex(id.lastOffset);
}
const closestPredecessorOfNewFirstId = Identifier.fromBase(this.newFirstId, this.newFirstId.lastOffset - 1);
const closestSuccessorOfNewLastId = Identifier.fromBase(this.newLastId, this.newLastId.lastOffset + 1);
const minFirstId = this.firstId.compareTo(closestPredecessorOfNewFirstId) === -1 /* Less */ ?
this.firstId : closestPredecessorOfNewFirstId;
const 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");
const 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
const closestPredecessorOfFirstId = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1);
return new Identifier([
...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
const tail2 = id.getTail(1);
if (tail2.compareTo(this.lastId) === -1 /* Less */) {
return new Identifier([
...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([
...this.lastId.tuples,
MIN_TUPLE,
...id.tuples,
]);
}
// newFirstId < id < newLastId
const tail = id.getTail(1);
const [predecessorId, successorId] = this.findPredecessorAndSuccessorFromIndex(id.tuples[0].offset);
if (tail.compareTo(predecessorId) === -1 /* Less */) {
// tail < predecessorId < predecessorId + MIN_TUPLE + tail < successorId
return new Identifier([
...predecessorId.tuples,
MIN_TUPLE,
...tail.tuples,
]);
}
else if (successorId.compareTo(tail) === -1 /* Less */) {
// predecessorId < closestPredecessorOfSuccessorId + MAX_TUPLE + tail < successorId < tail
const closestPredecessorOfSuccessorId = Identifier.fromBase(successorId, successorId.lastOffset - 1);
return new Identifier([
...closestPredecessorOfSuccessorId.tuples,
MAX_TUPLE,
...tail.tuples,
]);
}
return tail;
}
hasBeenRenamed(id) {
return id.equalsBase(this.newFirstId)
&& 0 <= id.lastOffset && id.lastOffset <= this.maxOffset;
}
findIndexOfIdOrPredecessor(id) {
let l = 0;
let r = this.renamedIdIntervals.length;
while (l < r) {
const m = Math.floor((l + r) / 2);
const 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
const offset = id.tuples[other.idBegin.length - 1].offset;
const 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;
}
findIdFromIndex(index) {
const [idIntervalIndex, offset] = this.findPositionFromIndex(index);
const idBegin = this.renamedIdIntervals[idIntervalIndex].idBegin;
return Identifier.fromBase(idBegin, offset);
}
findPredecessorAndSuccessorFromIndex(index) {
const [predecessorIndex, predecessorOffset] = this.findPositionFromIndex(index);
const predecessorIdInterval = this.renamedIdIntervals[predecessorIndex];
const predecessorId = Identifier.fromBase(predecessorIdInterval.idBegin, predecessorOffset);
const successorId = predecessorOffset !== predecessorIdInterval.end ?
Identifier.fromBase(predecessorId, predecessorOffset + 1) :
this.renamedIdIntervals[predecessorIndex + 1].idBegin;
return [predecessorId, successorId];
}
findPositionFromIndex(index) {
let l = 0;
let r = this.renamedIdIntervals.length;
while (l <= r) {
const m = Math.floor((l + r) / 2);
const otherIndex = this.indexes[m];
const otherIdInterval = this.renamedIdIntervals[m];
if (otherIndex + otherIdInterval.length <= index) {
l = m + 1;
}
else if (index < otherIndex) {
r = m;
}
else {
const offset = index - otherIndex + otherIdInterval.begin;
return [m, offset];
}
}
throw Error("Should have found the id in the renamedIdIntervals");
}
}
//# sourceMappingURL=renamingmap.js.map