ULID Generator — JavaScript source
Generate Universally Unique Lexicographically Sortable Identifiers (ULID) - 26-character Crockford-base32 strings that sort by millisecond timestamp. Paste any ULID to decode its timestamp and randomness. Runs entirely in your browser.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
'use strict';
/**
* ulid-generator - Universally Unique Lexicographically Sortable IDentifier.
*
* Language: JavaScript (Node 14+, standard library only)
* Source: CosmoDev polyglot showcase port of the ULID Generator tool, ported
* from cli/ulid-generator/ulid-generator.go (the hand-rolled Go twin
* of src/lib/ulid.ts - the canonical TypeScript implementation).
* License: display source - part of CosmoDev's polyglot tool pages.
*
* Design goals:
* - Pure + deterministic; the generator never throws (decode throws on
* malformed input, matching the TS `throw` and Go error).
* - Functionally equivalent to the Go twin: same inputs -> same outputs for
* the timestamp prefix, which is the cross-language lock-step anchor.
* - Self-contained: stdlib only (no npm dependencies - no `ulid` package).
*
* A ULID is 26 Crockford-base32 chars: the first 10 encode a 48-bit millisecond
* timestamp (MSB-first) and the last 16 encode 80 bits of randomness. Crockford
* alphabet: "0123456789ABCDEFGHJKMNPQRSTVWXYZ" (excludes I, L, O, U).
*
* Time-encoding is the lock-step ANCHOR. Because 32^16 === 2^80 exactly,
* packing [6 time bytes | 10 random bytes] into a 16-byte big-endian integer
* and base32-encoding the whole 128-bit value reproduces the Go twin's separate
* time encoding byte-for-byte in the first 10 chars - so
* decodeUlidTime(generateUlid(ms)) === ms holds identically. The random tail is
* drawn from crypto.randomBytes (a Node stdlib CSPRNG, the JS analogue of Go's
* crypto/rand) and varies call to call, exactly as in the Go default-RNG path.
*
* Bit-width note: JS bitwise operators are 32-bit, so a 48-bit timestamp
* cannot be sliced with `>>`. We extract each byte with plain arithmetic
* (Math.floor division + modulo) - exact for every integer up to 2^53, which
* comfortably covers the 48-bit timestamp field.
*/
const crypto = require('crypto');
/** The Crockford base32 alphabet (excludes I, L, O, U). */
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
/**
* Map from an ASCII char CODE to its Crockford value. Built once. Accepts
* lowercase a-z where the uppercase equivalent is valid; i/l/o/u are absent
* because I/L/O/U are not in the alphabet - matching the `ulid` npm package's
* DECODING table, so a lowercase TS-produced ULID decodes the same way here.
* Keys are char codes (numbers) so the case fold happens at build time, not on
* every lookup.
*/
const DECODE = (() => {
const map = new Map();
for (let i = 0; i < CROCKFORD.length; i++) map.set(CROCKFORD.charCodeAt(i), i);
for (let c = 97; c <= 122; c++) {
// 97..122 = 'a'..'z'; the uppercase equivalent is c - 32.
const up = c - 32;
if (map.has(up)) map.set(c, map.get(up));
}
return map;
})();
/**
* Divide the big-endian integer stored in `b` (mutated in place) by 32 and
* return the remainder (0-31). Long division in base 256: each byte is
* combined with the carry from the previous (more-significant) byte, the high
* 8 bits become the new byte and the low 5 bits become the next carry.
*
* Values stay well under 2^16 (max `(31 << 8) | 255 === 8191`), so the 32-bit
* JS bitwise operators are exact here. Mirrors `divBy32` in the Go twin so the
* base32 digits come out identically.
*/
function divBy32(b) {
let rem = 0;
for (let i = 0; i < b.length; i++) {
const cur = (rem << 8) | b[i];
b[i] = cur >> 5;
rem = cur & 0x1f;
}
return rem;
}
/**
* Encode a 48-bit millisecond timestamp into 6 big-endian bytes (MSB first).
* Uses arithmetic rather than `>>` because JS bitwise ops are 32-bit and the
* timestamp can span the full 48-bit field. Exposed so showcase tests can build
* a deterministic `encodeUlid` input without touching the RNG.
*
* @param {number} ms millisecond timestamp (integer, 0 .. 2^48-1)
* @returns {number[]}
*/
function timeBytes(ms) {
return [
Math.floor(ms / 0x10000000000) % 256, // >> 40
Math.floor(ms / 0x100000000) % 256, // >> 32
Math.floor(ms / 0x1000000) % 256, // >> 24
Math.floor(ms / 0x10000) % 256, // >> 16
Math.floor(ms / 0x100) % 256, // >> 8
ms % 256,
];
}
/**
* Pure, deterministic ULID encoder: base32-encode the 128-bit big-endian value
* `[timeBytes | randomBytes]` into the canonical 26 chars. No RNG, no time
* source - the showcase tests assert exact strings through this function.
*
* @param {number[]} timeBytes exactly 6 bytes
* @param {number[]} randomBytes exactly 10 bytes
* @returns {string}
*/
function encodeUlid(timeBytes, randomBytes) {
const b = timeBytes.concat(randomBytes); // 16 bytes
const out = new Array(26);
// Repeatedly divide the 128-bit value by 32, collecting remainders LSB-first
// into out[25] down to out[0]. 26 base32 digits cover 130 bits, so the most
// significant digit captures the leftover top 3 bits (0-7).
for (let i = 25; i >= 0; i--) {
out[i] = CROCKFORD[divBy32(b)];
}
return out.join('');
}
/**
* Generate a new ULID for the given millisecond timestamp. The Go twin of
* generateUlid() in src/lib/ulid.ts. Never throws: `ms` is truncated to its
* low 48 bits; negative values encode the low 48 bits of the two's-complement
* representation (defined but not meaningful).
*
* @param {number} ms millisecond timestamp
* @returns {string}
*/
function generateUlid(ms) {
const randomBytes = Array.from(crypto.randomBytes(10)); // Node stdlib CSPRNG
return encodeUlid(timeBytes(ms), randomBytes);
}
/**
* Extract the 48-bit millisecond timestamp encoded in the first 10 chars of
* `id`. The Go twin of decodeUlidTime() in src/lib/ulid.ts. Throws if `id` is
* not exactly 26 chars or contains a character outside the Crockford base32
* alphabet (I, L, O, U are invalid) - erroring on the same inputs as the `ulid`
* npm package's decodeTime.
*
* @param {string} id
* @returns {number}
*/
function decodeUlidTime(id) {
if (id.length !== 26) {
throw new Error(`ulid: malformed id: length ${id.length}, want 26`);
}
let ts = 0;
for (let i = 0; i < 10; i++) {
const v = DECODE.get(id.charCodeAt(i));
if (v === undefined) {
throw new Error(`ulid: invalid character '${id[i]}' at position ${i}`);
}
ts = ts * 32 + v; // ts stays under 2^48 (well within Number safe range)
}
return ts;
}
// CommonJS export so the file is consumable from Node without a build step,
// while staying dependency-free and framework-agnostic.
module.exports = {
CROCKFORD,
timeBytes,
encodeUlid,
generateUlid,
decodeUlidTime,
};
// ---------- showcase tests (the canonical suite lives in src/lib) ----------
// Run only when invoked directly: `node ulid-generator.js`.
if (require.main === module) {
const zeros = new Array(10).fill(0);
const ulidRe = /^[0-9A-HJKMNP-TV-Z]{26}$/;
// All-zero input -> all-zero output; the canonical base32 of 0.
console.assert(encodeUlid(timeBytes(0), zeros) === '0'.repeat(26), 'zero encode');
// The lock-step anchor: decode(encode(ms)) === ms, regardless of the random
// tail. Verified across several byte boundaries in the 48-bit time field.
for (const ms of [0, 1, 42, 150000, 1000000, 2000000, (1 << 48) - 1]) {
const round = decodeUlidTime(encodeUlid(timeBytes(ms), zeros));
console.assert(round === ms, `round-trip failed for ms=${ms}`);
}
// 150000 base32 in Crockford is "4JFG" -> zero-padded to 10 chars.
console.assert(encodeUlid(timeBytes(150000), zeros).slice(0, 10) === '0000004JFG', 'known prefix');
// generateUlid produces a well-formed 26-char Crockford ULID that round-trips.
const gid = generateUlid(150000);
console.assert(ulidRe.test(gid), 'generate format');
console.assert(decodeUlidTime(gid) === 150000, 'generate round-trip');
// The time prefix is the most-significant part of the string, so an older
// timestamp sorts before a newer one regardless of the tail.
console.assert(generateUlid(1000000) < generateUlid(2000000), 'lexicographic order');
// Malformed input throws (the canonical TS vector + an excluded letter).
let threw = false;
try {
decodeUlidTime('not-a-ulid');
} catch {
threw = true;
}
console.assert(threw, 'invalid throws');
console.log('ulid-generator showcase: all assertions passed');
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →