@@ -14,6 +14,9 @@ import kotlin.random.Random
1414// ideally, they would all be private unless there's a compelling reason to expose these
1515
1616private const val TIMESTAMP_BIT_SIZE = 48
17+
18+ // this is really just used for testing
19+ internal const val TIMESTAMP_BYTE_SIZE = TIMESTAMP_BIT_SIZE / 8
1720internal const val MAX_TIME = (1L shl TIMESTAMP_BIT_SIZE ) - 1 // 2^48 - 1
1821private const val RANDOM_BIT_SIZE = 80
1922private const val ULID_BIT_SIZE = TIMESTAMP_BIT_SIZE + RANDOM_BIT_SIZE
@@ -52,6 +55,17 @@ private val ENCODING_CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray()
5255public value class ULID private constructor(
5356 public val value : ByteString ,
5457) : Comparable<ULID> {
58+ private constructor (timestamp: Long , randomness: ByteArray ) : this (
59+ Buffer ()
60+ .apply {
61+ // the timestamp we use is 48 bits, so we need to drop the uppermost 16 bits of a Long
62+ // toShort() uses the least significant 16 bits of a number
63+ writeShort((timestamp shr 32 ).toShort())
64+ writeInt(timestamp.toInt())
65+ write(randomness)
66+ }.readByteString(),
67+ )
68+
5569 init {
5670 require(value.size == ULID_BYTE_SIZE ) { " ULID should have $ULID_BYTE_SIZE bytes, but had ${value.size} " }
5771 }
@@ -76,6 +90,38 @@ public value class ULID private constructor(
7690 */
7791 override fun toString (): String = encodeCrockfordBase32(value)
7892
93+ // we actually do need a class bc we have to keep track of the generator's state
94+ // ... then again, do we really? This at least will make things faster than ripping it out
95+ public class MonotonicGenerator (
96+ private val secureRandom : Boolean = true ,
97+ ) {
98+ private var lastTimestamp = 0L
99+ private var lastRandom = ByteArray (RANDOM_BYTE_SIZE )
100+
101+ public fun next (): ULID {
102+ val timestamp = Clock .System .now().toEpochMilliseconds()
103+
104+ if (timestamp == lastTimestamp) {
105+ incrementRandom()
106+ } else {
107+ lastTimestamp = timestamp
108+ randomBytes(secureRandom, lastRandom)
109+ }
110+
111+ return ULID (lastTimestamp, lastRandom)
112+ }
113+
114+ private fun incrementRandom () {
115+ for (i in lastRandom.indices.reversed()) {
116+ if (lastRandom[i] != 0xFF .toByte()) {
117+ lastRandom[i]++
118+ return
119+ }
120+ }
121+ error(" Random component has reached maximum value" )
122+ }
123+ }
124+
79125 /* *
80126 * Companion object providing factory methods and constants for ULID creation and manipulation.
81127 */
@@ -117,24 +163,7 @@ public value class ULID private constructor(
117163 ): ULID {
118164 check(timestamp >= 0L ) { " Time must be non-negative" }
119165 check(timestamp <= MAX_TIME ) { " Time must be less than $MAX_TIME " }
120-
121- val buf = Buffer ()
122- // the timestamp we use is 48 bits, so we need to drop the uppermost 16 bits of a Long
123- // toShort() uses the least significant 16 bits of a number
124- buf.writeShort((timestamp shr 32 ).toShort())
125- buf.writeInt(timestamp.toInt())
126-
127- val randomness =
128- if (secureRandom) {
129- CryptoRand .Default .nextBytes(
130- ByteArray (RANDOM_BYTE_SIZE ),
131- )
132- } else {
133- Random .nextBytes(ByteArray (RANDOM_BYTE_SIZE ))
134- }
135- buf.write(randomness)
136-
137- return ULID (buf.readByteString())
166+ return ULID (timestamp, randomBytes(secureRandom))
138167 }
139168
140169 /* *
@@ -244,5 +273,20 @@ public value class ULID private constructor(
244273 }
245274 }
246275 }
276+
277+ private fun randomBytes (
278+ secure : Boolean ,
279+ buf : ByteArray? = null,
280+ ): ByteArray {
281+ val buffer = buf ? : ByteArray (RANDOM_BYTE_SIZE )
282+ if (secure) {
283+ CryptoRand .Default .nextBytes(
284+ buffer,
285+ )
286+ } else {
287+ Random .nextBytes(buffer)
288+ }
289+ return buffer
290+ }
247291 }
248292}
0 commit comments