UNPKG

tsid-ts

Version:

A TypeScript library for generating Time-Sorted Unique Identifiers (TSID).

421 lines (420 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TSIDGenerator = exports.TSID = exports.getTsid = void 0; var utils_1 = require("./utils"); var TSID_BYTES_LENGTH = 8; var TSID_CHARS_LENGTH = 13; var TSID_HEX_CHARS_LENGTH = 16; var _EPOCH_ISO = '2020-01-01T00:00:00+00:00'; var TSID_EPOCH = new Date(_EPOCH_ISO).getTime(); var RANDOM_BITS = 22; var RANDOM_MASK = 0x3fffff; var TSID_DEFAULT_NODE_BITS = 10; /** Base 32 */ var ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; var ALPHABET_VALUES = Array.from({ length: 128 }, function () { return -1; }); function _setAlphabetValues(chars, start) { for (var i = 0; i < chars.length; i++) { ALPHABET_VALUES[chars.charCodeAt(i)] = start + i; ALPHABET_VALUES[chars.charCodeAt(i).toString().toUpperCase().charCodeAt(0)] = start + i; } } _setAlphabetValues('0123456789', 0); _setAlphabetValues('abcdefghjkmnpqrstvwxyz', 0xa); _setAlphabetValues('oi', 0); _setAlphabetValues('l', 1); /** * Returns a new TSID. * @returns TSID */ var getTsid = function () { return TSID.create(); }; exports.getTsid = getTsid; var TSID = /** @class */ (function () { function TSID(number) { var _this = this; this.toBigInt = function () { return _this._number; }; /** * Converts the TSID to a canonical string. * * @returns {string} The canonical string (13-chars length string) representation of the TSID. * * @example * // Convert a TSID to a canonical string. * const tsid = TSID.create() * const canonicalString = tsid.toString() * console.log(canonicalString) // Output: '0DXBG0DVMS8ER' */ this.toString = function () { return _this.toCanonicalString(); }; this.toHexString = function () { return (0, utils_1.encode)(_this._number, 16, TSID_BYTES_LENGTH * 2); }; this.toDecimalString = function () { return (0, utils_1.encode)(_this._number, 10); }; this.toBase62String = function () { return (0, utils_1.encode)(_this._number, 62); }; this._number = number & BigInt('0xFFFFFFFFFFFFFFFF'); // 64-bit this._epoch = TSID_EPOCH; } TSID.prototype.setEpoch = function (epoch) { this._epoch = epoch; }; Object.defineProperty(TSID.prototype, "timestamp", { /** * Returns the timestamp component of the TSID. * * The timestamp is the number of milliseconds elapsed since the TSID epoch. * * @returns {number} The timestamp value. * * @example * // The timestamp of a TSID with value 0 should be equal to TSID_EPOCH. * TSID(0).timestamp === TSID_EPOCH; // true * * // The timestamp of a newly created TSID should be close to the current time. * TSID.create().timestamp - time.time() * 1000 < 1; // true * * // The timestamp of a TSID with value (1 << RANDOM_BITS) should be TSID_EPOCH + 1. * TSID(1 << RANDOM_BITS).timestamp === TSID_EPOCH + 1; // true */ get: function () { return this._epoch + Number(this._number >> BigInt(RANDOM_BITS)); }, enumerable: false, configurable: true }); Object.defineProperty(TSID.prototype, "random", { /** * Returns the random component of the TSID. * * This component contains the node and counter bits. * * @returns {number} The random component value. * * @example * // The random component of a TSID with value 0 should be 0. * TSID(0).random === 0; // true * * // The random component of a TSID with value 1 should be 1. * TSID(1).random === 1; // true * * // The random component of a TSID with a specific value should be the expected value. * TSID((0xffffffff << RANDOM_BITS) + 255).random === 255; // true */ get: function () { return Number(this._number & BigInt(RANDOM_MASK)); }, enumerable: false, configurable: true }); Object.defineProperty(TSID.prototype, "number", { /** * Converts the TSID into a bigint object. * * This simply unwraps the internal value. * * @returns {bigint} The bigint object representation of the TSID. * * @example * // The number representation of a TSID with a specific value should be the expected value. * TSID(0xffff0000000000000000).number === 0; // true * * // The number representation of a TSID with value 0 should be 0. * TSID(0x0000000000000000).number === 0; // true * * // The number representation of a TSID with value 0xffff should be 0xffff. * TSID(0xffff).number === 0xffff; // true */ get: function () { return this._number; }, enumerable: false, configurable: true }); TSID.prototype.toCanonicalString = function () { var _this = this; return Array.from({ length: 13 }, function (_, i) { return ALPHABET[Number((_this._number >> BigInt(60 - i * 5)) & BigInt(0x1f))]; }).join(''); }; /** * Converts the TSID into a byte array. * * @returns {Uint8Array} The byte array representation of the TSID. * * @example * // Convert a TSID with value 1 into a byte array and check if it matches the expected result. * const t1 = TSID(1); * t1.toBytes() === new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1]); // true * * // Convert a TSID with value 0xfabada into a byte array and check if it matches the expected result. * const t2 = TSID(0xfabada); * t2.toBytes() === new Uint8Array([0, 0, 0, 0, 0, 0xfa, 0xba, 0xda]); // true * * // Convert a TSID with a larger value into a byte array and check if it matches the expected result. * const t3 = TSID(0xffcafefabadabeef); * t3.toBytes() === new Uint8Array([0xff, 0xca, 0xfe, 0xfa, 0xba, 0xda, 0xbe, 0xef]); // true * * // Convert a TSID with an even larger value into a byte array and check if it matches the expected result. * const t4 = TSID(0xffffffcafefabadabeef); * t4.toBytes() === new Uint8Array([0xff, 0xca, 0xfe, 0xfa, 0xba, 0xda, 0xbe, 0xef]); // true */ TSID.prototype.toBytes = function () { var byteArray = new Uint8Array(TSID_BYTES_LENGTH); for (var i = 0; i < TSID_BYTES_LENGTH; i++) { byteArray[i] = Number((this._number >> BigInt((TSID_BYTES_LENGTH - 1 - i) * 8)) & BigInt(0xff)); } return byteArray; }; /** * Converts the TSID into a string. * * Supports the following formats: * - `S`: canonical string in upper case. * - `s`: canonical string in lower case. * - `X`: hexadecimal in upper case. * - `x`: hexadecimal in lower case. * - `d`: base-10. * - `z`: base-62. * * @param {StringFormat} format - The desired format for the string representation. * @returns {string} The string representation of the TSID in the specified format. * * @example * // Create a TSID from a string and convert it to a canonical upper case string. * const t1 = TSID.fromString('0AWE5HZP3SKTK'); * t1.toStringWithFormat('S') === '0AWE5HZP3SKTK'; // true * * // Convert the same TSID to a lower case canonical string. * t1.toStringWithFormat('s') === '0awe5hzp3sktk'; // true * * // Create a TSID from a string and convert it to hexadecimal in upper case. * const t2 = TSID.fromString('0AXFXR5W7VBX0'); * t2.toStringWithFormat('X') === '0575FDC1787DAFA0'; // true * * // Create a TSID from a string and convert it to base-10. * const t3 = TSID(437283649808777971); * t3.toStringWithFormat('d') === '437283649808777971'; // true * * // Convert the same TSID to base-62. * t3.toStringWithFormat('z') === 'WIlLsHwljH'; // true */ TSID.prototype.toStringWithFormat = function (format) { if (format === void 0) { format = 'S'; } var result; switch (format) { case 'S': result = this.toCanonicalString(); break; case 's': result = this.toCanonicalString().toLowerCase(); break; case 'X': result = (0, utils_1.encode)(this._number, 16, TSID_BYTES_LENGTH * 2); break; case 'x': result = (0, utils_1.encode)(this._number, 16, TSID_BYTES_LENGTH * 2).toLowerCase(); break; case 'd': result = (0, utils_1.encode)(this._number, 10); break; case 'z': result = (0, utils_1.encode)(this._number, 62); break; } return result; }; /** * Returns a new TSID. * * This static method is a quick alternative to `TSIDGenerator::create()`. * It can generate up to `2^22` (`4,194,304`) TSIDs per millisecond. * It can be useful, for example, for logging. * * Security-sensitive applications that require a cryptographically secure pseudo-random generator should use `TSIDGenerator::create()`. * * > Note: This method is not thread-safe by default. It's the responsibility of TSIDGenerator to ensure thread safety. * * @returns {TSID} A new TSID instance. * * @example * // Create three TSIDs and ensure that they are in ascending order. * const a = TSID.create(); * const b = TSID.create(); * const c = TSID.create(); * a.number < b.number < c.number; // true */ TSID.create = function () { return defaultGenerator.create(); }; /** * Converts a byte array into a TSID. * * @param {Uint8Array} bytes - The byte array to convert into a TSID. * @returns {TSID} The TSID created from the given byte array. * * @throws {Error} Thrown if the length of the byte array is not equal to `TSID_BYTES_LENGTH`. * * @example * // Convert a byte array with value 0 into a TSID and check if it equals TSID(0). * TSID.fromBytes(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])) === TSID(0); // true * * // Convert a byte array with value 1 into a TSID and check if it equals TSID(1). * TSID.fromBytes(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])) === TSID(1); // true * * // Convert a byte array with value 2 into a TSID and check if it equals TSID(2). * TSID.fromBytes(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2])) === TSID(2); // true * * // Convert a byte array with value 11 into a TSID and check if it equals TSID(11). * TSID.fromBytes(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 11])) === TSID(11); // true */ TSID.fromBytes = function (bytes) { if (bytes.length !== TSID_BYTES_LENGTH) { throw new Error("Invalid TSID bytes (len=".concat(bytes.length, " bytes, expected ").concat(TSID_BYTES_LENGTH, ")")); } var number = BigInt('0x' + Array.from(bytes) .map(function (b) { return b.toString(16).padStart(2, '0'); }) .join('')); return new TSID(number); }; /** * Converts a string into a TSID. * * Supports the following formats: * - `S`: canonical string in upper case. * - `s`: canonical string in lower case. * - `X`: hexadecimal in upper case. * - `x`: hexadecimal in lower case. * - `d`: base-10. * - `z`: base-62. * * @param {string} value - The string to convert into a TSID. * @param {StringFormat} format - The format of the input string. * @returns {TSID} The TSID created from the given string. * * @throws {Error} Thrown if the length of the string is not equal to the expected length for the specified format. * @throws {Error} Thrown if the format is invalid. * * @example * // Convert a string in hexadecimal format to a TSID and check if its number matches another TSID. * const t1 = TSID.fromString('0575FDC1787DAFA0', 'X'); * const t2 = TSID.fromString('0AXFXR5W7VBX0'); * t1.number === t2.number; // true * * // Convert a string in base-10 format to a TSID. * const t3 = TSID.fromString('12345', 'd'); * t3.number === BigInt(12345); // true * * // Convert a string in base-62 format to a TSID. * const t4 = TSID.fromString('abc123', 'z'); * t4.number === BigInt(decode('abc123', 62)); // true */ TSID.fromString = function (value, format) { if (format === void 0) { format = 'S'; } var number; switch (format) { case 'S': case 's': if (value.length !== TSID_CHARS_LENGTH) { throw new Error("Invalid TSID string: (len=".concat(value.length, " chars, but expected ").concat(TSID_CHARS_LENGTH, ")")); } number = Array.from(value).reduce(function (acc, c, i) { return acc + (BigInt(ALPHABET.indexOf(c.toUpperCase())) << BigInt(5 * (TSID_CHARS_LENGTH - i - 1))); }, BigInt(0)); break; case 'X': case 'x': if (value.length !== TSID_HEX_CHARS_LENGTH) { throw new Error("Invalid TSID string: (len=".concat(value.length, " chars, but expected ").concat(TSID_HEX_CHARS_LENGTH, ")")); } number = BigInt((0, utils_1.decode)(value.toUpperCase(), 16)); break; case 'd': number = BigInt((0, utils_1.decode)(value, 10)); break; case 'z': number = BigInt((0, utils_1.decode)(value, 62)); break; default: throw new Error("Invalid format: '".concat(format, "'")); } return new TSID(number); }; return TSID; }()); exports.TSID = TSID; /** * Creates a new TSID generator. * * @param {number | null} node - Node identifier. Defaults to a random integer within the range of `node_bits`. * @param {number} nodeBits - Number of bytes used to represent the node id. Defaults to `TSID_DEFAULT_NODE_BITS`. * @param {number} epoch - Epoch start in milliseconds. Defaults to `TSID_EPOCH`. * @param {(n: number) => bigint} randomFunction - Function to use to randomize the counter. Must return an n-bit integer. If `null`, the stdlib `random.getrandbits()` is used. * * @throws {Error} Thrown if `node` is less than 0. * @throws {Error} Thrown if `nodeBits` is not in the range [0, 20]. * @throws {Error} Thrown if `node` is too large for the given `nodeBits`. * * @example * // Create a TSID generator with a specific node and node bits. * const generator = new TSIDGenerator(1, 1); * generator.node; // 1 */ var TSIDGenerator = /** @class */ (function () { /** * Constructs a TSIDGenerator instance. * * @param {number | null} node - Node identifier. Defaults to a random integer within the range of `node_bits`. * @param {number} nodeBits - Number of bytes used to represent the node id. Defaults to `TSID_DEFAULT_NODE_BITS`. * @param {number} epoch - Epoch start in milliseconds. Defaults to `TSID_EPOCH`. * @param {(n: number) => bigint} randomFunction - Function to use to randomize the counter. Must return an n-bit integer. If `null`, the stdlib `random.getrandbits()` is used. */ function TSIDGenerator(node, nodeBits, epoch, randomFunction) { if (node === void 0) { node = null; } if (nodeBits === void 0) { nodeBits = TSID_DEFAULT_NODE_BITS; } if (epoch === void 0) { epoch = TSID_EPOCH; } if (node !== null && node < 0) { throw new Error("Invalid node: ".concat(node)); } if (nodeBits < 0 || nodeBits > 20) { throw new Error("Invalid node_bits: ".concat(nodeBits)); } this.randomFunction = randomFunction || (function (n) { return BigInt((0, utils_1.uniformInt)(0, Math.pow(2, n))); }); this.node = node === null ? Number(BigInt((0, utils_1.uniformInt)(0, Math.pow(2, 20))) >> BigInt(20 - nodeBits)) : node; if (this.node >> nodeBits > 0) { throw new Error("Node ".concat(this.node, " too large for node_bits==").concat(nodeBits)); } this._epoch = epoch; this._nodeBits = nodeBits; this._counterBits = RANDOM_BITS - nodeBits; this.counter = this.randomFunction(this._counterBits); this._milliseconds = new Date().getTime(); this._counterMask = BigInt(RANDOM_MASK) >> BigInt(nodeBits); this._nodeMask = BigInt(RANDOM_MASK) >> BigInt(this._counterBits); } /** * Generates a new TSID. * * @returns {TSID} A new TSID instance. * * @example * // Generate a new TSID using the generator. * const t = generator.create(); */ TSIDGenerator.prototype.create = function () { var currentMilliseconds = new Date().getTime(); if (currentMilliseconds - this._milliseconds < 1) { this.counter += BigInt(1); if (this.counter >> BigInt(this._counterBits) !== BigInt(0)) { this._milliseconds += 1; this.counter = BigInt(0); } } else { this._milliseconds = currentMilliseconds; var rnd = this.randomFunction(this._counterBits); this.counter = rnd & this._counterMask; } var milliseconds = BigInt(this._milliseconds - this._epoch) << BigInt(RANDOM_BITS); var node = (BigInt(this.node) & this._nodeMask) << BigInt(this._counterBits); var counter = this.counter & this._counterMask; var result = new TSID(milliseconds + node + counter); result.setEpoch(this._epoch); return result; }; return TSIDGenerator; }()); exports.TSIDGenerator = TSIDGenerator; var defaultGenerator = new TSIDGenerator();