UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

326 lines (322 loc) 11.7 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 { IdentifierTuple } from "./identifiertuple"; import { INT32_BOTTOM, INT32_TOP, isInt32 } from "./int32"; export class Identifier { // Creation constructor(tuples) { 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; } static fromPlain(o) { if (isObject(o) && Array.isArray(o.tuples) && o.tuples.length > 0) { const tuples = o.tuples.map(IdentifierTuple.fromPlain) .filter((v) => 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, offset) { console.assert(isInt32(offset), "offset ∈ int32"); const tuples = id.tuples.map((tuple, i) => { if (i === id.length - 1) { return IdentifierTuple.fromBase(tuple, offset); } return tuple; }); return new Identifier(tuples); } /** * @return replica which generated this identifier. */ get generator() { return this.tuples[this.tuples.length - 1].replicaNumber; } /** * Shortcut to retrieve the length of the Identifier * * @return {number} The length */ get length() { return this.tuples.length; } get replicaNumber() { return this.tuples[this.length - 1].replicaNumber; } get clock() { return this.tuples[this.length - 1].clock; } get dot() { return { replicaNumber: this.replicaNumber, clock: this.clock, }; } /** * Retrieve the offset of the last tuple of the identifier * * @return {number} The offset */ get lastOffset() { return this.tuples[this.length - 1].offset; } get base() { const result = this.tuples .reduce((acc, 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) { const commonPrefix = []; 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) { const commonBase = []; const minLength = Math.min(this.tuples.length, other.tuples.length); let i = 0; let stop = false; while (!stop && i < minLength) { const tuple = this.tuples[i]; const otherTuple = 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) { 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) { return this.length <= other.length && this.tuples.every((tuple, index) => { const otherTuple = 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) { 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) { 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) { return this.length === other.length && this.tuples.every((tuple, index) => { const otherTuple = 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) { if (this.equals(other)) { return 0 /* Equal */; } if (this.isPrefix(other)) { return -1 /* Less */; } if (other.isPrefix(this)) { return 1 /* 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) { // 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) { // 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, max) { console.assert(isInt32(max), "max ∈ int32"); console.assert(this.compareTo(next) === -1 /* 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, min) { 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) { const tuples = this.tuples.concat(id.tuples); return new Identifier(tuples); } getTail(index) { 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() { return this.tuples.reduce((prev, tuple) => { return (prev * 17 + tuple.digest()) | 0; }, 0); } toString() { return "Id[" + this.tuples.join(",") + "]"; } } //# sourceMappingURL=identifier.js.map