tiny-ulid
Version:
Make and decode ULIDs.
175 lines (174 loc) • 6.9 kB
JavaScript
const Uint8Array_ = Uint8Array,
CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
ULID_REGEX = /^[0-7][\dA-HJKMNP-TV-Z]{25}$/,
isUlidBuffer = arrayBuffer => 16 == arrayBuffer.byteLength,
toUlidBuffer = arrayBuffer => {
if (isUlidBuffer(arrayBuffer)) return arrayBuffer
throw Error("ArrayBuffer's length was not 16")
},
isUlidBytes = bytes => 16 == bytes.length,
toUlidBytes = bytes => {
if (isUlidBytes(bytes)) return bytes
throw Error("Uint8Array's length was not 16")
},
ulidBufferToBytes = ulidBuffer => new Uint8Array_(ulidBuffer),
isUlid = string => ULID_REGEX.test(string),
toUlid = string => {
if (isUlid(string)) return string
throw Error("Invalid ULID.")
},
makeEmptyUlidBuffer = () => new ArrayBuffer(16),
setUlidBufferTime = (buffer, time = Date.now()) => {
const dataView = new DataView(buffer)
dataView.setUint16(0, Math.floor(time / 2 ** 32))
dataView.setUint32(2, time)
},
setUlidBytesTime = (ulidBytes, time = Date.now()) => {
ulidBytes[0] = time / 2 ** 40
ulidBytes[1] = time / 2 ** 32
ulidBytes[2] = time >> 24
ulidBytes[3] = time >> 16
ulidBytes[4] = time >> 8
ulidBytes[5] = time
},
getUlidBufferTime = buffer => {
const dataView = new DataView(buffer)
return dataView.getUint16(0) * 2 ** 32 + dataView.getUint32(2)
},
pool = new Uint8Array_(2560)
let poolOffset = 0
const makeUlidBuffer = ({ ulidBuffer = makeEmptyUlidBuffer(), time = Date.now() } = {}) => {
setUlidBufferTime(ulidBuffer, time)
poolOffset || crypto.getRandomValues(pool)
new Uint8Array_(ulidBuffer, 6).set(pool.slice(poolOffset, (poolOffset += 10)))
poolOffset %= 2560
return ulidBuffer
},
setUlidBytesRandom = ulidBytes => {
;(poolOffset %= 2560) || crypto.getRandomValues(pool)
ulidBytes.set(pool.subarray(poolOffset, (poolOffset += 10)), 6)
},
makeUlidBytes = () => {
const ulidBytes = makeEmptyUlidBytes()
setUlidBytesTime(ulidBytes)
setUlidBytesRandom(ulidBytes)
return ulidBytes
},
timeToUlidString = time => {
if (time > 0xffffffffffff) throw Error("ULID cannot represent a date more than 48 bits")
let result = ""
for (let offset = 50; offset;) result += CROCKFORD_BASE32[(time / 2 ** (offset -= 5)) & 31]
return result
},
ulidBufferRandomPartToUlidString = ulidBuffer => {
let result = ""
const dataView = new DataView(ulidBuffer)
for (let bitOffset = 48; bitOffset < 123; bitOffset += 5) {
const byteOffset = Math.floor(bitOffset / 8)
result += CROCKFORD_BASE32[(dataView.getUint16(byteOffset) >> (11 - (bitOffset % 8))) & 31]
}
return result + CROCKFORD_BASE32[31 & dataView.getUint8(15)]
},
persistedUlidStringBytes = new Uint8Array_(26),
textDecoder = new TextDecoder(),
CROCKFORD_BASE32_CHAR_CODES = CROCKFORD_BASE32.split("").map(char => char.charCodeAt(0)),
ulidBufferToString = ulidBuffer => {
const dataView = new DataView(ulidBuffer)
persistedUlidStringBytes[0] = CROCKFORD_BASE32_CHAR_CODES[dataView.getUint8(0) >> 5]
for (let bitOffset = 3, index = 1; bitOffset < 123; bitOffset += 5)
persistedUlidStringBytes[index++] =
CROCKFORD_BASE32_CHAR_CODES[
(dataView.getUint16(Math.floor(bitOffset / 8)) >> (11 - (bitOffset % 8))) & 31
]
persistedUlidStringBytes[25] = CROCKFORD_BASE32_CHAR_CODES[31 & dataView.getUint8(15)]
return textDecoder.decode(persistedUlidStringBytes)
},
makeEmptyUlidBytes = () => new Uint8Array_(16),
makeUlidPool = new Uint8Array_(4096)
let makeUlidPoolOffset = 0
const makeUlid = () => {
const time = Date.now()
persistedUlidStringBytes[0] = CROCKFORD_BASE32_CHAR_CODES[(time / 2 ** 45) & 31]
persistedUlidStringBytes[1] = CROCKFORD_BASE32_CHAR_CODES[(time / 2 ** 40) & 31]
persistedUlidStringBytes[2] = CROCKFORD_BASE32_CHAR_CODES[(time / 2 ** 35) & 31]
persistedUlidStringBytes[3] = CROCKFORD_BASE32_CHAR_CODES[(time / 2 ** 30) & 31]
for (let index = 4, offset = 30; index < 10;)
persistedUlidStringBytes[index++] = CROCKFORD_BASE32_CHAR_CODES[(time >> (offset -= 5)) & 31]
;(makeUlidPoolOffset %= 4096) || crypto.getRandomValues(makeUlidPool)
persistedUlidStringBytes.set(makeUlidPool.subarray(makeUlidPoolOffset, (makeUlidPoolOffset += 16)), 10)
for (let index = 10; index < 26;)
persistedUlidStringBytes[index] = CROCKFORD_BASE32_CHAR_CODES[31 & persistedUlidStringBytes[index++]]
return textDecoder.decode(persistedUlidStringBytes)
},
decodeUlid = ulid => {
const ulidBuffer = makeUlidBuffer(),
bytes = new Uint8Array_(ulidBuffer)
for (let bytesIndex = 0, ulidIndex = 0; bytesIndex < 15; bytesIndex++) {
bytes[bytesIndex] =
(CROCKFORD_BASE32.indexOf(ulid[ulidIndex]) << 5) | CROCKFORD_BASE32.indexOf(ulid[++ulidIndex])
bytes[++bytesIndex] =
(CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) << 3) | (CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) >> 2)
bytes[++bytesIndex] =
(CROCKFORD_BASE32.indexOf(ulid[ulidIndex]) << 6) |
(CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) << 1) |
(CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) >> 4)
bytes[++bytesIndex] =
(CROCKFORD_BASE32.indexOf(ulid[ulidIndex]) << 4) | (CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) >> 1)
bytes[++bytesIndex] =
(CROCKFORD_BASE32.indexOf(ulid[ulidIndex]) << 7) |
(CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) << 2) |
(CROCKFORD_BASE32.indexOf(ulid[++ulidIndex]) >> 3)
}
bytes[15] = (CROCKFORD_BASE32.indexOf(ulid[24]) << 5) | CROCKFORD_BASE32.indexOf(ulid[25])
return ulidBuffer
},
incrementUlidBuffer = (ulidBuffer, { throwOnOverflow = !1 } = {}) => {
const bytes = ulidBufferToBytes(ulidBuffer)
for (let index = 16; index > 6; index--) if ((bytes[index] = bytes[index] + 1)) return
if (throwOnOverflow) throw Error("Overflow when incrementing ULID buffer")
},
cloneUlidBuffer = ulidBuffer => {
const clone = makeEmptyUlidBytes()
clone.set(new Uint8Array_(ulidBuffer))
return clone.buffer
},
makeMonotonicallyIncrementingUlidBufferFunction = ({ mitigateOverflow = !0 } = {}) => {
const stateUlidBuffer = makeEmptyUlidBuffer(),
stateUlidBytes = new Uint8Array_(stateUlidBuffer)
return ({ ulidBuffer } = {}) => {
if (getUlidBufferTime(stateUlidBuffer) >= Date.now())
incrementUlidBuffer(stateUlidBuffer, { throwOnOverflow: !0 })
else {
makeUlidBuffer({ ulidBuffer: stateUlidBuffer })
mitigateOverflow && (stateUlidBytes[6] &= 127)
}
if (!ulidBuffer) return cloneUlidBuffer(stateUlidBuffer)
new Uint8Array_(ulidBuffer).set(stateUlidBytes)
return ulidBuffer
}
}
export {
cloneUlidBuffer,
decodeUlid,
getUlidBufferTime,
incrementUlidBuffer,
isUlid,
isUlidBuffer,
isUlidBytes,
makeEmptyUlidBuffer,
makeEmptyUlidBytes,
makeMonotonicallyIncrementingUlidBufferFunction,
makeUlid,
makeUlidBuffer,
makeUlidBytes,
setUlidBufferTime,
setUlidBytesRandom,
setUlidBytesTime,
timeToUlidString,
toUlid,
toUlidBuffer,
toUlidBytes,
ulidBufferRandomPartToUlidString,
ulidBufferToBytes,
ulidBufferToString
}