tsid-ts
Version:
A TypeScript library for generating Time-Sorted Unique Identifiers (TSID).
421 lines (420 loc) • 18.1 kB
JavaScript
"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();