mmzk-typeid-0.3.0.0: src/Data/UUID/V7.hs
-- |
-- Module : Data.UUID.V7
-- License : MIT
-- Maintainer : mmzk1526@outlook.com
-- Portability : GHC
--
-- UUIDv7 implementation.
--
-- UUIDv7 is not currently present in the uuid package, therefore I have to
-- make a quick patch of my own.
--
-- Note that since the specification for v7 is not yet finalised, this module's
-- implementation may change in the future according to the potential
-- adjustments in the specification.
--
-- WARNING: The 'nil' re-export will be removed in the next major version.
--
module Data.UUID.V7
(
-- * Data type
UUID
-- * 'UUID'v7 generation
, nil
, genUUID
, genUUID'
, genUUIDs
-- Validation
, validate
, validateWithTime
-- * Miscellaneous helpers
, getTime
, getEpochMilli
) where
import Control.Monad
import Control.Monad.IO.Class
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.Bits
import qualified Data.ByteString.Lazy as BSL
import Data.IORef
import Data.Time.Clock.POSIX
import Data.UUID.Types.Internal
import System.Entropy
import System.IO.Unsafe (unsafePerformIO)
-- | Generate a 'UUID'v7.
genUUID :: MonadIO m => m UUID
genUUID = head <$> genUUIDs 1
{-# INLINE genUUID #-}
-- | Generate a stateless 'UUID'v7.
--
-- It is faster than 'genUUID' but it is not guaranteed to be monotonically
-- increasing if multiple 'UUID's are generated at the same timestamp.
--
-- In use cases where the ordering is not important, this function is could be
-- preferred.
genUUID' :: MonadIO m => m UUID
genUUID' = do
timestamp <- getEpochMilli
entropy16 <- getEntropyWord16
entropy64 <- getEntropyWord64
let bs = runPut do
fillTime timestamp
fillVerAndRandA entropy16
fillVarAndRandB entropy16 entropy64
pure . uncurry UUID $ runGet (join (liftM2 (,)) getWord64be) bs
-- | Generate a list of 'UUID'v7s.
--
-- It tries its best to generate 'UUID's at the same timestamp, but it may not
-- be possible if we are asking too many 'UUID's at the same time.
--
-- It is guaranteed that the first 32768 'UUID's are generated at the same
-- timestamp.
genUUIDs :: MonadIO m => Word16 -> m [UUID]
genUUIDs 0 = pure []
genUUIDs n = liftIO do
timestamp <- getEpochMilli
-- We set the first bit of the entropy to 0 to ensure that there's enough
-- room for incrementing the sequence number.
entropy16 <- (.&. 0x7FFF) <$> getEntropyWord16
-- Calculate the maximum number of slots we can use for the current timestamp
-- before the sequence number overflows.
let getMaxSlots num seqNo = if 0xFFFF - seqNo < num
then (0xFFFF - seqNo, 0xFFFF)
else (num, seqNo + num)
-- Get the sequence number corresponding to the current timestamp and the
-- number of UUIDs we can generate.
(n', seqNo) <- atomicModifyIORef __state__ $ \(ts, seqNo) -> if
| ts < timestamp -> let (n', entropy16') = getMaxSlots n entropy16
in ((timestamp, entropy16'), (n', entropy16 + 1))
| ts > timestamp -> ((ts, seqNo), (0, 0))
| otherwise -> let (n', entropy16') = getMaxSlots n seqNo
in ((timestamp, entropy16'), (n', seqNo + 1))
-- If we can't generate any UUIDs, we try again, hoping that the timestamp
-- has changed.
if n' == 0
then genUUIDs n
else do
uuids <- forM [0..(n' - 1)] $ \curN -> do
entropy64 <- getEntropyWord64
let bs = runPut do
fillTime timestamp
fillVerAndRandA (seqNo + curN)
fillVarAndRandB (seqNo + curN) entropy64
pure . uncurry UUID $ runGet (join (liftM2 (,)) getWord64be) bs
if n' == n then pure uuids else (uuids ++) <$> genUUIDs (n - n')
-- | Validate the version and variant of the 'UUID'v7.
validate :: UUID -> Bool
validate (UUID w1 w2)
= (w1 `shiftR` 12) .&. 0xF == 0x7 && (w2 `shiftR` 62) .&. 0x3 == 0x2
{-# INLINE validate #-}
-- | Validate the version and variant of the 'UUID'v7 as well as its timestamp
-- is no greater than the current time.
validateWithTime :: MonadIO m => UUID -> m Bool
validateWithTime uuid = do
curTime <- getEpochMilli
pure $ validate uuid && (getTime uuid <= curTime)
{-# INLINE validateWithTime #-}
-- | Get the current time in milliseconds since the Unix epoch.
getEpochMilli :: MonadIO m => m Word64
getEpochMilli = liftIO do
t <- getPOSIXTime
pure . round $ t * 1000
{-# INLINE getEpochMilli #-}
-- | Get the time field (unix_ts_ms) of a 'UUID'v7.
getTime :: UUID -> Word64
getTime (UUID w1 _) = w1 `shiftR` 16
{-# INLINE getTime #-}
-- | The global mutable state of (timestamp, sequence number).
--
-- The "NOINLINE" pragma is IMPORTANT! The logic would be flawed if it is
-- is inlined by its definition.
__state__ :: IORef (Word64, Word16)
__state__ = unsafePerformIO (newIORef (0, 0))
{-# NOINLINE __state__ #-}
-- | Fill in the 48-bit time field (unix_ts_ms) of a 'UUID'v7 with the given
-- time.
fillTime :: Word64 -> Put
fillTime timestamp = do
let (_, p2, p1, p0) = splitWord64ToWord16s timestamp
mapM_ putWord16be [p2, p1, p0]
{-# INLINE fillTime #-}
-- | Fill in the version and rand_a part of a 'UUID'v7 with the given sequence
-- number.
--
-- The sequence number is a 16-bit integer, of which the first 12 bits are used
-- here in rand_a, and the last 4 bits are used in rand_b. The version is 7.
fillVerAndRandA :: Word16 -> Put
fillVerAndRandA seqNo = do
let seqNoRandA = seqNo `shiftR` 4
let randAWithVer = seqNoRandA .|. (0x7 `shiftL` 12)
putWord16be randAWithVer
{-# INLINE fillVerAndRandA #-}
-- | Fill in the variant and rand_b part of a 'UUID'v7 with the given sequence
-- number and random number. The variant is 2.
--
-- The sequence number is a 16-bit integer, of which the last 4 bits are used
-- here in rand_b while the first 12 bits are used in rand_a.
--
-- The random number is a 64-bit integer of which the last 58 bits are used
-- while the rest are replaced by the variant bits and the last 4 bits of the
-- sequence number.
fillVarAndRandB :: Word16 -> Word64 -> Put
fillVarAndRandB seqNo entropy = do
let seqNoRandB = seqNo .&. 0xF
let randBWithVar = fromIntegral (seqNoRandB .|. (0x2 `shiftL` 4))
putWord64be $ (entropy .&. 0x3FFFFFFFFFFFFFF) .|. (randBWithVar `shiftL` 58)
{-# INLINE fillVarAndRandB #-}
splitWord64ToWord16s :: Word64 -> (Word16, Word16, Word16, Word16)
splitWord64ToWord16s n =
let b0 = fromIntegral (n .&. 0xFFFF)
b1 = fromIntegral ((n `shiftR` 16) .&. 0xFFFF)
b2 = fromIntegral ((n `shiftR` 32) .&. 0xFFFF)
b3 = fromIntegral ((n `shiftR` 48) .&. 0xFFFF)
in (b3, b2, b1, b0)
{-# INLINE splitWord64ToWord16s #-}
getEntropyWord16 :: MonadIO m => m Word16
getEntropyWord16 = liftIO do
bs <- BSL.fromStrict <$> getEntropy 2
pure $ runGet getWord16host bs
{-# INLINE getEntropyWord16 #-}
getEntropyWord64 :: MonadIO m => m Word64
getEntropyWord64 = liftIO do
bs <- BSL.fromStrict <$> getEntropy 8
pure $ runGet getWord64host bs
{-# INLINE getEntropyWord64 #-}