@nats-io/nuid
Version:
NUID - A highly performant unique identifier generator.
177 lines • 6.31 kB
JavaScript
/*
* Copyright 2016-2026 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.nuid = exports.NuidImpl = void 0;
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const base = 62;
const preLen = 12;
const seqLen = 10;
const minInc = 33;
const maxInc = 333;
const totalLen = preLen + seqLen;
// Precomputed digit char codes — avoids repeated `digits.charCodeAt(i)`
// in the per-call hot path (`fillSeq` runs 10 lookups per `next()`).
const DIGIT_CODES = new Uint8Array(base);
for (let i = 0; i < base; i++)
DIGIT_CODES[i] = digits.charCodeAt(i);
// base^seqLen exceeds Number.MAX_SAFE_INTEGER, so seq is stored as two 32-bit
// halves: seq = seqHi * 2^32 + seqLo. Boundaries derived via BigInt to avoid
// double-precision rounding traps in the literal/intermediate math.
const TWO32 = 0x1_0000_0000;
const MAX_SEQ = BigInt(base) ** BigInt(seqLen);
const MAX_HI = Number(MAX_SEQ / (1n << 32n));
const MAX_LO = Number(MAX_SEQ % (1n << 32n));
function _getRandomValues(a) {
for (let i = 0; i < a.length; i++) {
a[i] = Math.floor(Math.random() * 256);
}
}
function fillRandom(a) {
if (globalThis?.crypto?.getRandomValues) {
globalThis.crypto.getRandomValues(a);
}
else {
_getRandomValues(a);
}
}
/**
* `NuidImpl` is the internal implementation backing the public `Nuid`
* interface exported from `mod.ts`. Generates 22-base-62-character ids:
* 12-char crypto-random prefix + 10-char base-62 sequence.
*
* Output format matches Go `nats-io/nuid` — same alphabet and length —
* so nuids generated here look the same as nuids generated by Go.
*/
class NuidImpl {
buf;
cbuf;
seqHi;
seqLo;
inc;
inited;
constructor() {
this.buf = new Uint8Array(totalLen);
this.cbuf = new Uint8Array(preLen);
this.inited = false;
}
/**
* Initializes a nuid with a crypto random prefix,
* and pseudo-random sequence and increment. This function
* is only called if any api on a nuid is called.
*
* @ignore
*/
init() {
this.inited = true;
this.setPre();
this.initSeqAndInc();
// fillSeq here keeps `buf` self-consistent after init()/reset(), so any
// observer of `buf` sees a prefix paired with its current seq encoding
// rather than a stale residue. Cold path — runs only at init / reset /
// prefix overflow, not in the per-call hot path.
this.fillSeq();
}
/**
* Initializes the pseudo random sequence number and the increment range.
* @ignore
*/
initSeqAndInc() {
// Two independent rand calls preserve full entropy across the two 32-bit
// halves; a single rand * MAX_SEQ would zero out the lower bits since
// MAX_SEQ > 2^53. Rejection sampling keeps the distribution uniform over
// [0, MAX_SEQ). Under any real PRNG this terminates on iteration 1
// (P(reject) ≈ 2e-9). The retry bound defends against adversarial
// Math.random stubs (e.g. test mocks pinned to one value) so we don't
// spin; on exhaustion we clamp to a known-valid pair.
let tries = 0;
do {
this.seqHi = Math.floor(Math.random() * (MAX_HI + 1));
this.seqLo = Math.floor(Math.random() * TWO32);
} while (tries++ < 8 && this.seqHi === MAX_HI && this.seqLo >= MAX_LO);
if (this.seqHi === MAX_HI && this.seqLo >= MAX_LO) {
this.seqLo = 0;
}
this.inc = (Math.random() * (maxInc - minInc) + minInc) | 0;
}
/**
* Sets the prefix from crypto random bytes. Converts them to base62.
*
* @ignore
*/
setPre() {
fillRandom(this.cbuf);
for (let i = 0; i < preLen; i++) {
this.buf[i] = DIGIT_CODES[this.cbuf[i] % base];
}
}
/**
* Fills the sequence portion of the buffer as base62 from
* the split-int seq (seqHi, seqLo). Performs long division
* by 62 over the 64-bit value using doubles only — no bigint.
*
* @ignore
*/
fillSeq() {
let hi = this.seqHi;
let lo = this.seqLo;
for (let i = totalLen - 1; i >= preLen; i--) {
const hiQ = Math.floor(hi / base);
const hiR = hi - hiQ * base;
// combined <= 61 * 2^32 + (2^32-1) < 2^53 — exact in double
const combined = hiR * TWO32 + lo;
const loQ = Math.floor(combined / base);
const rem = combined - loQ * base;
this.buf[i] = DIGIT_CODES[rem];
hi = hiQ;
lo = loQ;
}
}
/**
* Returns the next nuid.
*/
next() {
if (!this.inited) {
this.init();
}
this.seqLo += this.inc;
if (this.seqLo >= TWO32) {
this.seqLo -= TWO32;
this.seqHi += 1;
}
if (this.seqHi > MAX_HI ||
(this.seqHi === MAX_HI && this.seqLo >= MAX_LO)) {
this.setPre();
this.initSeqAndInc();
}
this.fillSeq();
const b = this.buf;
return String.fromCharCode(b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19], b[20], b[21]);
}
/**
* Resets the prefix and counter for the nuid. This is typically
* called automatically from within next() if the current sequence
* exceeds the resolution of the nuid.
*/
reset() {
this.init();
}
}
exports.NuidImpl = NuidImpl;
/**
* A nuid instance you can use by simply calling `next()` on it.
*/
exports.nuid = new NuidImpl();
//# sourceMappingURL=nuid.js.map