Skip to content

Commit

Permalink
New library version, rename NULID* types to ULID, remove async code (…
Browse files Browse the repository at this point in the history
…useless), add a lock and make threadsafe
  • Loading branch information
Yu-Vitaqua-fer-Chronos committed Oct 26, 2023
1 parent 57daeac commit 534ba1c
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 75 deletions.
2 changes: 1 addition & 1 deletion nulid.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.2.5"
version = "1.0.0"
author = "Yu Vitaqua fer Chronos"
description = "An implementation of ULID!"
license = "CC0"
Expand Down
155 changes: 84 additions & 71 deletions src/nulid.nim
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import std/[
asyncdispatch,
times
rlocks,
times,
os
]

import pkg/[
crockfordb32,
nint128
]
import crockfordb32
import nint128

import ./nulid/private/constants

Expand All @@ -21,35 +20,39 @@ else:
##[
Note: `--define:nulidInsecureRandom` can be passed to the compiler to make it
so that `std/random` is used instead of `std/sysrand`.
The JS backend and Nimscript use this by default (whether either work with NULID
is untested).
The JS backend and Nimscript use this by default (whether either work with NULID,
is assumed to be no, due to locks being used).
]##

type
NULID* = object
ULID* = object
## An object representing a ULID.
timestamp*: int64
randomness*: UInt128

NULIDGenerator* = ref object
## A NULID generator object, contains details needed to follow the spec.
## A generator was made to be compliant with the NULID spec and also to be
ULIDGenerator* = ref object
## A `ULID` generator object, contains details needed to follow the spec.
## A generator was made to be compliant with the ULID spec and also to be
## threadsafe not use globals that could change.
lastTime: int64 # Timestamp of last ULID
random: UInt128 # A random number
lock*: RLock
lastTime {.guard: lock.}: int64 # Timestamp of last ULID, 48 bits
random {.guard: lock.}: UInt128 # A random number, 80 bits

when InsecureRandom:
rand: Rand # Random generator when using insecure random
rand {.guard: lock.}: Rand # Random generator when using insecure random

proc initNulidGenerator*(): NULIDGenerator =
## Initialises a `NULIDGenerator` for use.
result = NULIDGenerator(lastTime: 0, random: 0.u128)
proc initUlidGenerator*(): ULIDGenerator =
## Initialises a `ULIDGenerator` for use.
result = ULIDGenerator(lock: RLock(), lastTime: 0, random: 0.u128)
initRLock(result.lock)

when InsecureRandom:
result.rand = initRand()
{.cast(gcsafe).}:
withRLock(result.lock):
result.rand = initRand()

# Discouraged to use it but it's fine for single-threaded apps really
let globalGen = initNulidGenerator()
let globalGen = initUlidGenerator()

func swapBytes(x: Int128): Int128 =
result.lo = swapBytes(cast[uint64](x.hi))
Expand All @@ -58,14 +61,16 @@ func swapBytes(x: Int128): Int128 =
func toArray[T](oa: openArray[T], size: static Slice[int]): array[size.len, T] =
result[0..<size.len] = oa[size]

proc randomBits(n: NULIDGenerator): UInt128 =
proc randomBits(n: ULIDGenerator): UInt128 {.gcsafe.} =
var arr: array[16, byte]

when InsecureRandom:
var rnd: array[10, byte]

rnd[0..7] = cast[array[8, byte]](n.rand.next())
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(int16)).int16)
{.cast(gcsafe).}:
withRLock(n.lock):
rnd[0..7] = cast[array[8, byte]](n.rand.next())
rnd[8..9] = cast[array[2, byte]](n.rand.rand(high(int16)).int16)

arr[6..15] = rnd

Expand All @@ -82,109 +87,117 @@ proc randomBits(n: NULIDGenerator): UInt128 =

template getTime: int64 = (epochTime() * 1000).int64

proc wait(gen: NULIDGenerator): Future[int64] {.async.} =
proc wait(gen: ULIDGenerator): int64 {.gcsafe.} =
result = getTime()

while result <= gen.lastTime:
await sleepAsync(1)
result = getTime()
{.cast(gcsafe).}:
withRLock(gen.lock):
while result <= gen.lastTime:
sleep(1)
result = getTime()

if result < gen.lastTime:
raise newException(OSError, "Time went backwards!")

