UNPKG

@nats-io/nuid

Version:

NUID - A highly performant unique identifier generator.

177 lines 6.31 kB
/* * 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