Comparing version 0.2.3 to 0.2.4
/** | ||
* SCRU128: Sortable, Clock and Random number-based Unique identifier | ||
* | ||
* @example | ||
* ```javascript | ||
* import { scru128 } from "scru128"; | ||
* | ||
* console.log(scru128()); // e.g. "00PGHAJ3Q9VAJ7IU6PQBHBUAK4" | ||
* console.log(scru128()); // e.g. "00PGHAJ3Q9VAJ7KU6PQ92NVBTV" | ||
* ``` | ||
* | ||
* @license Apache-2.0 | ||
@@ -8,7 +16,122 @@ * @copyright 2021 LiosK | ||
*/ | ||
/** Unix time in milliseconds at 2020-01-01 00:00:00+00:00. */ | ||
export declare const TIMESTAMP_BIAS = 1577836800000; | ||
/** | ||
* Represents a SCRU128 ID generator and provides an interface to do more than | ||
* just generate a string representation. | ||
* | ||
* @example | ||
* ```javascript | ||
* import { Generator } from "scru128"; | ||
* | ||
* const g = new Generator(); | ||
* const x = g.generate(); | ||
* console.log(x.toString()); | ||
* console.log(BigInt(x.toHex())); | ||
* ``` | ||
*/ | ||
export declare class Generator { | ||
/** Timestamp at last generation. */ | ||
private tsLastGen; | ||
/** Counter at last generation. */ | ||
private counter; | ||
/** Timestamp at last renewal of perSecRandom. */ | ||
private tsLastSec; | ||
/** Per-second random value at last generation. */ | ||
private perSecRandom; | ||
/** Maximum number of checking `Date.now()` until clock goes forward. */ | ||
private nClockCheckMax; | ||
/** Returns a `k`-bit (cryptographically strong) random unsigned integer. */ | ||
private getRandomBits; | ||
/** Generates a new SCRU128 ID object. */ | ||
generate(): Scru128Id; | ||
} | ||
/** | ||
* Represents a SCRU128 ID and provides converters to/from string and numbers. | ||
* | ||
* @example | ||
* ```javascript | ||
* import { Scru128Id } from "scru128"; | ||
* | ||
* const x = Scru128Id.fromString("00Q1D9AB6DTJNLJ80SJ42SNJ4F"); | ||
* console.log(x.toString()); | ||
* | ||
* const y = Scru128Id.fromHex(0xd05a952ccdecef5aa01c9904e5a115n.toString(16)); | ||
* console.log(BigInt(y.toHex())); | ||
* ``` | ||
*/ | ||
export declare class Scru128Id { | ||
readonly timestamp: number; | ||
readonly counter: number; | ||
readonly perSecRandom: number; | ||
readonly perGenRandom: number; | ||
/** Creates an object from field values. */ | ||
private constructor(); | ||
/** | ||
* Creates an object from field values. | ||
* | ||
* @param timestamp - 44-bit millisecond timestamp field. | ||
* @param counter - 28-bit per-millisecond counter field. | ||
* @param perSecRandom - 24-bit per-second randomness field. | ||
* @param perGenRandom - 32-bit per-generation randomness field. | ||
* @throws RangeError if any argument is out of the range of each field. | ||
* @category Conversion | ||
*/ | ||
static fromFields(timestamp: number, counter: number, perSecRandom: number, perGenRandom: number): Scru128Id; | ||
/** | ||
* Creates an object from a 26-digit string representation. | ||
* | ||
* @throws SyntaxError if the argument is not a valid string representation. | ||
* @category Conversion | ||
*/ | ||
static fromString(value: string): Scru128Id; | ||
/** | ||
* Returns the 26-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toString(): string; | ||
/** | ||
* Creates an object from a 128-bit unsigned integer encoded in a hexadecimal | ||
* string. | ||
* | ||
* @throws SyntaxError if the argument is not a hexadecimal string encoding a | ||
* 128-bit unsigned integer. | ||
* @category Conversion | ||
*/ | ||
static fromHex(value: string): Scru128Id; | ||
/** | ||
* Returns the 128-bit unsigned integer representation as a 32-digit | ||
* hexadecimal string prefixed with "0x". | ||
* | ||
* @category Conversion | ||
*/ | ||
toHex(): string; | ||
/** Represents `this` in JSON as a 26-digit canonical string. */ | ||
toJSON(): string; | ||
/** Creates an object from `this`. */ | ||
clone(): Scru128Id; | ||
/** Returns true if `this` is equivalent to `other`. */ | ||
equals(other: Scru128Id): boolean; | ||
/** | ||
* Returns a negative integer, zero, and positive integer if `this` is less | ||
* than, equal to, and greater than `other`, respectively. | ||
*/ | ||
compareTo(other: Scru128Id): number; | ||
} | ||
/** | ||
* Generates a new SCRU128 ID encoded in a string. | ||
* | ||
* Use this function to quickly get a new SCRU128 ID as a string. Use | ||
* [[Generator]] to do more. | ||
* | ||
* @returns 26-digit canonical string representation. | ||
* @example | ||
* ```javascript | ||
* import { scru128 } from "scru128"; | ||
* | ||
* const x = scru128(); | ||
* console.log(x); | ||
* ``` | ||
*/ | ||
export declare const scru128: () => string; |
@@ -5,2 +5,10 @@ "use strict"; | ||
* | ||
* @example | ||
* ```javascript | ||
* import { scru128 } from "scru128"; | ||
* | ||
* console.log(scru128()); // e.g. "00PGHAJ3Q9VAJ7IU6PQBHBUAK4" | ||
* console.log(scru128()); // e.g. "00PGHAJ3Q9VAJ7KU6PQ92NVBTV" | ||
* ``` | ||
* | ||
* @license Apache-2.0 | ||
@@ -11,8 +19,10 @@ * @copyright 2021 LiosK | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports._internal = exports.scru128 = void 0; | ||
exports._internal = exports.scru128 = exports.Scru128Id = exports.Generator = exports.TIMESTAMP_BIAS = void 0; | ||
const crypto_1 = require("crypto"); | ||
/** Unix time in milliseconds as at 2020-01-01 00:00:00+00:00. */ | ||
const TIMESTAMP_EPOCH = 1577836800000; // Date.UTC(2020, 0) | ||
/** Unix time in milliseconds at 2020-01-01 00:00:00+00:00. */ | ||
exports.TIMESTAMP_BIAS = 1577836800000; // Date.UTC(2020, 0) | ||
/** Maximum value of 28-bit counter field. */ | ||
const MAX_COUNTER = 268435455; | ||
/** Leading zeros to polyfill padStart(n, "0") with slice(-n). */ | ||
const PAD_ZEROS = "0000000000000000"; | ||
/** Returns a random bit generator based on available cryptographic RNG. */ | ||
@@ -43,2 +53,12 @@ const detectRng = () => { | ||
* just generate a string representation. | ||
* | ||
* @example | ||
* ```javascript | ||
* import { Generator } from "scru128"; | ||
* | ||
* const g = new Generator(); | ||
* const x = g.generate(); | ||
* console.log(x.toString()); | ||
* console.log(BigInt(x.toHex())); | ||
* ``` | ||
*/ | ||
@@ -55,2 +75,4 @@ class Generator { | ||
this.perSecRandom = 0; | ||
/** Maximum number of checking `Date.now()` until clock goes forward. */ | ||
this.nClockCheckMax = 1000000; | ||
/** Returns a `k`-bit (cryptographically strong) random unsigned integer. */ | ||
@@ -69,6 +91,6 @@ this.getRandomBits = detectRng(); | ||
// wait a moment until clock goes forward when counter overflows | ||
let nTrials = 0; | ||
let nClockCheck = 0; | ||
while (tsNow <= this.tsLastGen) { | ||
tsNow = Date.now(); | ||
if (++nTrials > 1000000) { | ||
if (++nClockCheck > this.nClockCheckMax) { | ||
console.warn("scru128: reset state as clock did not go forward"); | ||
@@ -87,7 +109,19 @@ this.tsLastSec = 0; | ||
} | ||
return Scru128Id.fromFields(this.tsLastGen - TIMESTAMP_EPOCH, this.counter, this.perSecRandom, this.getRandomBits(32)); | ||
return Scru128Id.fromFields(this.tsLastGen - exports.TIMESTAMP_BIAS, this.counter, this.perSecRandom, this.getRandomBits(32)); | ||
} | ||
} | ||
exports.Generator = Generator; | ||
/** | ||
* Represents a SCRU128 ID and provides converters to/from string and numbers. | ||
* | ||
* @example | ||
* ```javascript | ||
* import { Scru128Id } from "scru128"; | ||
* | ||
* const x = Scru128Id.fromString("00Q1D9AB6DTJNLJ80SJ42SNJ4F"); | ||
* console.log(x.toString()); | ||
* | ||
* const y = Scru128Id.fromHex(0xd05a952ccdecef5aa01c9904e5a115n.toString(16)); | ||
* console.log(BigInt(y.toHex())); | ||
* ``` | ||
*/ | ||
@@ -123,2 +157,4 @@ class Scru128Id { | ||
* @param perGenRandom - 32-bit per-generation randomness field. | ||
* @throws RangeError if any argument is out of the range of each field. | ||
* @category Conversion | ||
*/ | ||
@@ -128,12 +164,8 @@ static fromFields(timestamp, counter, perSecRandom, perGenRandom) { | ||
} | ||
/** Returns the 26-digit canonical string representation. */ | ||
toString() { | ||
const h48 = this.timestamp * 0x10 + (this.counter >> 24); | ||
const m40 = (this.counter & 16777215) * 65536 + (this.perSecRandom >> 8); | ||
const l40 = (this.perSecRandom & 0xff) * 4294967296 + this.perGenRandom; | ||
return (("000000000" + h48.toString(32)).slice(-10) + | ||
("0000000" + m40.toString(32)).slice(-8) + | ||
("0000000" + l40.toString(32)).slice(-8)).toUpperCase(); | ||
} | ||
/** Creates an object from a 26-digit string representation. */ | ||
/** | ||
* Creates an object from a 26-digit string representation. | ||
* | ||
* @throws SyntaxError if the argument is not a valid string representation. | ||
* @category Conversion | ||
*/ | ||
static fromString(value) { | ||
@@ -149,3 +181,67 @@ const m = value.match(/^([0-7][0-9A-V]{9})([0-9A-V]{8})([0-9A-V]{8})$/i); | ||
} | ||
/** | ||
* Returns the 26-digit canonical string representation. | ||
* | ||
* @category Conversion | ||
*/ | ||
toString() { | ||
const h48 = this.timestamp * 0x10 + (this.counter >> 24); | ||
const m40 = (this.counter & 16777215) * 65536 + (this.perSecRandom >> 8); | ||
const l40 = (this.perSecRandom & 0xff) * 4294967296 + this.perGenRandom; | ||
return ((PAD_ZEROS + h48.toString(32)).slice(-10) + | ||
(PAD_ZEROS + m40.toString(32)).slice(-8) + | ||
(PAD_ZEROS + l40.toString(32)).slice(-8)).toUpperCase(); | ||
} | ||
/** | ||
* Creates an object from a 128-bit unsigned integer encoded in a hexadecimal | ||
* string. | ||
* | ||
* @throws SyntaxError if the argument is not a hexadecimal string encoding a | ||
* 128-bit unsigned integer. | ||
* @category Conversion | ||
*/ | ||
static fromHex(value) { | ||
const m = value.match(/^(?:0x)?0*(0|[1-9a-f][0-9a-f]*)$/i); | ||
if (m === null || m[1].length > 32) { | ||
throw new SyntaxError("invalid hexadecimal integer: " + value); | ||
} | ||
return new Scru128Id(parseInt(m[1].slice(-32, -21) || "0", 16), parseInt(m[1].slice(-21, -14) || "0", 16), parseInt(m[1].slice(-14, -8) || "0", 16), parseInt(m[1].slice(-8) || "0", 16)); | ||
} | ||
/** | ||
* Returns the 128-bit unsigned integer representation as a 32-digit | ||
* hexadecimal string prefixed with "0x". | ||
* | ||
* @category Conversion | ||
*/ | ||
toHex() { | ||
return ("0x" + | ||
(PAD_ZEROS + this.timestamp.toString(16)).slice(-11) + | ||
(PAD_ZEROS + this.counter.toString(16)).slice(-7) + | ||
(PAD_ZEROS + this.perSecRandom.toString(16)).slice(-6) + | ||
(PAD_ZEROS + this.perGenRandom.toString(16)).slice(-8)); | ||
} | ||
/** Represents `this` in JSON as a 26-digit canonical string. */ | ||
toJSON() { | ||
return this.toString(); | ||
} | ||
/** Creates an object from `this`. */ | ||
clone() { | ||
return new Scru128Id(this.timestamp, this.counter, this.perSecRandom, this.perGenRandom); | ||
} | ||
/** Returns true if `this` is equivalent to `other`. */ | ||
equals(other) { | ||
return this.compareTo(other) === 0; | ||
} | ||
/** | ||
* Returns a negative integer, zero, and positive integer if `this` is less | ||
* than, equal to, and greater than `other`, respectively. | ||
*/ | ||
compareTo(other) { | ||
return Math.sign(this.timestamp - other.timestamp || | ||
this.counter - other.counter || | ||
this.perSecRandom - other.perSecRandom || | ||
this.perGenRandom - other.perGenRandom); | ||
} | ||
} | ||
exports.Scru128Id = Scru128Id; | ||
const defaultGenerator = new Generator(); | ||
@@ -155,3 +251,13 @@ /** | ||
* | ||
* Use this function to quickly get a new SCRU128 ID as a string. Use | ||
* [[Generator]] to do more. | ||
* | ||
* @returns 26-digit canonical string representation. | ||
* @example | ||
* ```javascript | ||
* import { scru128 } from "scru128"; | ||
* | ||
* const x = scru128(); | ||
* console.log(x); | ||
* ``` | ||
*/ | ||
@@ -165,2 +271,2 @@ const scru128 = () => defaultGenerator.generate().toString(); | ||
*/ | ||
exports._internal = { Scru128Id, detectRng }; | ||
exports._internal = { detectRng }; |
{ | ||
"name": "scru128", | ||
"version": "0.2.3", | ||
"version": "0.2.4", | ||
"description": "SCRU128: Sortable, Clock and Random number-based Unique identifier", | ||
@@ -46,7 +46,7 @@ "main": "./dist/index.js", | ||
"mocha": "^9.1.3", | ||
"typedoc": "^0.22.5", | ||
"typedoc": "^0.22.7", | ||
"typescript": "^4.4.4", | ||
"webpack": "^5.58.2", | ||
"webpack-cli": "^4.9.0" | ||
"webpack": "^5.60.0", | ||
"webpack-cli": "^4.9.1" | ||
} | ||
} |
@@ -9,3 +9,3 @@ # SCRU128: Sortable, Clock and Random number-based Unique identifier | ||
- Sortable by generation time (as integer and as text) | ||
- 26-character case-insensitive portable textual representation | ||
- 26-digit case-insensitive portable textual representation | ||
- 44-bit biased millisecond timestamp that ensures remaining life of 550 years | ||
@@ -22,71 +22,9 @@ - Up to 268 million time-ordered but unpredictable unique IDs per millisecond | ||
See [SCRU128 Specification] for details. | ||
[uuid]: https://en.wikipedia.org/wiki/Universally_unique_identifier | ||
[ulid]: https://github.com/ulid/spec | ||
[ksuid]: https://github.com/segmentio/ksuid | ||
[scru128 specification]: https://github.com/scru128/spec | ||
## Design | ||
A SCRU128 ID is a 128-bit unsigned integer consisting of four terms: | ||
``` | ||
timestamp * 2^84 + counter * 2^56 + per_sec_random * 2^32 + per_gen_random | ||
``` | ||
Where: | ||
- `timestamp` is a 44-bit unix time in milliseconds biased by 50 years (i.e. | ||
milliseconds elapsed since 2020-01-01 00:00:00+00:00, ignoring leap seconds). | ||
- `counter` is a 28-bit counter incremented by one for each ID generated within | ||
the same `timestamp` (reset to a random number every millisecond). | ||
- `per_sec_random` is a 24-bit random number refreshed only once per second. | ||
- `per_gen_random` is a 32-bit random number renewed per generation of a new ID. | ||
This is essentially equivalent to allocating four unsigned integer fields to a | ||
128-bit space as follows in a big-endian system, and thus it is easily | ||
implemented with binary operations. | ||
| Bit numbers | Field name | Size | Data type | | ||
| ------------ | -------------- | ------- | ---------------- | | ||
| Msb 0 - 43 | timestamp | 44 bits | Unsigned integer | | ||
| Msb 44 - 71 | counter | 28 bits | Unsigned integer | | ||
| Msb 72 - 95 | per_sec_random | 24 bits | Unsigned integer | | ||
| Msb 96 - 127 | per_gen_random | 32 bits | Unsigned integer | | ||
### Layered randomness | ||
SCRU128 utilizes monotonic `counter` to guarantee the uniqueness of IDs with the | ||
same `timestamp`; however, this mechanism does not ensure the uniqueness of IDs | ||
generated by multiple generators that do not share a `counter` state. SCRU128 | ||
relies on random numbers to avoid such collisions. | ||
For a given length of random bits, the greater the number of random numbers | ||
generated, the higher the probability of collision. Therefore, SCRU128 gives | ||
some random bits a longer life to reduce the number of random number generation | ||
per a unit of time. As a result, even if each of multiple generators generates a | ||
million IDs at the same millisecond, no collision will occur as long as the | ||
random numbers generated only once per second (`per_sec_random`) differ. | ||
That being said, the `per_sec_random` field is refreshed every second to prevent | ||
potential attackers from using this field as a generator's fingerprint. Also, | ||
the 32-bit `per_gen_random` field is reset to a new random number whenever an ID | ||
is generated to make sure the adjacent IDs generated within the same `timestamp` | ||
are not predictable. | ||
## Textual representation | ||
A SCRU128 ID is encoded in a string as a 128-bit unsigned integer denoted in the | ||
radix of 32 using the digits of `[0-9A-V]`, with leading zeros added to form a | ||
26-digit canonical representation. Converters for this simple base 32 notation | ||
are widely available in many languages; even if not, it is easily implemented | ||
with bitwise operations by translating each 5-bit group into one digit of | ||
`[0-9A-V]`, from the least significant digit to the most. Since the three most | ||
significant bits are mapped to one of `[0-7]`, any numeral greater than | ||
`7VVVVVVVVVVVVVVVVVVVVVVVVV` is not a valid SCRU128 ID. | ||
Note that this is different from some binary-to-text encodings referred to as | ||
_base32_ or _base32hex_ (e.g. [RFC 4648]), which read and translate 5-bit groups | ||
from the most significant one to the least. | ||
[rfc 4648]: https://www.ietf.org/rfc/rfc4648.txt | ||
## License | ||
@@ -93,0 +31,0 @@ |
28804
403
48