Source: token.js

"use strict";

const types = require("./types");
const util = require("util");

const _Murmur3TokenType = types.dataTypes.getByName("bigint");
const _RandomTokenType = types.dataTypes.getByName("varint");
const _OrderedTokenType = types.dataTypes.getByName("blob");

/**
 * Represents a token on the Cassandra ring.
 */
class Token {
    constructor(value) {
        this._value = value;
    }

    /**
     * @returns {{code: number, info: *|Object}} The type info for the
     *                                           type of the value of the token.
     */
    getType() {
        throw new Error(
            "You must implement a getType function for this Token instance",
        );
    }

    /**
     * @returns {*} The raw value of the token.
     */
    getValue() {
        return this._value;
    }

    toString() {
        return this._value.toString();
    }

    /**
     * Returns 0 if the values are equal, 1 if greater than other, -1
     * otherwise.
     *
     * @param {Token} other
     * @returns {Number}
     */
    compare(other) {
        return this._value.compare(other._value);
    }

    equals(other) {
        return this.compare(other) === 0;
    }

    inspect() {
        return this.constructor.name + " { " + this.toString() + " }";
    }
}

/**
 * Represents a token from a Cassandra ring where the partitioner
 * is Murmur3Partitioner.
 *
 * The raw token type is a varint (represented by MutableLong).
 */
class Murmur3Token extends Token {
    constructor(value) {
        super(value);
    }

    getType() {
        return _Murmur3TokenType;
    }
}

/**
 * Represents a token from a Cassandra ring where the partitioner
 * is RandomPartitioner.
 *
 * The raw token type is a bigint (represented by Number).
 */
class RandomToken extends Token {
    constructor(value) {
        super(value);
    }

    getType() {
        return _RandomTokenType;
    }
}

/**
 * Represents a token from a Cassandra ring where the partitioner
 * is ByteOrderedPartitioner.
 *
 * The raw token type is a blob (represented by Buffer or Array).
 */
class ByteOrderedToken extends Token {
    constructor(value) {
        super(value);
    }

    getType() {
        return _OrderedTokenType;
    }

    toString() {
        return this._value.toString("hex").toUpperCase();
    }
}

/**
 * Represents a range of tokens on a Cassandra ring.
 *
 * A range is start-exclusive and end-inclusive.  It is empty when
 * start and end are the same token, except if that is the minimum
 * token, in which case the range covers the whole ring (this is
 * consistent with the behavior of CQL range queries).
 *
 * Note that CQL does not handle wrapping.  To query all partitions
 * in a range, see {@link unwrap}.
 */
class TokenRange {
    constructor(start, end, tokenizer) {
        this.start = start;
        this.end = end;
        Object.defineProperty(this, "_tokenizer", {
            value: tokenizer,
            enumerable: false,
        });
    }

    /**
     * Splits this range into a number of smaller ranges of equal "size"
     * (referring to the number of tokens, not the actual amount of data).
     *
     * Splitting an empty range is not permitted.  But not that, in edge
     * cases, splitting a range might produce one or more empty ranges.
     *
     * @param {Number} numberOfSplits Number of splits to make.
     * @returns {TokenRange[]} Split ranges.
     * @throws {Error} If splitting an empty range.
     */
    splitEvenly(numberOfSplits) {
        if (numberOfSplits < 1) {
            throw new Error(
                util.format(
                    "numberOfSplits (%d) must be greater than 0.",
                    numberOfSplits,
                ),
            );
        }
        if (this.isEmpty()) {
            throw new Error("Can't split empty range " + this.toString());
        }

        const tokenRanges = [];
        const splitPoints = this._tokenizer.split(
            this.start,
            this.end,
            numberOfSplits,
        );
        let splitStart = this.start;
        let splitEnd;
        for (
            let splitIndex = 0;
            splitIndex < splitPoints.length;
            splitIndex++
        ) {
            splitEnd = splitPoints[splitIndex];
            tokenRanges.push(
                new TokenRange(splitStart, splitEnd, this._tokenizer),
            );
            splitStart = splitEnd;
        }
        tokenRanges.push(new TokenRange(splitStart, this.end, this._tokenizer));
        return tokenRanges;
    }

    /**
     * A range is empty when start and end are the same token, except if
     * that is the minimum token, in which case the range covers the
     * whole ring.  This is consistent with the behavior of CQL range
     * queries.
     *
     * @returns {boolean} Whether this range is empty.
     */
    isEmpty() {
        return (
            this.start.equals(this.end) &&
            !this.start.equals(this._tokenizer.minToken())
        );
    }

    /**
     * A range wraps around the end of the ring when the start token
     * is greater than the end token and the end token is not the
     * minimum token.
     *
     * @returns {boolean} Whether this range wraps around.
     */
    isWrappedAround() {
        return (
            this.start.compare(this.end) > 0 &&
            !this.end.equals(this._tokenizer.minToken())
        );
    }

    /**
     * Splits this range into a list of two non-wrapping ranges.
     *
     * This will return the range itself if it is non-wrapped, or two
     * ranges otherwise.
     *
     * This is useful for CQL range queries, which do not handle
     * wrapping.
     *
     * @returns {TokenRange[]} The list of non-wrapping ranges.
     */
    unwrap() {
        if (this.isWrappedAround()) {
            return [
                new TokenRange(
                    this.start,
                    this._tokenizer.minToken(),
                    this._tokenizer,
                ),
                new TokenRange(
                    this._tokenizer.minToken(),
                    this.end,
                    this._tokenizer,
                ),
            ];
        }
        return [this];
    }

    /**
     * Whether this range contains a given Token.
     *
     * @param {*} token Token to check for.
     * @returns {boolean} Whether or not the Token is in this range.
     */
    contains(token) {
        if (this.isEmpty()) {
            return false;
        }
        const minToken = this._tokenizer.minToken();
        if (this.end.equals(minToken)) {
            if (this.start.equals(minToken)) {
                return true; // ]minToken, minToken] === full ring
            } else if (token.equals(minToken)) {
                return true;
            }
            return token.compare(this.start) > 0;
        }

        const isAfterStart = token.compare(this.start) > 0;
        const isBeforeEnd = token.compare(this.end) <= 0;
        // if wrapped around ring, token is in ring if its after start or before end.
        // otherwise, token is in ring if its after start and before end.
        return this.isWrappedAround()
            ? isAfterStart || isBeforeEnd
            : isAfterStart && isBeforeEnd;
    }

    /**
     * Determines if the input range is equivalent to this one.
     *
     * @param {TokenRange} other Range to compare with.
     * @returns {boolean} Whether or not the ranges are equal.
     */
    equals(other) {
        if (other === this) {
            return true;
        } else if (other instanceof TokenRange) {
            return this.compare(other) === 0;
        }
        return false;
    }

    /**
     * Returns 0 if the values are equal, otherwise compares against
     * start, if start is equal, compares against end.
     *
     * @param {TokenRange} other Range to compare with.
     * @returns {Number}
     */
    compare(other) {
        const compareStart = this.start.compare(other.start);
        return compareStart !== 0 ? compareStart : this.end.compare(other.end);
    }

    toString() {
        return util.format(
            "]%s, %s]",
            this.start.toString(),
            this.end.toString(),
        );
    }
}

exports.Token = Token;
exports.TokenRange = TokenRange;
exports.ByteOrderedToken = ByteOrderedToken;
exports.Murmur3Token = Murmur3Token;
exports.RandomToken = RandomToken;