UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

397 lines (329 loc) 15.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/>. */ 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" import { Ordering } from "../ordering" export class RenamingMap { static fromPlain (o: unknown): RenamingMap | null { if (isObject<RenamingMap>(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 is IdentifierInterval => v !== null) if (o.renamedIdIntervals.length === renamedIdIntervals.length) { return new RenamingMap(o.replicaNumber, o.clock, renamedIdIntervals) } } return null } readonly replicaNumber: number readonly clock: number readonly renamedIdIntervals: IdentifierInterval[] readonly indexes: number[] readonly maxOffset: number constructor (replicaNumber: number, clock: number, renamedIdIntervals: IdentifierInterval[]) { 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 } get firstId (): Identifier { return this.renamedIdIntervals[0].idBegin } get lastId (): Identifier { return this.renamedIdIntervals[this.renamedIdIntervals.length - 1].idEnd } get newFirstId (): Identifier { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, 0) } get newLastId (): Identifier { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, this.maxOffset) } get newRandom (): number { return this.firstId.tuples[0].random } renameIds (idsToRename: Identifier[], initialIndex: number): Identifier[] { 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]) >= Ordering.Equal) { currentIndex++ } if (currentIndex === -1) { // idToRename < firstId return this.renameIdLessThanFirstId(idToRename) } else if (currentIndex < renamedIds.length && idToRename.compareTo(renamedIds[currentIndex]) === Ordering.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: Identifier[]): Identifier[] { const firstIdToRename = idsToRename[0] const initialIndex = this.findIndexOfIdOrPredecessor(firstIdToRename) return this.renameIds(idsToRename, initialIndex) } initRenameSeq (idsToRename: Identifier[]): Identifier[] { return this.renameIds(idsToRename, -1) } renameIdLessThanFirstId (id: Identifier): Identifier { console.assert(id.compareTo(this.firstId) === Ordering.Less) const closestPredecessorOfFirstId: Identifier = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1) const closestPredecessorOfNewFirstId: Identifier = Identifier.fromBase(this.newFirstId, this.newFirstId.lastOffset - 1) if (closestPredecessorOfFirstId.length + 1 < id.length && closestPredecessorOfFirstId.isPrefix(id) && id.tuples[closestPredecessorOfFirstId.length].compareTo(MAX_TUPLE) === Ordering.Equal) { const tail = id.getTail(closestPredecessorOfFirstId.length + 1) return closestPredecessorOfNewFirstId.concat(tail) } if (id.compareTo(this.newFirstId) === Ordering.Less) { return id } return closestPredecessorOfNewFirstId.concat(id) } renameIdGreaterThanLastId (id: Identifier): Identifier { console.assert(this.lastId.compareTo(id) === Ordering.Less) if (this.newLastId.compareTo(this.lastId) === Ordering.Less && this.lastId.length + 1 < id.length && this.lastId.isPrefix(id) && id.tuples[this.lastId.length].compareTo(MIN_TUPLE) === Ordering.Equal) { const tail = id.getTail(this.lastId.length + 1) return tail } if (this.newLastId.compareTo(id) === Ordering.Less) { return id } return this.newLastId.concat(id) } renameIdFromIndex (index: number): Identifier { return createAtPosition(this.replicaNumber, this.clock, this.newRandom, index) } renameIdFromPredecessorId (id: Identifier, predecessorId: Identifier, index: number): Identifier { 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) === Ordering.Equal && tail.compareTo(predecessorId) === Ordering.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) === Ordering.Equal && successorId.compareTo(tail) === Ordering.Less) { return newPredecessorId.concat(tail) } } return newPredecessorId.concat(id) } reverseRenameId (id: Identifier): Identifier { if (this.hasBeenRenamed(id)) { // id ∈ renamedIds return this.findIdFromIndex(id.lastOffset) } const closestPredecessorOfNewFirstId: Identifier = Identifier.fromBase(this.newFirstId, this.newFirstId.lastOffset - 1) const closestSuccessorOfNewLastId = Identifier.fromBase(this.newLastId, this.newLastId.lastOffset + 1) const minFirstId = this.firstId.compareTo(closestPredecessorOfNewFirstId) === Ordering.Less ? this.firstId : closestPredecessorOfNewFirstId const maxLastId = this.lastId.compareTo(this.newLastId) === Ordering.Greater ? this.lastId : closestSuccessorOfNewLastId if (id.compareTo(minFirstId) === Ordering.Less || maxLastId.compareTo(id) === Ordering.Less) { return id } if (id.compareTo(this.newFirstId) === Ordering.Less) { // closestPredecessorOfNewFirstId < id < newFirstId console.assert(this.newFirstId.compareTo(this.firstId) === Ordering.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) === Ordering.Less, "end should be such as newFirstId < end") if (end.tuples[0].random === this.newRandom) { // newFirstId < end < firstId console.assert(this.newFirstId.compareTo(end) === Ordering.Less && end.compareTo(this.firstId) === Ordering.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 = Identifier.fromBase(this.firstId, this.firstId.lastOffset - 1) return new Identifier([ ...closestPredecessorOfFirstId.tuples, MAX_TUPLE, ...end.tuples, ]) } } if (this.lastId.compareTo(this.newLastId) === Ordering.Less && this.newLastId.compareTo(id) === Ordering.Less && id.compareTo(closestSuccessorOfNewLastId) === Ordering.Less) { // lastId < newLastId < id < closestSuccessorOfNewLastId // id = newLastId + tail const tail2 = id.getTail(1) if (tail2.compareTo(this.lastId) === Ordering.Less) { return new Identifier([ ...this.lastId.tuples, MIN_TUPLE, ...tail2.tuples, ]) } else if (this.lastId.compareTo(tail2) === Ordering.Less && tail2.compareTo(this.newLastId) === Ordering.Less) { return tail2 } else { return id } } if (this.newLastId.compareTo(id) === Ordering.Less && id.compareTo(this.lastId) === Ordering.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) === Ordering.Less) { // tail < predecessorId < predecessorId + MIN_TUPLE + tail < successorId return new Identifier([ ...predecessorId.tuples, MIN_TUPLE, ...tail.tuples, ]) } else if (successorId.compareTo(tail) === Ordering.Less) { // predecessorId < closestPredecessorOfSuccessorId + MAX_TUPLE + tail < successorId < tail const closestPredecessorOfSuccessorId: Identifier = Identifier.fromBase(successorId, successorId.lastOffset - 1) return new Identifier([ ...closestPredecessorOfSuccessorId.tuples, MAX_TUPLE, ...tail.tuples, ]) } return tail } hasBeenRenamed (id: Identifier): boolean { return id.equalsBase(this.newFirstId) && 0 <= id.lastOffset && id.lastOffset <= this.maxOffset } findIndexOfIdOrPredecessor (id: Identifier): number { 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) === Ordering.Less) { l = m + 1 } else if (id.compareTo(other.idBegin) === Ordering.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: number): Identifier { const [idIntervalIndex, offset] = this.findPositionFromIndex(index) const idBegin = this.renamedIdIntervals[idIntervalIndex].idBegin return Identifier.fromBase(idBegin, offset) } findPredecessorAndSuccessorFromIndex (index: number): [Identifier, Identifier] { 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: number): [number, number] { 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") } }