proc ulid*(gen: ULIDGenerator, timestamp = 0'i64, randomness = u128(0)): ULID =
## Generate a `ULID`, if timestamp is equal to `0`, the `randomness` parameter
## will be ignored.
runnableExamples:
let gen = initUlidGenerator()

echo gen.ulid()

proc nulid*(gen: NULIDGenerator, timestamp: int64 = 0): Future[NULID] {.async.} =
## Asynchronously generate a `NULID`.
if timestamp == 0:
var now = getTime()

if now < gen.lastTime:
raise newException(OSError, "Time went backwards!")
{.cast(gcsafe).}:
withRLock(gen.lock):
if gen.lastTime == now:
inc gen.random

if gen.lastTime == now:
inc gen.random
if gen.random == HighUInt80:
now = gen.wait()
gen.random = gen.randomBits()

if gen.random == HighUInt80:
now = await gen.wait()
else:
gen.random = gen.randomBits()

else:
gen.random = gen.randomBits()
result.randomness = gen.random

result.timestamp = now

else:
result.timestamp = timestamp
result.randomness = gen.random

proc nulidSync*(gen: NULIDGenerator, timestamp: int64 = 0): NULID =
## Synchronously generate a `NULID`.
result = waitFor gen.nulid(timestamp)

proc nulid*(timestamp: int64 = 0): Future[NULID] =
## Asynchronously generate a `NULID` using the global generator.
result = nulid(globalGen, timestamp)

proc nulidSync*(timestamp: int64 = 0): NULID =
## Synchronously generate a `NULID` using the global generator.
result.timestamp = clamp(timestamp, 0, HighInt48)
result.randomness = clamp(randomness, low(UInt128), HighUint80)

proc ulid*(timestamp = 0'i64, randomness = u128(0)): ULID =
## Generate a `ULID` using the global generator.
##
## See also:
## * `ulid(ULIDGenerator, int64, UInt128) <#ulid,ULIDGenerator,int64>`_
runnableExamples:
echo nulidSync()
echo ulid()

result = waitFor nulid(timestamp)
result = ulid(globalGen, timestamp)

func toInt128*(ulid: NULID): Int128 =
## Allows for a ULID to be converted to an Int128.
func toInt128*(ulid: ULID): Int128 =
## Allows for a `ULID` to be converted to an Int128.
runnableExamples:
echo nulidSync().toInt128()
echo ulid().toInt128()

result = i128(ulid.timestamp) shl 80

result.hi += cast[int64](ulid.randomness.hi)
result.lo += ulid.randomness.lo

func fromInt128*(_: typedesc[NULID], val: Int128): NULID =
## Parses an Int128 to a NULID.
func fromInt128*(_: typedesc[ULID], val: Int128): ULID =
## Parses an Int128 to a ULID.
result.timestamp = (val shr 16).hi
result.randomness = UInt128(
hi: cast[uint64]((val.hi shl 48) shr 48),
lo: val.lo
)

func toBytes*(ulid: NULID): array[16, byte] =
## Allows for a NULID to be converted to a byte array.
func toBytes*(ulid: ULID): array[16, byte] =
## Allows for a `ULID` to be converted to a byte array for the binary format.
runnableExamples:
let
ulid = parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
ulid = ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")
ulidBytes = [1.byte, 138, 82, 154, 46, 26, 114, 144, 182, 228, 20, 42, 184, 229, 172, 42]

echo ulid == NULID.fromBytes(ulidBytes)
assert ulid == ULID.fromBytes(ulidBytes)

when cpuEndian == littleEndian:
return cast[array[16, byte]](ulid.toInt128().swapBytes())

else:
return cast[array[16, byte]](ulid.toInt128())

func fromBytes*(_: typedesc[NULID], ulidBytes: openArray[byte]): NULID =
## Parses a byte array to a NULID.
func fromBytes*(_: typedesc[ULID], ulidBytes: openArray[byte]): ULID =
## Parses a byte array to a `ULID.`.
if ulidBytes.len != 16:
raise newException(RangeDefect, "Given byte array must be 16 bytes long!")

when cpuEndian == littleEndian:
return NULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)).swapBytes())

else:
return NULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))
return ULID.fromInt128(cast[Int128](ulidBytes.toArray(0..15)))

func parseNulid*(ulidStr: string): NULID =
## Parses a string to a NULID.
func parse*(_: typedesc[ULID], ulidStr: string): ULID =
## Parses a `ULID` from a string.
runnableExamples:
echo parseNulid("01H999MBGTEA8BDS0M5AWEBB1A")
echo ULID.parse("01H999MBGTEA8BDS0M5AWEBB1A")

if ulidStr.len != 26:
raise newException(RangeDefect, "Invalid ULID! Must be 26 characters long!")

result.timestamp = int64.decode(ulidStr[0..9])
result.randomness = UInt128.decode(ulidStr[10..25])

func `$`*(ulid: NULID): string =
func `$`*(ulid: ULID): string =
## Returns the string representation of a ULID.
runnableExamples:
echo nulidSync()
echo $ulid()

result = Int128.encode(ulid.toInt128(), 26)
7 changes: 5 additions & 2 deletions src/nulid/private/constants.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import nint128

const HighUint80* = u128("1208925819614629174706176")
# No sysrand on the JS backend nor VM (does this even work on either?)
const
HighInt48* = 281474976710655'i64
HighUint80* = u128("1208925819614629174706176")

# No sysrand on the JS backend nor VM (though neither is expected to work).
const InsecureRandom* = defined(nulidInsecureRandom) or defined(js) or defined(nimvm)
2 changes: 1 addition & 1 deletion tests/test1.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import nulid

test "NULID Generation":
for _ in 0..5:
let nulid = nulidSync()
let nulid = nulid()
echo nulid

test "NULID Parsing":
Expand Down

0 comments on commit 534ba1c

Please sign in to comment.