UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

370 lines (327 loc) 12.5 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 {Dot} from "./dot" import {IdentifierTuple} from "./identifiertuple" import {INT32_BOTTOM, INT32_TOP, isInt32} from "./int32" import {Ordering} from "./ordering" export class Identifier { static fromPlain (o: unknown): Identifier | null { if (isObject<Identifier>(o) && Array.isArray(o.tuples) && o.tuples.length > 0) { const tuples = o.tuples.map(IdentifierTuple.fromPlain) .filter((v): v is IdentifierTuple => v !== null) if (o.tuples.length === tuples.length) { return new Identifier(tuples) } } return null } /** * Generate a new Identifier with the same base as the provided one but with a different offset * * @param {Identifier} id The identifier to partly copy * @param {number} offset The last offset of the new Identifier * @return {IdentifierTuple} The generated Identifier */ static fromBase (id: Identifier, offset: number): Identifier { console.assert(isInt32(offset), "offset ∈ int32") const tuples: IdentifierTuple[] = id.tuples.map((tuple: IdentifierTuple, i: number) => { if (i === id.length - 1) { return IdentifierTuple.fromBase(tuple, offset) } return tuple }) return new Identifier(tuples) } // Access readonly tuples: IdentifierTuple[] // Creation constructor (tuples: IdentifierTuple[]) { console.assert(tuples.length > 0, "tuples must not be empty") // Last random must be different of INT32_BOTTOM // This ensures a dense set. const lastRandom = tuples[tuples.length - 1].random console.assert(lastRandom > INT32_BOTTOM) this.tuples = tuples } /** * @return replica which generated this identifier. */ get generator (): number { return this.tuples[this.tuples.length - 1].replicaNumber } /** * Shortcut to retrieve the length of the Identifier * * @return {number} The length */ get length (): number { return this.tuples.length } get replicaNumber (): number { return this.tuples[this.length - 1].replicaNumber } get clock (): number { return this.tuples[this.length - 1].clock } get dot (): Dot { return { replicaNumber: this.replicaNumber, clock: this.clock, } } /** * Retrieve the offset of the last tuple of the identifier * * @return {number} The offset */ get lastOffset (): number { return this.tuples[this.length - 1].offset } get base (): number[] { const result = this.tuples .reduce((acc: number[], tuple) => ( acc.concat(tuple.asArray()) ), []) result.pop() // remove last offset return result } /** * Retrieve the longest common prefix shared by this identifier with another one * * @param {Identifier} other The other identifier * @return {IdentifierTuple[]} The longest common prefix */ longestCommonPrefix (other: Identifier): IdentifierTuple[] { const commonPrefix: IdentifierTuple[] = [] const minLength = Math.min(this.tuples.length, other.tuples.length) let i = 0 while (i < minLength && this.tuples[i].equals(other.tuples[i])) { commonPrefix.push(this.tuples[i]) i++ } return commonPrefix } /** * Retrieve the longest common base shared by this identifier with another one * * @param {Identifier} other The other identifier * @return {IdentifierTuple[]} The longest common base */ longestCommonBase (other: Identifier): IdentifierTuple[] { const commonBase: IdentifierTuple[] = [] const minLength = Math.min(this.tuples.length, other.tuples.length) let i = 0 let stop = false while (!stop && i < minLength) { const tuple: IdentifierTuple = this.tuples[i] const otherTuple: IdentifierTuple = other.tuples[i] if (tuple.equals(otherTuple)) { commonBase.push(tuple) } else { stop = true if (tuple.equalsBase(otherTuple)) { commonBase.push(tuple) } } i++ } return commonBase } /** * Check if this identifier is a prefix of another one * * @param {Identifier} other The other identifier * @return {boolean} Is this identifier a prefix of other */ isPrefix (other: Identifier): boolean { return this.isBasePrefix(other) && this.lastOffset === other.tuples[this.length - 1].offset } /** * Check if the base of this identifier is a prefix of the other one * * @param {Identifier} other The other identifier * @return {boolean} Is this base a prefix of the other one */ isBasePrefix (other: Identifier): boolean { return this.length <= other.length && this.tuples.every((tuple: IdentifierTuple, index: number) => { const otherTuple: IdentifierTuple = other.tuples[index] if (index === this.tuples.length - 1) { return tuple.equalsBase(otherTuple) } return tuple.equals(otherTuple) }) } /** * Compute the common prefix between this identifier and the other one * and return its length * * @param other The other identifier * @return {number} The length of the common prefix */ commonPrefixLength (other: Identifier): number { const minLength = Math.min(this.tuples.length, other.tuples.length) let i = 0 while (i < minLength && this.tuples[i].equals(other.tuples[i])) { i++ } return i } equals (other: Identifier): boolean { return this.equalsBase(other) && this.lastOffset === other.lastOffset } /** * Check if two identifiers share the same base * Two identifiers share the same base if only the offset * of the last tuple of each identifier differs. * * @param {Identifier} other The other identifier * @return {boolean} Are the bases equals */ equalsBase (other: Identifier): boolean { return this.length === other.length && this.tuples.every((tuple: IdentifierTuple, index: number) => { const otherTuple: IdentifierTuple = other.tuples[index] if (index < this.length - 1) { return tuple.equals(otherTuple) } return tuple.equalsBase(otherTuple) }) } /** * Compare this identifier to another one to order them * Ordering.Less means that this is less than other * Ordering.Greater means that this is greater than other * Ordering.Equal means that this is equal to other * * @param {Identifier} other The identifier to compare * @return {Ordering} The order of the two identifier */ compareTo (other: Identifier): Ordering { if (this.equals(other)) { return Ordering.Equal } if (this.isPrefix(other)) { return Ordering.Less } if (other.isPrefix(this)) { return Ordering.Greater } const index = this.commonPrefixLength(other) return this.tuples[index].compareTo(other.tuples[index]) } /** * Check if we can generate new identifiers using * the same base as this without overflowing * * @param {number} length The number of characters we want to add * @return {boolean} */ hasPlaceAfter (length: number): boolean { // Precondition: the node which contains this identifier must be appendableAfter() console.assert(isInt32(length), "length ∈ int32") console.assert(length > 0, "length > 0 ") // Prevent an overflow when computing lastOffset + length return this.lastOffset <= INT32_TOP - length } /** * Check if we can generate new identifiers using * the same base as this without underflowing * * @param {number} length The number of characters we want to add * @return {boolean} */ hasPlaceBefore (length: number): boolean { // Precondition: the node which contains this identifier must be appendableBefore() console.assert(isInt32(length), "length ∈ int32") console.assert(length > 0, "length > 0 ") // Prevent an underflow when computing lastOffset - length return this.lastOffset >= INT32_BOTTOM + length } /** * Compute the offset of the last identifier we can generate using * the same base as this without overflowing on next * * @param {Identifier} next The next identifier * @param {number} max The desired offset * @return {number} The actual offset we can use */ maxOffsetBeforeNext (next: Identifier, max: number): number { console.assert(isInt32(max), "max ∈ int32") console.assert(this.compareTo(next) === Ordering.Less, "this must be less than next") if (this.equalsBase(next)) { // Happen if we receive append/prepend operations in causal disorder console.assert(max < next.lastOffset, "max must be less than next.lastOffset") return max } if (this.isBasePrefix(next)) { // Happen if we receive split operations in causal disorder const offset = next.tuples[this.length - 1].offset return Math.min(offset, max) } // Bases differ return max } /** * Compute the offset of the last identifier we can generate using * the same base as this without underflowing on prev * * @param {Identifier} prev The previous identifier * @param {number} min The desired offset * @return {number} The actual offset we can use */ minOffsetAfterPrev (prev: Identifier, min: number): number { console.assert(isInt32(min), "min ∈ int32") if (this.equalsBase(prev)) { // Happen if we receive append/prepend operations in causal disorder console.assert(min > prev.lastOffset, "min must be greater than prev.lastOffset") return min } if (this.isBasePrefix(prev)) { // Happen if we receive split operations in causal disorder const offset = prev.tuples[this.length - 1].offset return Math.max(offset + 1, min) } // Bases differ return min } /** * Generate a new identifier by concatening another identifier to the current one. * @param {Identifier} id The identifier to concatenate * @returns {Identifier} The resulting identifier, this + id */ concat (id: Identifier): Identifier { const tuples = this.tuples.concat(id.tuples) return new Identifier(tuples) } getTail (index: number): Identifier { console.assert(0 < index && index < this.length, "index should belong to [0, this.length[") const tailTuples = this.tuples.slice(index) return new Identifier(tailTuples) } digest (): number { return this.tuples.reduce((prev: number, tuple: IdentifierTuple) => { return (prev * 17 + tuple.digest()) | 0 }, 0) } toString (): string { return "Id[" + this.tuples.join(",") + "]" } }