ulid
Version:
A universally unique lexicographically sortable identifier generator
185 lines (163 loc) • 4.87 kB
text/typescript
;
interface PRNG {
(): number
}
interface ULID {
(seedTime?: number): string
}
interface ExportedObject extends ULID {
createMonotonic(): ULID
prng: PRNG
incrementBase32(str: string): string
randomChar(): string
encodeTime(now: number, len: number): string
encodeRandom(len: number): string
decodeTime(id: string): number
factory(prng: PRNG): ExportedObject
}
const factory = (prng: PRNG): ExportedObject => {
// These values should NEVER change. If
// they do, we're no longer making ulids!
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" // Crockford's Base32
const ENCODING_LEN = ENCODING.length
const TIME_MAX = Math.pow(2, 48) - 1
const TIME_LEN = 10
const RANDOM_LEN = 16
function replaceCharAt(str: string, index: number, char: string) {
if (index > str.length - 1) {
return str;
}
return str.substr(0, index) + char + str.substr(index + 1);
}
function incrementBase32(str: string): string {
let done: string = undefined
let index = str.length
let char
let charIndex
const maxCharIndex = ENCODING_LEN - 1
while (!done && index-- >= 0) {
char = str[index]
charIndex = ENCODING.indexOf(char)
if (charIndex === -1) {
throw new Error("incorrectly encoded string")
}
if (charIndex === maxCharIndex) {
str = replaceCharAt(str, index, ENCODING[0])
continue
}
done = replaceCharAt(str, index, ENCODING[charIndex + 1])
}
if (typeof done === "string") {
return done
}
throw new Error("cannot increment this string")
}
function randomChar(): string {
let rand = Math.floor(prng() * ENCODING_LEN)
if (rand === ENCODING_LEN) {
rand = ENCODING_LEN - 1
}
return ENCODING.charAt(rand)
}
function encodeTime(now: number, len: number): string {
if (isNaN(now)) {
throw new Error(now + " must be a number")
}
if (now > TIME_MAX) {
throw new Error("cannot encode time greater than " + TIME_MAX)
}
if (now < 0) {
throw new Error("time must be positive")
}
if (Number.isInteger(now) === false) {
throw new Error("time must be an integer")
}
let mod
let str = ""
for (let x = len; x > 0; x--) {
mod = now % ENCODING_LEN
str = ENCODING.charAt(mod) + str
now = (now - mod) / ENCODING_LEN
}
return str
}
function encodeRandom(len: number): string {
let str = ""
for (; len > 0; len--) {
str = randomChar() + str
}
return str
}
function decodeTime(id: string): number {
if (id.length !== TIME_LEN + RANDOM_LEN) {
throw new Error("malformed ulid")
}
var time = id
.substr(0, TIME_LEN)
.split('')
.reverse()
.reduce((carry, char, index) => {
const encodingIndex = ENCODING.indexOf(char)
if (encodingIndex === -1) {
throw new Error("invalid character found: " + char)
}
return carry += encodingIndex * Math.pow(ENCODING_LEN, index)
}, 0)
if (time > TIME_MAX) {
throw new Error("malformed ulid, timestamp too large")
}
return time
}
function createMonotonic(): ULID {
let lastTime: number = 0
let lastRandom: string
return function ulid(seedTime?: number): string {
if (isNaN(seedTime)) {
seedTime = Date.now()
}
if (seedTime <= lastTime) {
const incrementedRandom = lastRandom = incrementBase32(lastRandom)
return encodeTime(lastTime, TIME_LEN) + incrementedRandom
}
lastTime = seedTime
const newRandom = lastRandom = encodeRandom(RANDOM_LEN)
return encodeTime(seedTime, TIME_LEN) + newRandom
}
}
const ulid = function ulid(seedTime?: number): string {
if (isNaN(seedTime)) {
seedTime = Date.now()
}
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN)
} as ExportedObject
ulid.prng = prng
ulid.createMonotonic = createMonotonic
ulid.incrementBase32 = incrementBase32
ulid.randomChar = randomChar
ulid.encodeTime = encodeTime
ulid.encodeRandom = encodeRandom
ulid.decodeTime = decodeTime
ulid.factory = factory
return ulid
}
/* istanbul ignore next */
function _prng(root): PRNG {
const browserCrypto = root && (root.crypto || root.msCrypto)
if (browserCrypto) {
try {
return () => browserCrypto.getRandomValues(new Uint8Array(1))[0] / 0xFF
} catch (e) {}
} else {
try {
const nodeCrypto = require("crypto")
return () => nodeCrypto.randomBytes(1).readUInt8() / 0xFF
} catch (e) {}
}
try {
console.error("[ulid] secure crypto unusable, falling back to insecure Math.random()!")
} catch (e) {}
return () => Math.random()
}
const root = typeof window !== "undefined" ? window : null
const prng = _prng(root)
export = factory(prng)